@pkmn/engine 0.1.0-dev.96b6c8fa → 0.1.0-dev.976cf3c3

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,
@@ -852,15 +788,12 @@ fn doMove(
852
788
  (@enumToInt(move.type.effectiveness(foe.active.types.type1)) == 0 or
853
789
  @enumToInt(move.type.effectiveness(foe.active.types.type2)) == 0);
854
790
  if (immune and move.effect != .Binding) break :miss true;
855
- if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) {
856
- battle.last_damage = 0;
857
- break :miss true;
858
- }
791
+ if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) break :miss true;
859
792
  break :miss move.target != .Self and !moveHit(battle, player, move, &immune, &mist);
860
793
  };
861
794
  assert(!immune or miss or (showdown and move.effect == .Binding));
862
795
 
863
- var late = showdown and move.effect != .Explode;
796
+ var late = showdown;
864
797
  const skip = status or immune;
865
798
  if ((!showdown or (!skip or counter)) and !miss) blk: {
866
799
  if (showdown and move.effect.isMulti()) {
@@ -913,9 +846,9 @@ fn doMove(
913
846
 
914
847
  if (!showdown or !miss) {
915
848
  if (move.effect == .MirrorMove) {
916
- return mirrorMove(battle, player, mslot, rewrap, auto, residual, log);
849
+ return mirrorMove(battle, player, mslot, auto, residual, log);
917
850
  } else if (move.effect == .Metronome) {
918
- return metronome(battle, player, mslot, rewrap, auto, residual, log);
851
+ return metronome(battle, player, mslot, auto, residual, log);
919
852
  } else if (move.effect.onEnd()) {
920
853
  try onEnd(battle, player, move, log);
921
854
  return null;
@@ -930,7 +863,7 @@ fn doMove(
930
863
  move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
931
864
  if (ohko) {
932
865
  try log.immune(foe_ident, .OHKO);
933
- } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
866
+ } else if (immune and !invulnerable) {
934
867
  try log.immune(foe_ident, .None);
935
868
  } else if (mist) {
936
869
  if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
@@ -980,7 +913,7 @@ fn doMove(
980
913
  if (hit == 0 and ohko) try log.ohko();
981
914
  hit += 1;
982
915
  if (foe.stored().hp == 0) break;
983
- if (!late and (!sub or move.effect == .Explode)) try buildRage(battle, player.foe(), log);
916
+ if (!late and !sub) try buildRage(battle, player.foe(), log);
984
917
  // If the substitute breaks during a multi-hit attack, the attack ends
985
918
  if (nullified) break;
986
919
  }
@@ -996,17 +929,11 @@ fn doMove(
996
929
  } else if (showdown) {
997
930
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
998
931
  if (move.effect == .Binding) {
999
- Effects.binding(battle, player, rewrap);
932
+ Effects.binding(battle, player);
1000
933
  if (immune) {
1001
934
  battle.last_damage = 0;
1002
- assert(foe.stored().hp > 0);
1003
935
  // 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
- }
936
+ try log.damage(battle.active(player.foe()), foe.stored(), .None);
1010
937
  return null;
1011
938
  }
1012
939
  }
@@ -1057,7 +984,8 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
1057
984
  chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
1058
985
  chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
1059
986
 
1060
- return Rolls.criticalHit(battle, chance);
987
+ if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
988
+ return std.math.rotl(u8, battle.rng.next(), 3) < chance;
1061
989
  }
1062
990
 
1063
991
  fn calcDamage(
@@ -1161,7 +1089,16 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
1161
1089
 
1162
1090
  fn randomizeDamage(battle: anytype) void {
1163
1091
  if (battle.last_damage <= 1) return;
1164
- const random = Rolls.damage(battle);
1092
+
1093
+ const random = if (showdown)
1094
+ battle.rng.range(u8, 217, 256)
1095
+ else loop: {
1096
+ while (true) {
1097
+ const r = std.math.rotr(u8, battle.rng.next(), 1);
1098
+ if (r >= 217) break :loop r;
1099
+ }
1100
+ };
1101
+
1165
1102
  battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
1166
1103
  }
1167
1104
 
@@ -1179,9 +1116,16 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
1179
1116
  // GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
1180
1117
  .Psywave => power: {
1181
1118
  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);
1119
+ if (showdown) {
1120
+ break :power battle.rng.range(u8, 0, max);
1121
+ } else {
1122
+ // GLITCH: Psywave infinite glitch loop
1123
+ if (max <= 1) return Result.Error;
1124
+ while (true) {
1125
+ const r = battle.rng.next();
1126
+ if (r < max) break :power r;
1127
+ }
1128
+ }
1185
1129
  },
1186
1130
  else => unreachable,
1187
1131
  };
@@ -1203,17 +1147,19 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
1203
1147
  return null;
1204
1148
  }
1205
1149
 
1206
- // Pretend Splash was used as a stand-in when no move has been used to fail below with 0 BP
1150
+ // Pretend Counter was used as a stand-in when no move has been used to fail below with 0 BP
1151
+ const foe_last_used_move =
1152
+ Move.get(if (foe.last_used_move == .None) .Counter else foe.last_used_move);
1207
1153
  const foe_last_selected_move =
1208
1154
  Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
1209
- .Splash
1155
+ .Counter
1210
1156
  else
1211
1157
  foe.last_selected_move);
1212
1158
 
1213
- const used = (if (player.foe() == .P1)
1214
- battle.last_moves.p1_counterable
1215
- else
1216
- battle.last_moves.p2_counterable) != 0;
1159
+ const used = foe_last_used_move.bp > 0 and
1160
+ foe.last_used_move != .Counter and
1161
+ (foe_last_used_move.type == .Normal or
1162
+ foe_last_used_move.type == .Fighting);
1217
1163
 
1218
1164
  const selected = foe_last_selected_move.bp > 0 and
1219
1165
  foe.last_selected_move != .Counter and
@@ -1232,14 +1178,12 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
1232
1178
  return null;
1233
1179
  }
1234
1180
 
1235
- battle.last_damage *|= 2;
1181
+ battle.last_damage = if (battle.last_damage > 0x7FFF) 0xFFFF else battle.last_damage * 2;
1236
1182
 
1237
1183
  // Pokémon Showdown calls checkHit before Counter
1238
1184
  if (!showdown and !try checkHit(battle, player, move, log)) return null;
1239
1185
 
1240
- const sub = showdown and foe.active.volatiles.Substitute;
1241
1186
  _ = 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
1187
  return null;
1244
1188
  }
1245
1189
 
@@ -1283,7 +1227,6 @@ fn mirrorMove(
1283
1227
  battle: anytype,
1284
1228
  player: Player,
1285
1229
  mslot: u4,
1286
- rewrap: bool,
1287
1230
  auto: bool,
1288
1231
  residual: *bool,
1289
1232
  log: anytype,
@@ -1296,30 +1239,41 @@ fn mirrorMove(
1296
1239
  if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
1297
1240
  try log.fail(battle.active(player), .None);
1298
1241
  return null;
1299
- } else if (!showdown or foe.last_used_move != .Struggle) {
1300
- incrementPP(side, mslot);
1301
1242
  }
1302
1243
 
1244
+ incrementPP(side, mslot);
1245
+
1303
1246
  if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
1304
- return doMove(battle, player, mslot, rewrap, auto, residual, log);
1247
+ return doMove(battle, player, mslot, auto, residual, log);
1305
1248
  }
1306
1249
 
1307
1250
  fn metronome(
1308
1251
  battle: anytype,
1309
1252
  player: Player,
1310
1253
  mslot: u4,
1311
- rewrap: bool,
1312
1254
  auto: bool,
1313
1255
  residual: *bool,
1314
1256
  log: anytype,
1315
1257
  ) !?Result {
1316
1258
  var side = battle.side(player);
1317
1259
 
1318
- side.last_selected_move = Rolls.metronome(battle);
1260
+ side.last_selected_move = if (showdown) blk: {
1261
+ const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
1262
+ const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
1263
+ break :blk @intToEnum(Move, r + mod);
1264
+ } else loop: {
1265
+ while (true) {
1266
+ const r = battle.rng.next();
1267
+ if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
1268
+ if (r >= @enumToInt(Move.Struggle)) continue;
1269
+ break :loop @intToEnum(Move, r);
1270
+ }
1271
+ };
1272
+
1319
1273
  incrementPP(side, mslot);
1320
1274
 
1321
1275
  if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
1322
- return doMove(battle, player, mslot, rewrap, auto, residual, log);
1276
+ return doMove(battle, player, mslot, auto, residual, log);
1323
1277
  }
1324
1278
 
1325
1279
  fn checkHit(battle: anytype, player: Player, move: Move.Data, log: anytype) !bool {
@@ -1373,24 +1327,23 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
1373
1327
  }
1374
1328
 
1375
1329
  // 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));
1330
+ var state = &side.active.volatiles.state;
1331
+ const overwritten = (move.effect == .Thrashing or move.effect == .Rage) and state.* > 0;
1332
+ assert(!overwritten or (0 < state.* and state.* <= 255 and !side.active.volatiles.Bide));
1333
+ var accuracy = if (overwritten) state.* else @as(u16, Gen12.percent(move.accuracy));
1382
1334
  var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
1383
1335
  accuracy = accuracy * boost[0] / boost[1];
1384
1336
  boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
1385
1337
  accuracy = accuracy * boost[0] / boost[1];
1386
1338
  accuracy = @min(255, @max(1, accuracy));
1387
1339
 
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;
1340
+ if (move.effect == .Thrashing or move.effect == .Rage) state.* = accuracy;
1391
1341
 
1392
1342
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1393
- break :miss !Rolls.hit(battle, accuracy);
1343
+ break :miss if (showdown)
1344
+ !battle.rng.chance(u8, @intCast(u8, accuracy), 256)
1345
+ else
1346
+ battle.rng.next() >= accuracy;
1394
1347
  };
1395
1348
 
1396
1349
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1522,6 +1475,9 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1522
1475
  damage *= volatiles.toxic;
1523
1476
  }
1524
1477
 
1478
+ // Pokémon Showdown erroneously updates last damage with uncapped Leech Seed damage
1479
+ if (showdown) battle.last_damage = damage;
1480
+
1525
1481
  const amount = @min(damage, stored.hp);
1526
1482
  stored.hp -= amount;
1527
1483
 
@@ -1540,13 +1496,13 @@ fn endTurn(battle: anytype, log: anytype) @TypeOf(log).Error!Result {
1540
1496
  assert(!battle.side(.P1).active.volatiles.MultiHit);
1541
1497
  assert(!battle.side(.P2).active.volatiles.MultiHit);
1542
1498
 
1543
- battle.turn += 1;
1544
-
1545
1499
  if (showdown and options.ebc and checkEBC(battle)) {
1546
1500
  try log.tie();
1547
1501
  return Result.Tie;
1548
1502
  }
1549
1503
 
1504
+ battle.turn += 1;
1505
+
1550
1506
  if (showdown and battle.turn >= 1000) {
1551
1507
  try log.tie();
1552
1508
  return Result.Tie;
@@ -1613,42 +1569,6 @@ fn checkEBC(battle: anytype) bool {
1613
1569
  return false;
1614
1570
  }
1615
1571
 
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
1572
  inline fn onBegin(
1653
1573
  battle: anytype,
1654
1574
  player: Player,
@@ -1739,7 +1659,10 @@ pub const Effects = struct {
1739
1659
  side.active.volatiles.Bide = true;
1740
1660
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1741
1661
  side.active.volatiles.state = 0;
1742
- side.active.volatiles.attacks = Rolls.bideThrashDuration(battle);
1662
+ side.active.volatiles.attacks = @intCast(u3, if (showdown)
1663
+ battle.rng.range(u4, 2, 4)
1664
+ else
1665
+ (battle.rng.next() & 1) + 2);
1743
1666
 
1744
1667
  try log.start(battle.active(player), .Bide);
1745
1668
  }
@@ -1761,8 +1684,14 @@ pub const Effects = struct {
1761
1684
  return;
1762
1685
  }
1763
1686
 
1764
- if (foe.active.types.includes(move.type)) return;
1765
- if (!Rolls.secondaryChance(battle, move.effect == .BurnChance1)) return;
1687
+ const chance = !foe.active.types.includes(move.type) and if (showdown)
1688
+ battle.rng.chance(u8, @as(u8, if (move.effect == .BurnChance1) 26 else 77), 256)
1689
+ else
1690
+ battle.rng.next() < 1 + (if (move.effect == .BurnChance1)
1691
+ Gen12.percent(10)
1692
+ else
1693
+ Gen12.percent(30));
1694
+ if (!chance) return;
1766
1695
 
1767
1696
  foe_stored.status = Status.init(.BRN);
1768
1697
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
@@ -1786,7 +1715,11 @@ pub const Effects = struct {
1786
1715
  const sub = foe.active.volatiles.Substitute;
1787
1716
 
1788
1717
  if (move.effect == .ConfusionChance) {
1789
- if (!Rolls.confusionChance(battle)) return;
1718
+ const chance = if (showdown)
1719
+ battle.rng.chance(u8, 25, 256)
1720
+ else
1721
+ battle.rng.next() < Gen12.percent(10);
1722
+ if (!chance) return;
1790
1723
  } else {
1791
1724
  if (showdown) {
1792
1725
  if (!try checkHit(battle, player, move, log)) {
@@ -1805,7 +1738,10 @@ pub const Effects = struct {
1805
1738
 
1806
1739
  if (foe.active.volatiles.Confusion) return;
1807
1740
  foe.active.volatiles.Confusion = true;
1808
- foe.active.volatiles.confusion = Rolls.confusionDuration(battle);
1741
+ foe.active.volatiles.confusion = @intCast(u3, if (showdown)
1742
+ battle.rng.range(u8, 2, 6)
1743
+ else
1744
+ (battle.rng.next() & 3) + 2);
1809
1745
 
1810
1746
  try log.start(battle.active(player.foe()), .Confusion);
1811
1747
  }
@@ -1855,8 +1791,11 @@ pub const Effects = struct {
1855
1791
  return Result.Error;
1856
1792
  }
1857
1793
 
1858
- volatiles.disabled_move = @intCast(u3, Rolls.moveSlot(battle, &foe.active.moves, n));
1859
- volatiles.disabled_duration = Rolls.disableDuration(battle);
1794
+ volatiles.disabled_move = @intCast(u3, randomMoveSlot(&battle.rng, &foe.active.moves, n));
1795
+ volatiles.disabled_duration = @intCast(u4, if (showdown)
1796
+ battle.rng.range(u8, 1, 9)
1797
+ else
1798
+ (battle.rng.next() & 7) + 1);
1860
1799
 
1861
1800
  try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
1862
1801
  return null;
@@ -1868,7 +1807,7 @@ pub const Effects = struct {
1868
1807
 
1869
1808
  if (battle.last_damage == 0) {
1870
1809
  assert(showdown);
1871
- if (!battle.foe(player).active.volatiles.Substitute) return;
1810
+ return;
1872
1811
  }
1873
1812
 
1874
1813
  const drain = @max(battle.last_damage / 2, 1);
@@ -1895,7 +1834,15 @@ pub const Effects = struct {
1895
1834
  var volatiles = &battle.foe(player).active.volatiles;
1896
1835
 
1897
1836
  if (volatiles.Substitute) return;
1898
- if (!Rolls.secondaryChance(battle, move.effect == .FlinchChance1)) return;
1837
+
1838
+ const chance = if (showdown)
1839
+ battle.rng.chance(u8, @as(u8, if (move.effect == .FlinchChance1) 26 else 77), 256)
1840
+ else
1841
+ battle.rng.next() < 1 + (if (move.effect == .FlinchChance1)
1842
+ Gen12.percent(10)
1843
+ else
1844
+ Gen12.percent(30));
1845
+ if (!chance) return;
1899
1846
 
1900
1847
  volatiles.Flinch = true;
1901
1848
  volatiles.Recharging = false;
@@ -1921,8 +1868,12 @@ pub const Effects = struct {
1921
1868
  return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
1922
1869
  }
1923
1870
 
1924
- if (foe.active.types.includes(move.type)) return;
1925
- if (!Rolls.secondaryChance(battle, true)) return;
1871
+ const chance = !foe.active.types.includes(move.type) and if (showdown)
1872
+ battle.rng.chance(u8, 26, 256)
1873
+ else
1874
+ battle.rng.next() < 1 + Gen12.percent(10);
1875
+ if (!chance) return;
1876
+
1926
1877
  // Freeze Clause Mod
1927
1878
  if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1928
1879
 
@@ -2059,15 +2010,15 @@ pub const Effects = struct {
2059
2010
  }
2060
2011
  break :has_mimic false;
2061
2012
  };
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;
2013
+ // If the foe is Invulnerable we still want to fall through to checkHit to be able to
2014
+ // trigger |-miss| instead of |-fail|
2015
+ if (!has_mimic and !foe.active.volatiles.Invulnerable) {
2065
2016
  return try log.fail(battle.active(player.foe()), .None);
2066
2017
  }
2067
2018
  }
2068
2019
  if (!try checkHit(battle, player, move, log)) return;
2069
2020
 
2070
- const rslot = Rolls.moveSlot(battle, &foe.active.moves, 0);
2021
+ const rslot = randomMoveSlot(&battle.rng, &foe.active.moves, 0);
2071
2022
  side.active.move(oslot).id = foe.active.move(rslot).id;
2072
2023
 
2073
2024
  try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
@@ -2088,8 +2039,7 @@ pub const Effects = struct {
2088
2039
  assert(!side.active.volatiles.MultiHit);
2089
2040
  side.active.volatiles.MultiHit = true;
2090
2041
 
2091
- side.active.volatiles.attacks =
2092
- if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
2042
+ side.active.volatiles.attacks = if (move.effect == .MultiHit) distribution(battle) else 2;
2093
2043
  }
2094
2044
 
2095
2045
  fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2133,8 +2083,14 @@ pub const Effects = struct {
2133
2083
  }
2134
2084
 
2135
2085
  // 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;
2086
+ const chance = !foe.active.types.includes(move.type) and if (showdown)
2087
+ battle.rng.chance(u8, @as(u8, if (move.effect == .ParalyzeChance1) 26 else 77), 256)
2088
+ else
2089
+ battle.rng.next() < 1 + (if (move.effect == .ParalyzeChance1)
2090
+ Gen12.percent(10)
2091
+ else
2092
+ Gen12.percent(30));
2093
+ if (!chance) return;
2138
2094
 
2139
2095
  foe_stored.status = Status.init(.PAR);
2140
2096
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
@@ -2177,7 +2133,16 @@ pub const Effects = struct {
2177
2133
 
2178
2134
  if (move.effect == .Poison) {
2179
2135
  if (!showdown and !try checkHit(battle, player, move, log)) return;
2180
- } else if (!Rolls.poisonChance(battle, move.effect == .PoisonChance1)) return;
2136
+ } else {
2137
+ const chance = if (showdown)
2138
+ battle.rng.chance(u8, @as(u8, if (move.effect == .PoisonChance1) 52 else 103), 256)
2139
+ else
2140
+ battle.rng.next() < 1 + (if (move.effect == .PoisonChance1)
2141
+ Gen12.percent(20)
2142
+ else
2143
+ Gen12.percent(40));
2144
+ if (!chance) return;
2145
+ }
2181
2146
 
2182
2147
  foe_stored.status = Status.init(.PSN);
2183
2148
  if (toxic) {
@@ -2191,8 +2156,10 @@ pub const Effects = struct {
2191
2156
 
2192
2157
  fn rage(battle: anytype, player: Player) !void {
2193
2158
  var volatiles = &battle.side(player).active.volatiles;
2194
- assert(!volatiles.Bide);
2159
+ if (volatiles.Rage) return;
2195
2160
  volatiles.Rage = true;
2161
+ assert(!volatiles.Bide);
2162
+ volatiles.state = 0;
2196
2163
  }
2197
2164
 
2198
2165
  fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
@@ -2244,7 +2211,16 @@ pub const Effects = struct {
2244
2211
  }
2245
2212
  foe.active.volatiles.Recharging = false;
2246
2213
 
2247
- foe_stored.status = Status.slp(Rolls.sleepDuration(battle));
2214
+ const duration = @intCast(u3, if (showdown)
2215
+ battle.rng.range(u8, 1, 8)
2216
+ else loop: {
2217
+ while (true) {
2218
+ const r = battle.rng.next() & 7;
2219
+ if (r != 0) break :loop r;
2220
+ }
2221
+ });
2222
+
2223
+ foe_stored.status = Status.slp(duration);
2248
2224
  try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
2249
2225
  }
2250
2226
 
@@ -2297,7 +2273,11 @@ pub const Effects = struct {
2297
2273
  assert(!volatiles.Bide);
2298
2274
 
2299
2275
  volatiles.Thrashing = true;
2300
- volatiles.attacks = Rolls.bideThrashDuration(battle);
2276
+ volatiles.state = 0;
2277
+ volatiles.attacks = @intCast(u3, if (showdown)
2278
+ battle.rng.range(u8, 2, 4)
2279
+ else
2280
+ (battle.rng.next() & 1) + 2);
2301
2281
  }
2302
2282
 
2303
2283
  fn transform(battle: anytype, player: Player, log: anytype) !void {
@@ -2329,7 +2309,7 @@ pub const Effects = struct {
2329
2309
  try log.transform(battle.active(player), foe_ident);
2330
2310
  }
2331
2311
 
2332
- fn binding(battle: anytype, player: Player, rewrap: bool) void {
2312
+ fn binding(battle: anytype, player: Player) void {
2333
2313
  var side = battle.side(player);
2334
2314
  var foe = battle.foe(player);
2335
2315
 
@@ -2337,11 +2317,9 @@ pub const Effects = struct {
2337
2317
  side.active.volatiles.Binding = true;
2338
2318
  // GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
2339
2319
  // (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);
2320
+ if (!showdown) foe.active.volatiles.Recharging = false;
2343
2321
 
2344
- side.active.volatiles.attacks = Rolls.distribution(battle) - 1;
2322
+ side.active.volatiles.attacks = distribution(battle) - 1;
2345
2323
  }
2346
2324
 
2347
2325
  fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2368,7 +2346,7 @@ pub const Effects = struct {
2368
2346
  return try log.fail(ident, .None);
2369
2347
  }
2370
2348
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2371
- const stat = unmodifiedStats(battle, side).atk;
2349
+ const stat = unmodifiedStats(battle, player).atk;
2372
2350
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2373
2351
  try log.boost(ident, reason, n);
2374
2352
  // Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
@@ -2388,7 +2366,7 @@ pub const Effects = struct {
2388
2366
  return try log.fail(ident, .None);
2389
2367
  }
2390
2368
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2391
- const stat = unmodifiedStats(battle, side).def;
2369
+ const stat = unmodifiedStats(battle, player).def;
2392
2370
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2393
2371
  try log.boost(ident, .Defense, n);
2394
2372
  },
@@ -2405,7 +2383,7 @@ pub const Effects = struct {
2405
2383
  return try log.fail(ident, .None);
2406
2384
  }
2407
2385
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2408
- const stat = unmodifiedStats(battle, side).spe;
2386
+ const stat = unmodifiedStats(battle, player).spe;
2409
2387
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2410
2388
  try log.boost(ident, .Speed, 2);
2411
2389
  },
@@ -2425,7 +2403,7 @@ pub const Effects = struct {
2425
2403
  return try log.fail(ident, .None);
2426
2404
  }
2427
2405
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2428
- const stat = unmodifiedStats(battle, side).spc;
2406
+ const stat = unmodifiedStats(battle, player).spc;
2429
2407
  stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2430
2408
  try log.boost(ident, .SpecialAttack, n);
2431
2409
  try log.boost(ident, .SpecialDefense, n);
@@ -2452,7 +2430,11 @@ pub const Effects = struct {
2452
2430
  }
2453
2431
 
2454
2432
  if (move.effect.isStatDownChance()) {
2455
- if (!Rolls.unboost(battle) or foe.active.volatiles.Invulnerable) return;
2433
+ const chance = if (showdown)
2434
+ battle.rng.chance(u8, 85, 256)
2435
+ else
2436
+ battle.rng.next() < Gen12.percent(33) + 1;
2437
+ if (!chance or foe.active.volatiles.Invulnerable) return;
2456
2438
  } else if (!showdown and !try checkHit(battle, player, move, log)) {
2457
2439
  return; // checkHit already checks for Invulnerable
2458
2440
  }
@@ -2474,7 +2456,7 @@ pub const Effects = struct {
2474
2456
  return try log.fail(foe_ident, .None);
2475
2457
  }
2476
2458
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2477
- const stat = unmodifiedStats(battle, foe).atk;
2459
+ const stat = unmodifiedStats(battle, player.foe()).atk;
2478
2460
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2479
2461
  try log.boost(foe_ident, .Attack, -1);
2480
2462
  },
@@ -2492,7 +2474,7 @@ pub const Effects = struct {
2492
2474
  return try log.fail(foe_ident, .None);
2493
2475
  }
2494
2476
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2495
- const stat = unmodifiedStats(battle, foe).def;
2477
+ const stat = unmodifiedStats(battle, player.foe()).def;
2496
2478
  stats.def = @max(1, stat * mod[0] / mod[1]);
2497
2479
  try log.boost(foe_ident, .Defense, -@as(i8, n));
2498
2480
  },
@@ -2509,7 +2491,7 @@ pub const Effects = struct {
2509
2491
  return try log.fail(foe_ident, .None);
2510
2492
  }
2511
2493
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2512
- const stat = unmodifiedStats(battle, foe).spe;
2494
+ const stat = unmodifiedStats(battle, player.foe()).spe;
2513
2495
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2514
2496
  try log.boost(foe_ident, .Speed, -1);
2515
2497
  assert(boosts.spe >= -6);
@@ -2529,7 +2511,7 @@ pub const Effects = struct {
2529
2511
  return try log.fail(foe_ident, .None);
2530
2512
  }
2531
2513
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2532
- const stat = unmodifiedStats(battle, foe).spc;
2514
+ const stat = unmodifiedStats(battle, player.foe()).spc;
2533
2515
  stats.spc = @max(1, stat * mod[0] / mod[1]);
2534
2516
  try log.boost(foe_ident, .SpecialAttack, -1);
2535
2517
  try log.boost(foe_ident, .SpecialDefense, -1);
@@ -2548,7 +2530,8 @@ pub const Effects = struct {
2548
2530
  }
2549
2531
  };
2550
2532
 
2551
- fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
2533
+ fn unmodifiedStats(battle: anytype, who: Player) *Stats(u16) {
2534
+ const side = battle.side(who);
2552
2535
  if (!side.active.volatiles.Transform) return &side.stored().stats;
2553
2536
  const id = ID.from(side.active.volatiles.transform);
2554
2537
  return &battle.side(id.player).pokemon[id.id - 1].stats;
@@ -2614,140 +2597,80 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
2614
2597
  }
2615
2598
  }
2616
2599
 
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;
2600
+ fn disabled(side: *Side, ident: ID, log: anytype) !bool {
2601
+ if (side.active.volatiles.disabled_move != 0) {
2602
+ // A Pokémon that transforms after being disabled may end up with less move slots
2603
+ const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
2604
+ // side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
2605
+ const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
2606
+ m
2607
+ else
2608
+ side.last_selected_move;
2609
+ if (m != .None and m == last) {
2610
+ side.active.volatiles.Charging = false;
2611
+ try log.disabled(ident, last);
2612
+ return true;
2633
2613
  }
2634
2614
  }
2615
+ return false;
2616
+ }
2635
2617
 
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);
2618
+ inline fn buildRage(battle: anytype, who: Player, log: anytype) !void {
2619
+ const side = battle.side(who);
2620
+ if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
2621
+ try Effects.boost(battle, who, Move.get(.Rage), log);
2654
2622
  }
2623
+ }
2655
2624
 
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
- }
2625
+ fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
2626
+ var volatiles = &active.volatiles;
2627
+ assert(volatiles.Thrashing);
2628
+ if (volatiles.attacks > 0) return false;
2660
2629
 
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
- }
2630
+ volatiles.Thrashing = false;
2631
+ volatiles.Confusion = true;
2632
+ volatiles.confusion = @intCast(u3, if (showdown)
2633
+ battle.rng.range(u8, 2, 6)
2634
+ else
2635
+ (battle.rng.next() & 3) + 2);
2665
2636
 
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
- }
2637
+ return true;
2638
+ }
2670
2639
 
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
- }
2640
+ const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
2678
2641
 
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
- }
2642
+ fn distribution(battle: anytype) u3 {
2643
+ if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
2644
+ const r = (battle.rng.next() & 3);
2645
+ return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
2646
+ }
2683
2647
 
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
- }
2648
+ fn randomMoveSlot(rand: anytype, moves: []MoveSlot, check_pp: u4) u4 {
2649
+ if (showdown) {
2650
+ if (check_pp == 0) {
2651
+ var i: usize = moves.len;
2652
+ while (i > 0) {
2653
+ i -= 1;
2654
+ if (moves[i].id != .None) return rand.range(u4, 0, @intCast(u4, i + 1)) + 1;
2655
+ }
2656
+ } else {
2657
+ var r = rand.range(u4, 0, @intCast(u4, check_pp)) + 1;
2658
+ var i: usize = 0;
2659
+ while (i < moves.len and r > 0) : (i += 1) {
2660
+ if (moves[i].pp > 0) {
2661
+ r -= 1;
2662
+ if (r == 0) break;
2702
2663
  }
2703
- return @intCast(u4, i + 1);
2704
2664
  }
2705
- }
2706
-
2707
- while (true) {
2708
- const r = @intCast(u4, battle.rng.next() & 3);
2709
- if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
2710
- }
2711
- }
2712
-
2713
- inline fn disableDuration(battle: anytype) u4 {
2714
- if (showdown) return @intCast(u4, battle.rng.range(u8, 1, 9));
2715
- return @intCast(u4, (battle.rng.next() & 7) + 1);
2716
- }
2717
-
2718
- inline fn metronome(battle: anytype) Move {
2719
- if (showdown) {
2720
- const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
2721
- const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
2722
- return @intToEnum(Move, r + mod);
2723
- }
2724
- while (true) {
2725
- const r = battle.rng.next();
2726
- if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
2727
- if (r >= @enumToInt(Move.Struggle)) continue;
2728
- return @intToEnum(Move, r);
2729
- }
2730
- }
2731
-
2732
- inline fn psywave(battle: anytype, max: u8) u8 {
2733
- if (showdown) return battle.rng.range(u8, 0, max);
2734
- while (true) {
2735
- const r = battle.rng.next();
2736
- if (r < max) return r;
2665
+ return @intCast(u4, i + 1);
2737
2666
  }
2738
2667
  }
2739
2668
 
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);
2669
+ while (true) {
2670
+ const r = @intCast(u4, rand.next() & 3);
2671
+ if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
2744
2672
  }
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
- };
2673
+ }
2751
2674
 
2752
2675
  test "RNG agreement" {
2753
2676
  if (!showdown) return;