@pkmn/engine 0.1.0-dev.83de29ee → 0.1.0-dev.84cc1a02

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;
@@ -320,7 +301,11 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
320
301
  // Pokémon Showdown's beforeTurnCallback shenanigans
321
302
  if (showdown and m1 == .Counter and m2 == .Counter) battle.rng.advance(1);
322
303
 
323
- const p1 = Rolls.speedTie(battle);
304
+ const p1 = if (showdown)
305
+ battle.rng.range(u8, 0, 2) == 0
306
+ else
307
+ battle.rng.next() < Gen12.percent(50) + 1;
308
+
324
309
  if (!showdown) return if (p1) .P1 else .P2;
325
310
 
326
311
  // Pokémon Showdown's "lockedmove" volatile's onBeforeTurn uses BattleQueue#changeAction,
@@ -330,7 +315,7 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
330
315
  const t1 = battle.side(.P1).active.volatiles.Thrashing;
331
316
  const t2 = battle.side(.P2).active.volatiles.Thrashing;
332
317
  // If *both* sides are thrashing it really should be another speed tie, but we've patched
333
- // 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
334
319
  // won the original coin flip
335
320
  if (t1 and t2) return .P1;
336
321
  return if (p1) if (t1 and !t2) .P2 else .P1 else if (t2 and !t1) .P1 else .P2;
@@ -343,13 +328,9 @@ fn doTurn(
343
328
  battle: anytype,
344
329
  player: Player,
345
330
  player_choice: Choice,
346
- player_rewrap: bool,
347
- player_skip: bool,
348
331
  player_from: ?Move,
349
332
  foe_player: Player,
350
333
  foe_choice: Choice,
351
- foe_rewrap: bool,
352
- foe_skip: bool,
353
334
  foe_from: ?Move,
354
335
  log: anytype,
355
336
  ) !?Result {
@@ -357,16 +338,7 @@ fn doTurn(
357
338
 
358
339
  var residual = true;
359
340
  var replace = battle.side(player).stored().hp == 0;
360
- if (try executeMove(
361
- battle,
362
- player,
363
- player_choice,
364
- player_rewrap,
365
- player_skip,
366
- player_from,
367
- &residual,
368
- log,
369
- )) |r| return r;
341
+ if (try executeMove(battle, player, player_choice, player_from, &residual, log)) |r| return r;
370
342
  if (!replace) {
371
343
  if (player_choice.type != .Switch) {
372
344
  if (try checkFaint(battle, foe_player, log)) |r| return r;
@@ -377,16 +349,7 @@ fn doTurn(
377
349
 
378
350
  residual = true;
379
351
  replace = battle.side(foe_player).stored().hp == 0;
380
- if (try executeMove(
381
- battle,
382
- foe_player,
383
- foe_choice,
384
- foe_rewrap,
385
- foe_skip,
386
- foe_from,
387
- &residual,
388
- log,
389
- )) |r| return r;
352
+ if (try executeMove(battle, foe_player, foe_choice, foe_from, &residual, log)) |r| return r;
390
353
  if (!replace) {
391
354
  if (foe_choice.type != .Switch) {
392
355
  if (try checkFaint(battle, player, log)) |r| return r;
@@ -409,8 +372,6 @@ fn executeMove(
409
372
  battle: anytype,
410
373
  player: Player,
411
374
  choice: Choice,
412
- rewrap: bool,
413
- skip: bool,
414
375
  from: ?Move,
415
376
  residual: *bool,
416
377
  log: anytype,
@@ -422,10 +383,9 @@ fn executeMove(
422
383
  return null;
423
384
  }
424
385
 
425
- // This is the correct place to check for SKIP_TURN and abort early, however since Pokémon
426
- // Showdown overwrites the SKIP_TURN sentinel with its botched move select we need to add an
427
- // additional skip boolean to accomplish the same thing in the Binding check of BeforeMove
428
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
429
389
  assert(!showdown);
430
390
  if (battle.foe(player).active.volatiles.Binding) {
431
391
  try log.cant(battle.active(player), .Bound);
@@ -444,9 +404,9 @@ fn executeMove(
444
404
  // choice.data == 0 only happens with Struggle on Pokémon Showdown
445
405
  assert(!showdown);
446
406
  mslot = @intCast(u4, if (player == .P1)
447
- battle.last_moves.p1_index
407
+ battle.last_selected_indexes.p1
448
408
  else
449
- battle.last_moves.p2_index);
409
+ battle.last_selected_indexes.p2);
450
410
  const stored = side.stored();
451
411
  // GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
452
412
  auto = side.last_selected_move == .HyperBeam or side.active.volatiles.Bide or
@@ -468,14 +428,14 @@ fn executeMove(
468
428
  // Incorrect mslot with Pokémon Showdown choice semantics so we need to recover from index
469
429
  assert(mslot == 1);
470
430
  mslot = @intCast(u4, if (player == .P1)
471
- battle.last_moves.p1_index
431
+ battle.last_selected_indexes.p1
472
432
  else
473
- battle.last_moves.p2_index);
433
+ battle.last_selected_indexes.p2);
474
434
  }
475
435
 
476
436
  var skip_can = false;
477
437
  var skip_pp = false;
478
- switch (try beforeMove(battle, player, skip, from, residual, log)) {
438
+ switch (try beforeMove(battle, player, from, residual, log)) {
479
439
  .done => return null,
480
440
  .skip_can => skip_can = true,
481
441
  .skip_pp => skip_pp = true,
@@ -487,15 +447,14 @@ fn executeMove(
487
447
  return null;
488
448
  }
489
449
 
490
- return doMove(battle, player, mslot, rewrap, auto, residual, log);
450
+ return doMove(battle, player, mslot, auto, residual, log);
491
451
  }
492
452
 
493
- const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
453
+ const BeforeMove = union(enum) { done, skip_can, skip_pp, ok, err };
494
454
 
495
455
  fn beforeMove(
496
456
  battle: anytype,
497
457
  player: Player,
498
- skip: bool,
499
458
  from: ?Move,
500
459
  residual: *bool,
501
460
  log: anytype,
@@ -527,15 +486,13 @@ fn beforeMove(
527
486
  return .done;
528
487
  }
529
488
 
530
- if (skip or foe.active.volatiles.Binding) {
489
+ if (foe.active.volatiles.Binding) {
531
490
  try log.cant(ident, .Bound);
532
491
  return .done;
533
492
  }
534
493
 
535
- if (volatiles.Flinch) {
536
- // Pokémon Showdown doesn't clear Flinch until its imaginary "residual" phase, meaning
537
- // Pokémon can sometimes flinch multiple times from the same original hit
538
- if (!showdown) volatiles.Flinch = false;
494
+ if (!showdown and volatiles.Flinch) {
495
+ volatiles.Flinch = false;
539
496
  try log.cant(ident, .Flinch);
540
497
  return .done;
541
498
  }
@@ -556,6 +513,13 @@ fn beforeMove(
556
513
  // Pokémon Showdown's disable condition has a single onBeforeMove handler
557
514
  if (showdown and try disabled(side, ident, log)) return .done;
558
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
+
559
523
  // This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
560
524
  // before the side had a selected a move - we simply need to assume this leads to a desync
561
525
  if (side.last_selected_move == .None) {
@@ -573,7 +537,12 @@ fn beforeMove(
573
537
  } else {
574
538
  try log.activate(ident, .Confusion);
575
539
 
576
- 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) {
577
546
  assert(!volatiles.MultiHit);
578
547
  if (!volatiles.Rage) volatiles.state = 0;
579
548
  volatiles.Bide = false;
@@ -599,12 +568,8 @@ fn beforeMove(
599
568
  player
600
569
  else
601
570
  player.foe();
602
-
603
- const uncapped = battle.last_damage;
604
571
  // Skipping adjustDamage / randomizeDamage / checkHit
605
572
  _ = try applyDamage(battle, player, target, .Confusion, log);
606
- // Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
607
- if (showdown) battle.last_damage = uncapped;
608
573
 
609
574
  return .done;
610
575
  }
@@ -613,15 +578,22 @@ fn beforeMove(
613
578
 
614
579
  if (!showdown and try disabled(side, ident, log)) return .done;
615
580
 
616
- if (Status.is(stored.status, .PAR) and Rolls.paralyzed(battle)) {
617
- if (!volatiles.Rage) volatiles.state = 0;
618
- volatiles.Bide = false;
619
- volatiles.Thrashing = false;
620
- volatiles.Charging = false;
621
- volatiles.Binding = false;
622
- // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
623
- try log.cant(ident, .Paralysis);
624
- 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
+ }
625
597
  }
626
598
 
627
599
  if (volatiles.Bide) {
@@ -648,9 +620,8 @@ fn beforeMove(
648
620
  return .done;
649
621
  }
650
622
 
651
- const sub = showdown and foe.active.volatiles.Substitute;
652
623
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
653
- if (foe.stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
624
+ if (foe.stored().hp > 0) try buildRage(battle, player.foe(), log);
654
625
 
655
626
  // For reasons passing understanding, Pokémon Showdown still inflicts residual damage to
656
627
  // Bide's user even if the above damage has caused the foe to faint. It's simpler to always
@@ -671,13 +642,8 @@ fn beforeMove(
671
642
  try log.start(battle.active(player), .ConfusionSilent);
672
643
  }
673
644
  try log.move(ident, side.last_selected_move, battle.active(player.foe()), from);
674
- if (showdown) {
675
- // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
676
- // used in useMove and doesn't have the notion of skipping canMove semantics
677
- side.last_used_move = side.last_selected_move;
678
- if (handleThrashing(battle, active)) {
679
- try log.start(battle.active(player), .ConfusionSilent);
680
- }
645
+ if (showdown and handleThrashing(battle, active)) {
646
+ try log.start(battle.active(player), .ConfusionSilent);
681
647
  }
682
648
  return .skip_can;
683
649
  }
@@ -692,11 +658,7 @@ fn beforeMove(
692
658
  side.last_selected_move,
693
659
  );
694
660
  if (showdown or battle.last_damage != 0) {
695
- const sub = showdown and foe.active.volatiles.Substitute;
696
661
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
697
- if (battle.foe(player).stored().hp > 0 and !sub) {
698
- try buildRage(battle, player.foe(), log);
699
- }
700
662
  }
701
663
  return .done;
702
664
  }
@@ -745,19 +707,6 @@ fn canMove(
745
707
  const f = if (from != null and from.? == .None) null else from;
746
708
  try log.move(player_ident, side.last_selected_move, battle.active(target), f);
747
709
 
748
- // The Counter desync is caused by the cartridge not calling GetCurrentMove until now, meaning
749
- // in cases where an early return happens the data for a players last selected move does not get
750
- // reloaded and HandleCounterMove actually bases its success/failure off of stale information.
751
- // This boolean state we track here doesn't exist on the cartridge because it instead manifests
752
- // as the desync results from actually having two separate battle states that subtly disagree.
753
- const counterable = side.last_selected_move != .Counter and move.bp > 0 and
754
- (move.type == .Normal or move.type == .Fighting);
755
- if (player == .P1) {
756
- battle.last_moves.p1_counterable = @boolToInt(counterable);
757
- } else {
758
- battle.last_moves.p2_counterable = @boolToInt(counterable);
759
- }
760
-
761
710
  if (move.effect.onBegin()) {
762
711
  try onBegin(battle, player, move, mslot, residual, log);
763
712
  return false;
@@ -767,7 +716,7 @@ fn canMove(
767
716
  Effects.thrashing(battle, player);
768
717
  } else if (!showdown and move.effect == .Binding) {
769
718
  // Pokémon Showdown handles this after hit/miss checks and damage calculation
770
- Effects.binding(battle, player, false);
719
+ Effects.binding(battle, player);
771
720
  }
772
721
 
773
722
  return true;
@@ -808,7 +757,6 @@ fn doMove(
808
757
  battle: anytype,
809
758
  player: Player,
810
759
  mslot: u4,
811
- rewrap: bool,
812
760
  auto: bool,
813
761
  residual: *bool,
814
762
  log: anytype,
@@ -839,15 +787,12 @@ fn doMove(
839
787
  (@enumToInt(move.type.effectiveness(foe.active.types.type1)) == 0 or
840
788
  @enumToInt(move.type.effectiveness(foe.active.types.type2)) == 0);
841
789
  if (immune and move.effect != .Binding) break :miss true;
842
- if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) {
843
- battle.last_damage = 0;
844
- break :miss true;
845
- }
790
+ if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) break :miss true;
846
791
  break :miss move.target != .Self and !moveHit(battle, player, move, &immune, &mist);
847
792
  };
848
793
  assert(!immune or miss or (showdown and move.effect == .Binding));
849
794
 
850
- var late = showdown and move.effect != .Explode;
795
+ var late = showdown;
851
796
  const skip = status or immune;
852
797
  if ((!showdown or (!skip or counter)) and !miss) blk: {
853
798
  if (showdown and move.effect.isMulti()) {
@@ -900,9 +845,9 @@ fn doMove(
900
845
 
901
846
  if (!showdown or !miss) {
902
847
  if (move.effect == .MirrorMove) {
903
- return mirrorMove(battle, player, mslot, rewrap, auto, residual, log);
848
+ return mirrorMove(battle, player, mslot, auto, residual, log);
904
849
  } else if (move.effect == .Metronome) {
905
- return metronome(battle, player, mslot, rewrap, auto, residual, log);
850
+ return metronome(battle, player, mslot, auto, residual, log);
906
851
  } else if (move.effect.onEnd()) {
907
852
  try onEnd(battle, player, move, log);
908
853
  return null;
@@ -917,7 +862,7 @@ fn doMove(
917
862
  move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
918
863
  if (ohko) {
919
864
  try log.immune(foe_ident, .OHKO);
920
- } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
865
+ } else if (immune and !invulnerable) {
921
866
  try log.immune(foe_ident, .None);
922
867
  } else if (mist) {
923
868
  if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
@@ -949,9 +894,6 @@ fn doMove(
949
894
  hits = side.active.volatiles.attacks;
950
895
  }
951
896
 
952
- // Pokémon Showdown only builds Rage for Disable/Explosion (hit/miss) when attacking into a sub
953
- const sub = showdown and foe.active.volatiles.Substitute;
954
-
955
897
  var nullified = false;
956
898
  var hit: u4 = 0;
957
899
  while (hit < hits) {
@@ -967,7 +909,7 @@ fn doMove(
967
909
  if (hit == 0 and ohko) try log.ohko();
968
910
  hit += 1;
969
911
  if (foe.stored().hp == 0) break;
970
- if (!late and (!sub or move.effect == .Explode)) try buildRage(battle, player.foe(), log);
912
+ if (!late) try buildRage(battle, player.foe(), log);
971
913
  // If the substitute breaks during a multi-hit attack, the attack ends
972
914
  if (nullified) break;
973
915
  }
@@ -983,17 +925,11 @@ fn doMove(
983
925
  } else if (showdown) {
984
926
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
985
927
  if (move.effect == .Binding) {
986
- Effects.binding(battle, player, rewrap);
928
+ Effects.binding(battle, player);
987
929
  if (immune) {
988
930
  battle.last_damage = 0;
989
- assert(foe.stored().hp > 0);
990
931
  // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
991
- if (sub) {
992
- try log.activate(battle.active(player.foe()), .Substitute);
993
- } else {
994
- try log.damage(battle.active(player.foe()), foe.stored(), .None);
995
- try buildRage(battle, player.foe(), log);
996
- }
932
+ try log.damage(battle.active(player.foe()), foe.stored(), .None);
997
933
  return null;
998
934
  }
999
935
  }
@@ -1010,8 +946,8 @@ fn doMove(
1010
946
 
1011
947
  if (foe.stored().hp == 0) return null;
1012
948
 
1013
- // Pokémon Showdown builds Rage at the wrong time for non-MultiHit move
1014
- if (late and !sub and move.effect != .Disable) try buildRage(battle, player.foe(), log);
949
+ // Pokémon Showdown builds Rage at the wrong time for non-MultiHit moves
950
+ if (late and move.effect != .Disable) try buildRage(battle, player.foe(), log);
1015
951
 
1016
952
  if (!move.effect.isSpecial()) {
1017
953
  // On the cartridge Rage is not considered to be "special" and thus gets executed for a
@@ -1044,7 +980,8 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
1044
980
  chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
1045
981
  chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
1046
982
 
1047
- return Rolls.criticalHit(battle, chance);
983
+ if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
984
+ return std.math.rotl(u8, battle.rng.next(), 3) < chance;
1048
985
  }
1049
986
 
1050
987
  fn calcDamage(
@@ -1148,36 +1085,50 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
1148
1085
 
1149
1086
  fn randomizeDamage(battle: anytype) void {
1150
1087
  if (battle.last_damage <= 1) return;
1151
- const random = Rolls.damage(battle);
1088
+
1089
+ const random = if (showdown)
1090
+ battle.rng.range(u8, 217, 256)
1091
+ else loop: {
1092
+ while (true) {
1093
+ const r = std.math.rotr(u8, battle.rng.next(), 1);
1094
+ if (r >= 217) break :loop r;
1095
+ }
1096
+ };
1097
+
1152
1098
  battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
1153
1099
  }
1154
1100
 
1155
1101
  fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
1156
1102
  const side = battle.side(player);
1157
- const foe = battle.foe(player);
1158
1103
 
1159
1104
  if (!try checkHit(battle, player, move, log)) return null;
1160
1105
 
1161
1106
  battle.last_damage = switch (side.last_selected_move) {
1162
- .SuperFang => @max(foe.stored().hp / 2, 1),
1107
+ .SuperFang => @max(battle.foe(player).stored().hp / 2, 1),
1163
1108
  .SeismicToss, .NightShade => side.stored().level,
1164
1109
  .SonicBoom => 20,
1165
1110
  .DragonRage => 40,
1166
1111
  // GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
1167
1112
  .Psywave => power: {
1168
1113
  const max = @intCast(u8, @as(u16, side.stored().level) * 3 / 2);
1169
- // GLITCH: Psywave infinite glitch loop
1170
- if (!showdown and max <= 1) return Result.Error;
1171
- break :power Rolls.psywave(battle, max);
1114
+ if (showdown) {
1115
+ break :power battle.rng.range(u8, 0, max);
1116
+ } else {
1117
+ // GLITCH: Psywave infinite glitch loop
1118
+ if (max <= 1) return Result.Error;
1119
+ while (true) {
1120
+ const r = battle.rng.next();
1121
+ if (r < max) break :power r;
1122
+ }
1123
+ }
1172
1124
  },
1173
1125
  else => unreachable,
1174
1126
  };
1175
1127
 
1176
1128
  if (battle.last_damage == 0) return if (showdown) null else Result.Error;
1177
1129
 
1178
- const sub = showdown and foe.active.volatiles.Substitute;
1179
1130
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
1180
- if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
1131
+ if (battle.foe(player).stored().hp > 0) try buildRage(battle, player.foe(), log);
1181
1132
 
1182
1133
  return null;
1183
1134
  }
@@ -1190,17 +1141,19 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
1190
1141
  return null;
1191
1142
  }
1192
1143
 
1193
- // Pretend Splash was used as a stand-in when no move has been used to fail below with 0 BP
1144
+ // Pretend Counter was used as a stand-in when no move has been used to fail below with 0 BP
1145
+ const foe_last_used_move =
1146
+ Move.get(if (foe.last_used_move == .None) .Counter else foe.last_used_move);
1194
1147
  const foe_last_selected_move =
1195
1148
  Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
1196
- .Splash
1149
+ .Counter
1197
1150
  else
1198
1151
  foe.last_selected_move);
1199
1152
 
1200
- const used = (if (player.foe() == .P1)
1201
- battle.last_moves.p1_counterable
1202
- else
1203
- battle.last_moves.p2_counterable) != 0;
1153
+ const used = foe_last_used_move.bp > 0 and
1154
+ foe.last_used_move != .Counter and
1155
+ (foe_last_used_move.type == .Normal or
1156
+ foe_last_used_move.type == .Fighting);
1204
1157
 
1205
1158
  const selected = foe_last_selected_move.bp > 0 and
1206
1159
  foe.last_selected_move != .Counter and
@@ -1224,9 +1177,7 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
1224
1177
  // Pokémon Showdown calls checkHit before Counter
1225
1178
  if (!showdown and !try checkHit(battle, player, move, log)) return null;
1226
1179
 
1227
- const sub = showdown and foe.active.volatiles.Substitute;
1228
1180
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
1229
- if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
1230
1181
  return null;
1231
1182
  }
1232
1183
 
@@ -1270,7 +1221,6 @@ fn mirrorMove(
1270
1221
  battle: anytype,
1271
1222
  player: Player,
1272
1223
  mslot: u4,
1273
- rewrap: bool,
1274
1224
  auto: bool,
1275
1225
  residual: *bool,
1276
1226
  log: anytype,
@@ -1283,30 +1233,41 @@ fn mirrorMove(
1283
1233
  if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
1284
1234
  try log.fail(battle.active(player), .None);
1285
1235
  return null;
1286
- } else if (!showdown or foe.last_used_move != .Struggle) {
1287
- incrementPP(side, mslot);
1288
1236
  }
1289
1237
 
1238
+ incrementPP(side, mslot);
1239
+
1290
1240
  if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
1291
- return doMove(battle, player, mslot, rewrap, auto, residual, log);
1241
+ return doMove(battle, player, mslot, auto, residual, log);
1292
1242
  }
1293
1243
 
1294
1244
  fn metronome(
1295
1245
  battle: anytype,
1296
1246
  player: Player,
1297
1247
  mslot: u4,
1298
- rewrap: bool,
1299
1248
  auto: bool,
1300
1249
  residual: *bool,
1301
1250
  log: anytype,
1302
1251
  ) !?Result {
1303
1252
  var side = battle.side(player);
1304
1253
 
1305
- side.last_selected_move = Rolls.metronome(battle);
1254
+ side.last_selected_move = if (showdown) blk: {
1255
+ const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
1256
+ const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
1257
+ break :blk @intToEnum(Move, r + mod);
1258
+ } else loop: {
1259
+ while (true) {
1260
+ const r = battle.rng.next();
1261
+ if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
1262
+ if (r >= @enumToInt(Move.Struggle)) continue;
1263
+ break :loop @intToEnum(Move, r);
1264
+ }
1265
+ };
1266
+
1306
1267
  incrementPP(side, mslot);
1307
1268
 
1308
1269
  if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
1309
- return doMove(battle, player, mslot, rewrap, auto, residual, log);
1270
+ return doMove(battle, player, mslot, auto, residual, log);
1310
1271
  }
1311
1272
 
1312
1273
  fn checkHit(battle: anytype, player: Player, move: Move.Data, log: anytype) !bool {
@@ -1360,24 +1321,23 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
1360
1321
  }
1361
1322
 
1362
1323
  // GLITCH: Thrash / Petal Dance / Rage get their accuracy overwritten on subsequent hits
1363
- const state = side.active.volatiles.state;
1364
- var overwrite = move.effect == .Rage or move.effect == .Thrashing;
1365
- const overwritten = overwrite and state > 0;
1366
- assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
1367
-
1368
- var accuracy = if (overwritten) state else @as(u16, Gen12.percent(move.accuracy));
1324
+ var state = &side.active.volatiles.state;
1325
+ const overwritten = (move.effect == .Thrashing or move.effect == .Rage) and state.* > 0;
1326
+ assert(!overwritten or (0 < state.* and state.* <= 255 and !side.active.volatiles.Bide));
1327
+ var accuracy = if (overwritten) state.* else @as(u16, Gen12.percent(move.accuracy));
1369
1328
  var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
1370
1329
  accuracy = accuracy * boost[0] / boost[1];
1371
1330
  boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
1372
1331
  accuracy = accuracy * boost[0] / boost[1];
1373
1332
  accuracy = @min(255, @max(1, accuracy));
1374
1333
 
1375
- // Pokémon Showdown only overwrites if the volatile is present
1376
- if (showdown) overwrite = side.active.volatiles.Rage or side.active.volatiles.Thrashing;
1377
- if (overwrite) side.active.volatiles.state = accuracy;
1334
+ if (move.effect == .Thrashing or move.effect == .Rage) state.* = accuracy;
1378
1335
 
1379
1336
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1380
- break :miss !Rolls.hit(battle, accuracy);
1337
+ break :miss if (showdown)
1338
+ !battle.rng.chance(u8, @intCast(u8, accuracy), 256)
1339
+ else
1340
+ battle.rng.next() >= accuracy;
1381
1341
  };
1382
1342
 
1383
1343
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1509,6 +1469,9 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1509
1469
  damage *= volatiles.toxic;
1510
1470
  }
1511
1471
 
1472
+ // Pokémon Showdown erroneously updates last damage with uncapped Leech Seed damage
1473
+ if (showdown) battle.last_damage = damage;
1474
+
1512
1475
  const amount = @min(damage, stored.hp);
1513
1476
  stored.hp -= amount;
1514
1477
 
@@ -1600,42 +1563,6 @@ fn checkEBC(battle: anytype) bool {
1600
1563
  return false;
1601
1564
  }
1602
1565
 
1603
- fn disabled(side: *Side, ident: ID, log: anytype) !bool {
1604
- if (side.active.volatiles.disabled_move != 0) {
1605
- // A Pokémon that transforms after being disabled may end up with less move slots
1606
- const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
1607
- // side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
1608
- const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
1609
- m
1610
- else
1611
- side.last_selected_move;
1612
- if (m != .None and m == last) {
1613
- side.active.volatiles.Charging = false;
1614
- try log.disabled(ident, last);
1615
- return true;
1616
- }
1617
- }
1618
- return false;
1619
- }
1620
-
1621
- inline fn buildRage(battle: anytype, who: Player, log: anytype) !void {
1622
- const side = battle.side(who);
1623
- if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
1624
- try Effects.boost(battle, who, Move.get(.Rage), log);
1625
- }
1626
- }
1627
-
1628
- fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
1629
- var volatiles = &active.volatiles;
1630
- assert(volatiles.Thrashing);
1631
- if (volatiles.attacks > 0) return false;
1632
-
1633
- volatiles.Thrashing = false;
1634
- volatiles.Confusion = true;
1635
- volatiles.confusion = Rolls.confusionDuration(battle);
1636
- return true;
1637
- }
1638
-
1639
1566
  inline fn onBegin(
1640
1567
  battle: anytype,
1641
1568
  player: Player,
@@ -1726,7 +1653,10 @@ pub const Effects = struct {
1726
1653
  side.active.volatiles.Bide = true;
1727
1654
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1728
1655
  side.active.volatiles.state = 0;
1729
- side.active.volatiles.attacks = Rolls.bideThrashDuration(battle);
1656
+ side.active.volatiles.attacks = @intCast(u3, if (showdown)
1657
+ battle.rng.range(u4, 2, 4)
1658
+ else
1659
+ (battle.rng.next() & 1) + 2);
1730
1660
 
1731
1661
  try log.start(battle.active(player), .Bide);
1732
1662
  }
@@ -1748,8 +1678,14 @@ pub const Effects = struct {
1748
1678
  return;
1749
1679
  }
1750
1680
 
1751
- if (foe.active.types.includes(move.type)) return;
1752
- if (!Rolls.secondaryChance(battle, move.effect == .BurnChance1)) return;
1681
+ const chance = !foe.active.types.includes(move.type) and if (showdown)
1682
+ battle.rng.chance(u8, @as(u8, if (move.effect == .BurnChance1) 26 else 77), 256)
1683
+ else
1684
+ battle.rng.next() < 1 + (if (move.effect == .BurnChance1)
1685
+ Gen12.percent(10)
1686
+ else
1687
+ Gen12.percent(30));
1688
+ if (!chance) return;
1753
1689
 
1754
1690
  foe_stored.status = Status.init(.BRN);
1755
1691
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
@@ -1773,7 +1709,11 @@ pub const Effects = struct {
1773
1709
  const sub = foe.active.volatiles.Substitute;
1774
1710
 
1775
1711
  if (move.effect == .ConfusionChance) {
1776
- if (!Rolls.confusionChance(battle)) return;
1712
+ const chance = if (showdown)
1713
+ battle.rng.chance(u8, 25, 256)
1714
+ else
1715
+ battle.rng.next() < Gen12.percent(10);
1716
+ if (!chance) return;
1777
1717
  } else {
1778
1718
  if (showdown) {
1779
1719
  if (!try checkHit(battle, player, move, log)) {
@@ -1792,7 +1732,10 @@ pub const Effects = struct {
1792
1732
 
1793
1733
  if (foe.active.volatiles.Confusion) return;
1794
1734
  foe.active.volatiles.Confusion = true;
1795
- foe.active.volatiles.confusion = Rolls.confusionDuration(battle);
1735
+ foe.active.volatiles.confusion = @intCast(u3, if (showdown)
1736
+ battle.rng.range(u8, 2, 6)
1737
+ else
1738
+ (battle.rng.next() & 3) + 2);
1796
1739
 
1797
1740
  try log.start(battle.active(player.foe()), .Confusion);
1798
1741
  }
@@ -1842,8 +1785,11 @@ pub const Effects = struct {
1842
1785
  return Result.Error;
1843
1786
  }
1844
1787
 
1845
- volatiles.disabled_move = @intCast(u3, Rolls.moveSlot(battle, &foe.active.moves, n));
1846
- volatiles.disabled_duration = Rolls.disableDuration(battle);
1788
+ volatiles.disabled_move = @intCast(u3, randomMoveSlot(&battle.rng, &foe.active.moves, n));
1789
+ volatiles.disabled_duration = @intCast(u4, if (showdown)
1790
+ battle.rng.range(u8, 1, 9)
1791
+ else
1792
+ (battle.rng.next() & 7) + 1);
1847
1793
 
1848
1794
  try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
1849
1795
  return null;
@@ -1855,7 +1801,7 @@ pub const Effects = struct {
1855
1801
 
1856
1802
  if (battle.last_damage == 0) {
1857
1803
  assert(showdown);
1858
- if (!battle.foe(player).active.volatiles.Substitute) return;
1804
+ return;
1859
1805
  }
1860
1806
 
1861
1807
  const drain = @max(battle.last_damage / 2, 1);
@@ -1882,7 +1828,15 @@ pub const Effects = struct {
1882
1828
  var volatiles = &battle.foe(player).active.volatiles;
1883
1829
 
1884
1830
  if (volatiles.Substitute) return;
1885
- if (!Rolls.secondaryChance(battle, move.effect == .FlinchChance1)) return;
1831
+
1832
+ const chance = if (showdown)
1833
+ battle.rng.chance(u8, @as(u8, if (move.effect == .FlinchChance1) 26 else 77), 256)
1834
+ else
1835
+ battle.rng.next() < 1 + (if (move.effect == .FlinchChance1)
1836
+ Gen12.percent(10)
1837
+ else
1838
+ Gen12.percent(30));
1839
+ if (!chance) return;
1886
1840
 
1887
1841
  volatiles.Flinch = true;
1888
1842
  volatiles.Recharging = false;
@@ -1908,8 +1862,12 @@ pub const Effects = struct {
1908
1862
  return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
1909
1863
  }
1910
1864
 
1911
- if (foe.active.types.includes(move.type)) return;
1912
- if (!Rolls.secondaryChance(battle, true)) return;
1865
+ const chance = !foe.active.types.includes(move.type) and if (showdown)
1866
+ battle.rng.chance(u8, 26, 256)
1867
+ else
1868
+ battle.rng.next() < 1 + Gen12.percent(10);
1869
+ if (!chance) return;
1870
+
1913
1871
  // Freeze Clause Mod
1914
1872
  if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1915
1873
 
@@ -2046,15 +2004,15 @@ pub const Effects = struct {
2046
2004
  }
2047
2005
  break :has_mimic false;
2048
2006
  };
2049
- if (!has_mimic) {
2050
- // Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
2051
- if (!try checkHit(battle, player, move, log)) return;
2007
+ // If the foe is Invulnerable we still want to fall through to checkHit to be able to
2008
+ // trigger |-miss| instead of |-fail|
2009
+ if (!has_mimic and !foe.active.volatiles.Invulnerable) {
2052
2010
  return try log.fail(battle.active(player.foe()), .None);
2053
2011
  }
2054
2012
  }
2055
2013
  if (!try checkHit(battle, player, move, log)) return;
2056
2014
 
2057
- const rslot = Rolls.moveSlot(battle, &foe.active.moves, 0);
2015
+ const rslot = randomMoveSlot(&battle.rng, &foe.active.moves, 0);
2058
2016
  side.active.move(oslot).id = foe.active.move(rslot).id;
2059
2017
 
2060
2018
  try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
@@ -2075,8 +2033,7 @@ pub const Effects = struct {
2075
2033
  assert(!side.active.volatiles.MultiHit);
2076
2034
  side.active.volatiles.MultiHit = true;
2077
2035
 
2078
- side.active.volatiles.attacks =
2079
- if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
2036
+ side.active.volatiles.attacks = if (move.effect == .MultiHit) distribution(battle) else 2;
2080
2037
  }
2081
2038
 
2082
2039
  fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2120,8 +2077,14 @@ pub const Effects = struct {
2120
2077
  }
2121
2078
 
2122
2079
  // Body Slam can't paralyze a Normal type Pokémon
2123
- if (foe.active.types.includes(move.type)) return;
2124
- if (!Rolls.secondaryChance(battle, move.effect == .ParalyzeChance1)) return;
2080
+ const chance = !foe.active.types.includes(move.type) and if (showdown)
2081
+ battle.rng.chance(u8, @as(u8, if (move.effect == .ParalyzeChance1) 26 else 77), 256)
2082
+ else
2083
+ battle.rng.next() < 1 + (if (move.effect == .ParalyzeChance1)
2084
+ Gen12.percent(10)
2085
+ else
2086
+ Gen12.percent(30));
2087
+ if (!chance) return;
2125
2088
 
2126
2089
  foe_stored.status = Status.init(.PAR);
2127
2090
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
@@ -2164,7 +2127,16 @@ pub const Effects = struct {
2164
2127
 
2165
2128
  if (move.effect == .Poison) {
2166
2129
  if (!showdown and !try checkHit(battle, player, move, log)) return;
2167
- } else if (!Rolls.poisonChance(battle, move.effect == .PoisonChance1)) return;
2130
+ } else {
2131
+ const chance = if (showdown)
2132
+ battle.rng.chance(u8, @as(u8, if (move.effect == .PoisonChance1) 52 else 103), 256)
2133
+ else
2134
+ battle.rng.next() < 1 + (if (move.effect == .PoisonChance1)
2135
+ Gen12.percent(20)
2136
+ else
2137
+ Gen12.percent(40));
2138
+ if (!chance) return;
2139
+ }
2168
2140
 
2169
2141
  foe_stored.status = Status.init(.PSN);
2170
2142
  if (toxic) {
@@ -2178,8 +2150,10 @@ pub const Effects = struct {
2178
2150
 
2179
2151
  fn rage(battle: anytype, player: Player) !void {
2180
2152
  var volatiles = &battle.side(player).active.volatiles;
2181
- assert(!volatiles.Bide);
2153
+ if (volatiles.Rage) return;
2182
2154
  volatiles.Rage = true;
2155
+ assert(!volatiles.Bide);
2156
+ volatiles.state = 0;
2183
2157
  }
2184
2158
 
2185
2159
  fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
@@ -2231,7 +2205,16 @@ pub const Effects = struct {
2231
2205
  }
2232
2206
  foe.active.volatiles.Recharging = false;
2233
2207
 
2234
- foe_stored.status = Status.slp(Rolls.sleepDuration(battle));
2208
+ const duration = @intCast(u3, if (showdown)
2209
+ battle.rng.range(u8, 1, 8)
2210
+ else loop: {
2211
+ while (true) {
2212
+ const r = battle.rng.next() & 7;
2213
+ if (r != 0) break :loop r;
2214
+ }
2215
+ });
2216
+
2217
+ foe_stored.status = Status.slp(duration);
2235
2218
  try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
2236
2219
  }
2237
2220
 
@@ -2284,7 +2267,11 @@ pub const Effects = struct {
2284
2267
  assert(!volatiles.Bide);
2285
2268
 
2286
2269
  volatiles.Thrashing = true;
2287
- volatiles.attacks = Rolls.bideThrashDuration(battle);
2270
+ volatiles.state = 0;
2271
+ volatiles.attacks = @intCast(u3, if (showdown)
2272
+ battle.rng.range(u8, 2, 4)
2273
+ else
2274
+ (battle.rng.next() & 1) + 2);
2288
2275
  }
2289
2276
 
2290
2277
  fn transform(battle: anytype, player: Player, log: anytype) !void {
@@ -2316,7 +2303,7 @@ pub const Effects = struct {
2316
2303
  try log.transform(battle.active(player), foe_ident);
2317
2304
  }
2318
2305
 
2319
- fn binding(battle: anytype, player: Player, rewrap: bool) void {
2306
+ fn binding(battle: anytype, player: Player) void {
2320
2307
  var side = battle.side(player);
2321
2308
  var foe = battle.foe(player);
2322
2309
 
@@ -2324,11 +2311,9 @@ pub const Effects = struct {
2324
2311
  side.active.volatiles.Binding = true;
2325
2312
  // GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
2326
2313
  // (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
2327
- if (!showdown) {
2328
- foe.active.volatiles.Recharging = false;
2329
- } else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
2314
+ if (!showdown) foe.active.volatiles.Recharging = false;
2330
2315
 
2331
- side.active.volatiles.attacks = Rolls.distribution(battle) - 1;
2316
+ side.active.volatiles.attacks = distribution(battle) - 1;
2332
2317
  }
2333
2318
 
2334
2319
  fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2355,11 +2340,9 @@ pub const Effects = struct {
2355
2340
  return try log.fail(ident, .None);
2356
2341
  }
2357
2342
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2358
- const stat = unmodifiedStats(battle, side).atk;
2343
+ const stat = unmodifiedStats(battle, player).atk;
2359
2344
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2360
2345
  try log.boost(ident, reason, n);
2361
- // Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
2362
- if (showdown and move.effect == .Rage) return;
2363
2346
  },
2364
2347
  .DefenseUp1, .DefenseUp2 => {
2365
2348
  assert(boosts.def >= -6 and boosts.def <= 6);
@@ -2375,7 +2358,7 @@ pub const Effects = struct {
2375
2358
  return try log.fail(ident, .None);
2376
2359
  }
2377
2360
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2378
- const stat = unmodifiedStats(battle, side).def;
2361
+ const stat = unmodifiedStats(battle, player).def;
2379
2362
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2380
2363
  try log.boost(ident, .Defense, n);
2381
2364
  },
@@ -2392,7 +2375,7 @@ pub const Effects = struct {
2392
2375
  return try log.fail(ident, .None);
2393
2376
  }
2394
2377
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2395
- const stat = unmodifiedStats(battle, side).spe;
2378
+ const stat = unmodifiedStats(battle, player).spe;
2396
2379
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2397
2380
  try log.boost(ident, .Speed, 2);
2398
2381
  },
@@ -2412,7 +2395,7 @@ pub const Effects = struct {
2412
2395
  return try log.fail(ident, .None);
2413
2396
  }
2414
2397
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2415
- const stat = unmodifiedStats(battle, side).spc;
2398
+ const stat = unmodifiedStats(battle, player).spc;
2416
2399
  stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2417
2400
  try log.boost(ident, .SpecialAttack, n);
2418
2401
  try log.boost(ident, .SpecialDefense, n);
@@ -2439,7 +2422,11 @@ pub const Effects = struct {
2439
2422
  }
2440
2423
 
2441
2424
  if (move.effect.isStatDownChance()) {
2442
- if (!Rolls.unboost(battle) or foe.active.volatiles.Invulnerable) return;
2425
+ const chance = if (showdown)
2426
+ battle.rng.chance(u8, 85, 256)
2427
+ else
2428
+ battle.rng.next() < Gen12.percent(33) + 1;
2429
+ if (!chance or foe.active.volatiles.Invulnerable) return;
2443
2430
  } else if (!showdown and !try checkHit(battle, player, move, log)) {
2444
2431
  return; // checkHit already checks for Invulnerable
2445
2432
  }
@@ -2461,7 +2448,7 @@ pub const Effects = struct {
2461
2448
  return try log.fail(foe_ident, .None);
2462
2449
  }
2463
2450
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2464
- const stat = unmodifiedStats(battle, foe).atk;
2451
+ const stat = unmodifiedStats(battle, player.foe()).atk;
2465
2452
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2466
2453
  try log.boost(foe_ident, .Attack, -1);
2467
2454
  },
@@ -2479,7 +2466,7 @@ pub const Effects = struct {
2479
2466
  return try log.fail(foe_ident, .None);
2480
2467
  }
2481
2468
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2482
- const stat = unmodifiedStats(battle, foe).def;
2469
+ const stat = unmodifiedStats(battle, player.foe()).def;
2483
2470
  stats.def = @max(1, stat * mod[0] / mod[1]);
2484
2471
  try log.boost(foe_ident, .Defense, -@as(i8, n));
2485
2472
  },
@@ -2496,7 +2483,7 @@ pub const Effects = struct {
2496
2483
  return try log.fail(foe_ident, .None);
2497
2484
  }
2498
2485
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2499
- const stat = unmodifiedStats(battle, foe).spe;
2486
+ const stat = unmodifiedStats(battle, player.foe()).spe;
2500
2487
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2501
2488
  try log.boost(foe_ident, .Speed, -1);
2502
2489
  assert(boosts.spe >= -6);
@@ -2516,7 +2503,7 @@ pub const Effects = struct {
2516
2503
  return try log.fail(foe_ident, .None);
2517
2504
  }
2518
2505
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2519
- const stat = unmodifiedStats(battle, foe).spc;
2506
+ const stat = unmodifiedStats(battle, player.foe()).spc;
2520
2507
  stats.spc = @max(1, stat * mod[0] / mod[1]);
2521
2508
  try log.boost(foe_ident, .SpecialAttack, -1);
2522
2509
  try log.boost(foe_ident, .SpecialDefense, -1);
@@ -2535,7 +2522,8 @@ pub const Effects = struct {
2535
2522
  }
2536
2523
  };
2537
2524
 
2538
- fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
2525
+ fn unmodifiedStats(battle: anytype, who: Player) *Stats(u16) {
2526
+ const side = battle.side(who);
2539
2527
  if (!side.active.volatiles.Transform) return &side.stored().stats;
2540
2528
  const id = ID.from(side.active.volatiles.transform);
2541
2529
  return &battle.side(id.player).pokemon[id.id - 1].stats;
@@ -2601,140 +2589,75 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
2601
2589
  }
2602
2590
  }
2603
2591
 
2604
- pub const Rolls = struct {
2605
- inline fn speedTie(battle: anytype) bool {
2606
- if (showdown) return battle.rng.range(u8, 0, 2) == 0;
2607
- return battle.rng.next() < Gen12.percent(50) + 1;
2608
- }
2609
-
2610
- inline fn criticalHit(battle: anytype, chance: u16) bool {
2611
- if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
2612
- return std.math.rotl(u8, battle.rng.next(), 3) < chance;
2613
- }
2614
-
2615
- inline fn damage(battle: anytype) u8 {
2616
- if (showdown) return battle.rng.range(u8, 217, 256);
2617
- while (true) {
2618
- const r = std.math.rotr(u8, battle.rng.next(), 1);
2619
- if (r >= 217) return r;
2592
+ fn disabled(side: *Side, ident: ID, log: anytype) !bool {
2593
+ if (side.active.volatiles.disabled_move != 0) {
2594
+ // A Pokémon that transforms after being disabled may end up with less move slots
2595
+ const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
2596
+ if (m != .None and m == side.last_selected_move) {
2597
+ side.active.volatiles.Charging = false;
2598
+ try log.disabled(ident, side.last_selected_move);
2599
+ return true;
2620
2600
  }
2621
2601
  }
2602
+ return false;
2603
+ }
2622
2604
 
2623
- inline fn hit(battle: anytype, accuracy: u16) bool {
2624
- if (showdown) return battle.rng.chance(u8, @intCast(u8, accuracy), 256);
2625
- return battle.rng.next() < accuracy;
2626
- }
2627
-
2628
- inline fn confusionDuration(battle: anytype) u3 {
2629
- if (showdown) return @intCast(u3, battle.rng.range(u8, 2, 6));
2630
- return @intCast(u3, (battle.rng.next() & 3) + 2);
2631
- }
2632
-
2633
- inline fn confused(battle: anytype) bool {
2634
- if (showdown) return !battle.rng.chance(u8, 128, 256);
2635
- return battle.rng.next() >= Gen12.percent(50) + 1;
2636
- }
2637
-
2638
- inline fn paralyzed(battle: anytype) bool {
2639
- if (showdown) return battle.rng.chance(u8, 63, 256);
2640
- return battle.rng.next() < Gen12.percent(25);
2605
+ inline fn buildRage(battle: anytype, who: Player, log: anytype) !void {
2606
+ const side = battle.side(who);
2607
+ if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
2608
+ try Effects.boost(battle, who, Move.get(.Rage), log);
2641
2609
  }
2610
+ }
2642
2611
 
2643
- inline fn confusionChance(battle: anytype) bool {
2644
- if (showdown) return battle.rng.chance(u8, 25, 256);
2645
- return battle.rng.next() < Gen12.percent(10);
2646
- }
2612
+ fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
2613
+ var volatiles = &active.volatiles;
2614
+ assert(volatiles.Thrashing);
2615
+ if (volatiles.attacks > 0) return false;
2647
2616
 
2648
- inline fn secondaryChance(battle: anytype, low: bool) bool {
2649
- if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 26 else 77), 256);
2650
- return battle.rng.next() < 1 + (if (low) Gen12.percent(10) else Gen12.percent(30));
2651
- }
2617
+ volatiles.Thrashing = false;
2618
+ volatiles.Confusion = true;
2619
+ volatiles.confusion = @intCast(u3, if (showdown)
2620
+ battle.rng.range(u8, 2, 6)
2621
+ else
2622
+ (battle.rng.next() & 3) + 2);
2652
2623
 
2653
- inline fn poisonChance(battle: anytype, low: bool) bool {
2654
- if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 52 else 103), 256);
2655
- return battle.rng.next() < 1 + (if (low) Gen12.percent(20) else Gen12.percent(40));
2656
- }
2624
+ return true;
2625
+ }
2657
2626
 
2658
- inline fn sleepDuration(battle: anytype) u3 {
2659
- if (showdown) return @intCast(u3, battle.rng.range(u8, 1, 8));
2660
- while (true) {
2661
- const r = battle.rng.next() & 7;
2662
- if (r != 0) return @intCast(u3, r);
2663
- }
2664
- }
2627
+ const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
2665
2628
 
2666
- inline fn bideThrashDuration(battle: anytype) u3 {
2667
- if (showdown) return @intCast(u3, battle.rng.range(u4, 2, 4));
2668
- return @intCast(u3, (battle.rng.next() & 1) + 2);
2669
- }
2629
+ fn distribution(battle: anytype) u3 {
2630
+ if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
2631
+ const r = (battle.rng.next() & 3);
2632
+ return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
2633
+ }
2670
2634
 
2671
- fn moveSlot(battle: anytype, moves: []MoveSlot, check_pp: u4) u4 {
2672
- if (showdown) {
2673
- if (check_pp == 0) {
2674
- var i: usize = moves.len;
2675
- while (i > 0) {
2676
- i -= 1;
2677
- if (moves[i].id != .None) {
2678
- return battle.rng.range(u4, 0, @intCast(u4, i + 1)) + 1;
2679
- }
2680
- }
2681
- } else {
2682
- var r = battle.rng.range(u4, 0, @intCast(u4, check_pp)) + 1;
2683
- var i: usize = 0;
2684
- while (i < moves.len and r > 0) : (i += 1) {
2685
- if (moves[i].pp > 0) {
2686
- r -= 1;
2687
- if (r == 0) break;
2688
- }
2635
+ fn randomMoveSlot(rand: anytype, moves: []MoveSlot, check_pp: u4) u4 {
2636
+ if (showdown) {
2637
+ if (check_pp == 0) {
2638
+ var i: usize = moves.len;
2639
+ while (i > 0) {
2640
+ i -= 1;
2641
+ if (moves[i].id != .None) return rand.range(u4, 0, @intCast(u4, i + 1)) + 1;
2642
+ }
2643
+ } else {
2644
+ var r = rand.range(u4, 0, @intCast(u4, check_pp)) + 1;
2645
+ var i: usize = 0;
2646
+ while (i < moves.len and r > 0) : (i += 1) {
2647
+ if (moves[i].pp > 0) {
2648
+ r -= 1;
2649
+ if (r == 0) break;
2689
2650
  }
2690
- return @intCast(u4, i + 1);
2691
2651
  }
2692
- }
2693
-
2694
- while (true) {
2695
- const r = @intCast(u4, battle.rng.next() & 3);
2696
- if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
2652
+ return @intCast(u4, i + 1);
2697
2653
  }
2698
2654
  }
2699
2655
 
2700
- inline fn disableDuration(battle: anytype) u4 {
2701
- if (showdown) return @intCast(u4, battle.rng.range(u8, 1, 9));
2702
- return @intCast(u4, (battle.rng.next() & 7) + 1);
2656
+ while (true) {
2657
+ const r = @intCast(u4, rand.next() & 3);
2658
+ if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
2703
2659
  }
2704
-
2705
- inline fn metronome(battle: anytype) Move {
2706
- if (showdown) {
2707
- const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
2708
- const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
2709
- return @intToEnum(Move, r + mod);
2710
- }
2711
- while (true) {
2712
- const r = battle.rng.next();
2713
- if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
2714
- if (r >= @enumToInt(Move.Struggle)) continue;
2715
- return @intToEnum(Move, r);
2716
- }
2717
- }
2718
-
2719
- inline fn psywave(battle: anytype, max: u8) u8 {
2720
- if (showdown) return battle.rng.range(u8, 0, max);
2721
- while (true) {
2722
- const r = battle.rng.next();
2723
- if (r < max) return r;
2724
- }
2725
- }
2726
-
2727
- inline fn distribution(battle: anytype) u3 {
2728
- if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
2729
- const r = (battle.rng.next() & 3);
2730
- return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
2731
- }
2732
-
2733
- inline fn unboost(battle: anytype) bool {
2734
- if (showdown) return battle.rng.chance(u8, 85, 256);
2735
- return battle.rng.next() < Gen12.percent(33) + 1;
2736
- }
2737
- };
2660
+ }
2738
2661
 
2739
2662
  test "RNG agreement" {
2740
2663
  if (!showdown) return;
@@ -2834,7 +2757,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
2834
2757
  while (slot <= 4) : (slot += 1) {
2835
2758
  const m = active.moves[slot - 1];
2836
2759
  if (m.id == .None) break;
2837
- if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
2760
+ if (m.id == side.last_selected_move) {
2838
2761
  // Pokémon Showdown displays Struggle if limited to Bide but unable to pick
2839
2762
  const struggle =
2840
2763
  m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);