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

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
 
@@ -657,9 +620,8 @@ fn beforeMove(
657
620
  return .done;
658
621
  }
659
622
 
660
- const sub = showdown and foe.active.volatiles.Substitute;
661
623
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
662
- 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);
663
625
 
664
626
  // For reasons passing understanding, Pokémon Showdown still inflicts residual damage to
665
627
  // Bide's user even if the above damage has caused the foe to faint. It's simpler to always
@@ -680,13 +642,8 @@ fn beforeMove(
680
642
  try log.start(battle.active(player), .ConfusionSilent);
681
643
  }
682
644
  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
- }
645
+ if (showdown and handleThrashing(battle, active)) {
646
+ try log.start(battle.active(player), .ConfusionSilent);
690
647
  }
691
648
  return .skip_can;
692
649
  }
@@ -701,11 +658,7 @@ fn beforeMove(
701
658
  side.last_selected_move,
702
659
  );
703
660
  if (showdown or battle.last_damage != 0) {
704
- const sub = showdown and foe.active.volatiles.Substitute;
705
661
  _ = 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
662
  }
710
663
  return .done;
711
664
  }
@@ -737,7 +690,6 @@ fn canMove(
737
690
  side.active.volatiles.Invulnerable = false;
738
691
  } else if (move.effect == .Charge) {
739
692
  try log.move(player_ident, side.last_selected_move, .{}, from);
740
- setCounterable(battle, player, side, move);
741
693
  try Effects.charge(battle, player, log);
742
694
  return false;
743
695
  }
@@ -754,7 +706,6 @@ fn canMove(
754
706
  // regrettable, but this information is more "nice to have" than required
755
707
  const f = if (from != null and from.? == .None) null else from;
756
708
  try log.move(player_ident, side.last_selected_move, battle.active(target), f);
757
- setCounterable(battle, player, side, move);
758
709
 
759
710
  if (move.effect.onBegin()) {
760
711
  try onBegin(battle, player, move, mslot, residual, log);
@@ -765,27 +716,12 @@ fn canMove(
765
716
  Effects.thrashing(battle, player);
766
717
  } else if (!showdown and move.effect == .Binding) {
767
718
  // Pokémon Showdown handles this after hit/miss checks and damage calculation
768
- Effects.binding(battle, player, false);
719
+ Effects.binding(battle, player);
769
720
  }
770
721
 
771
722
  return true;
772
723
  }
773
724
 
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
725
  fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
790
726
  if (side.last_selected_move == .Struggle) return;
791
727
 
@@ -821,7 +757,6 @@ fn doMove(
821
757
  battle: anytype,
822
758
  player: Player,
823
759
  mslot: u4,
824
- rewrap: bool,
825
760
  auto: bool,
826
761
  residual: *bool,
827
762
  log: anytype,
@@ -852,15 +787,12 @@ fn doMove(
852
787
  (@enumToInt(move.type.effectiveness(foe.active.types.type1)) == 0 or
853
788
  @enumToInt(move.type.effectiveness(foe.active.types.type2)) == 0);
854
789
  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
- }
790
+ if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) break :miss true;
859
791
  break :miss move.target != .Self and !moveHit(battle, player, move, &immune, &mist);
860
792
  };
861
793
  assert(!immune or miss or (showdown and move.effect == .Binding));
862
794
 
863
- var late = showdown and move.effect != .Explode;
795
+ var late = showdown;
864
796
  const skip = status or immune;
865
797
  if ((!showdown or (!skip or counter)) and !miss) blk: {
866
798
  if (showdown and move.effect.isMulti()) {
@@ -913,9 +845,9 @@ fn doMove(
913
845
 
914
846
  if (!showdown or !miss) {
915
847
  if (move.effect == .MirrorMove) {
916
- return mirrorMove(battle, player, mslot, rewrap, auto, residual, log);
848
+ return mirrorMove(battle, player, mslot, auto, residual, log);
917
849
  } else if (move.effect == .Metronome) {
918
- return metronome(battle, player, mslot, rewrap, auto, residual, log);
850
+ return metronome(battle, player, mslot, auto, residual, log);
919
851
  } else if (move.effect.onEnd()) {
920
852
  try onEnd(battle, player, move, log);
921
853
  return null;
@@ -930,7 +862,7 @@ fn doMove(
930
862
  move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
931
863
  if (ohko) {
932
864
  try log.immune(foe_ident, .OHKO);
933
- } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
865
+ } else if (immune and !invulnerable) {
934
866
  try log.immune(foe_ident, .None);
935
867
  } else if (mist) {
936
868
  if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
@@ -962,9 +894,6 @@ fn doMove(
962
894
  hits = side.active.volatiles.attacks;
963
895
  }
964
896
 
965
- // Pokémon Showdown only builds Rage for Disable/Explosion (hit/miss) when attacking into a sub
966
- const sub = showdown and foe.active.volatiles.Substitute;
967
-
968
897
  var nullified = false;
969
898
  var hit: u4 = 0;
970
899
  while (hit < hits) {
@@ -980,7 +909,7 @@ fn doMove(
980
909
  if (hit == 0 and ohko) try log.ohko();
981
910
  hit += 1;
982
911
  if (foe.stored().hp == 0) break;
983
- if (!late and (!sub or move.effect == .Explode)) try buildRage(battle, player.foe(), log);
912
+ if (!late) try buildRage(battle, player.foe(), log);
984
913
  // If the substitute breaks during a multi-hit attack, the attack ends
985
914
  if (nullified) break;
986
915
  }
@@ -996,17 +925,11 @@ fn doMove(
996
925
  } else if (showdown) {
997
926
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
998
927
  if (move.effect == .Binding) {
999
- Effects.binding(battle, player, rewrap);
928
+ Effects.binding(battle, player);
1000
929
  if (immune) {
1001
930
  battle.last_damage = 0;
1002
- assert(foe.stored().hp > 0);
1003
931
  // 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
- }
932
+ try log.damage(battle.active(player.foe()), foe.stored(), .None);
1010
933
  return null;
1011
934
  }
1012
935
  }
@@ -1023,8 +946,8 @@ fn doMove(
1023
946
 
1024
947
  if (foe.stored().hp == 0) return null;
1025
948
 
1026
- // Pokémon Showdown builds Rage at the wrong time for non-MultiHit move
1027
- if (late and !sub and move.effect != .Disable) try buildRage(battle, player.foe(), log);
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);
1028
951
 
1029
952
  if (!move.effect.isSpecial()) {
1030
953
  // On the cartridge Rage is not considered to be "special" and thus gets executed for a
@@ -1057,7 +980,8 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
1057
980
  chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
1058
981
  chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
1059
982
 
1060
- 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;
1061
985
  }
1062
986
 
1063
987
  fn calcDamage(
@@ -1161,36 +1085,50 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
1161
1085
 
1162
1086
  fn randomizeDamage(battle: anytype) void {
1163
1087
  if (battle.last_damage <= 1) return;
1164
- 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
+
1165
1098
  battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
1166
1099
  }
1167
1100
 
1168
1101
  fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
1169
1102
  const side = battle.side(player);
1170
- const foe = battle.foe(player);
1171
1103
 
1172
1104
  if (!try checkHit(battle, player, move, log)) return null;
1173
1105
 
1174
1106
  battle.last_damage = switch (side.last_selected_move) {
1175
- .SuperFang => @max(foe.stored().hp / 2, 1),
1107
+ .SuperFang => @max(battle.foe(player).stored().hp / 2, 1),
1176
1108
  .SeismicToss, .NightShade => side.stored().level,
1177
1109
  .SonicBoom => 20,
1178
1110
  .DragonRage => 40,
1179
1111
  // GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
1180
1112
  .Psywave => power: {
1181
1113
  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);
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
+ }
1185
1124
  },
1186
1125
  else => unreachable,
1187
1126
  };
1188
1127
 
1189
1128
  if (battle.last_damage == 0) return if (showdown) null else Result.Error;
1190
1129
 
1191
- const sub = showdown and foe.active.volatiles.Substitute;
1192
1130
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
1193
- if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
1131
+ if (battle.foe(player).stored().hp > 0) try buildRage(battle, player.foe(), log);
1194
1132
 
1195
1133
  return null;
1196
1134
  }
@@ -1203,17 +1141,19 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
1203
1141
  return null;
1204
1142
  }
1205
1143
 
1206
- // 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);
1207
1147
  const foe_last_selected_move =
1208
1148
  Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
1209
- .Splash
1149
+ .Counter
1210
1150
  else
1211
1151
  foe.last_selected_move);
1212
1152
 
1213
- const used = (if (player.foe() == .P1)
1214
- battle.last_moves.p1_counterable
1215
- else
1216
- 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);
1217
1157
 
1218
1158
  const selected = foe_last_selected_move.bp > 0 and
1219
1159
  foe.last_selected_move != .Counter and
@@ -1232,14 +1172,12 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
1232
1172
  return null;
1233
1173
  }
1234
1174
 
1235
- battle.last_damage *|= 2;
1175
+ battle.last_damage = if (battle.last_damage > 0x7FFF) 0xFFFF else battle.last_damage * 2;
1236
1176
 
1237
1177
  // Pokémon Showdown calls checkHit before Counter
1238
1178
  if (!showdown and !try checkHit(battle, player, move, log)) return null;
1239
1179
 
1240
- const sub = showdown and foe.active.volatiles.Substitute;
1241
1180
  _ = 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
1181
  return null;
1244
1182
  }
1245
1183
 
@@ -1283,7 +1221,6 @@ fn mirrorMove(
1283
1221
  battle: anytype,
1284
1222
  player: Player,
1285
1223
  mslot: u4,
1286
- rewrap: bool,
1287
1224
  auto: bool,
1288
1225
  residual: *bool,
1289
1226
  log: anytype,
@@ -1296,30 +1233,41 @@ fn mirrorMove(
1296
1233
  if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
1297
1234
  try log.fail(battle.active(player), .None);
1298
1235
  return null;
1299
- } else if (!showdown or foe.last_used_move != .Struggle) {
1300
- incrementPP(side, mslot);
1301
1236
  }
1302
1237
 
1238
+ incrementPP(side, mslot);
1239
+
1303
1240
  if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
1304
- return doMove(battle, player, mslot, rewrap, auto, residual, log);
1241
+ return doMove(battle, player, mslot, auto, residual, log);
1305
1242
  }
1306
1243
 
1307
1244
  fn metronome(
1308
1245
  battle: anytype,
1309
1246
  player: Player,
1310
1247
  mslot: u4,
1311
- rewrap: bool,
1312
1248
  auto: bool,
1313
1249
  residual: *bool,
1314
1250
  log: anytype,
1315
1251
  ) !?Result {
1316
1252
  var side = battle.side(player);
1317
1253
 
1318
- 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
+
1319
1267
  incrementPP(side, mslot);
1320
1268
 
1321
1269
  if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
1322
- return doMove(battle, player, mslot, rewrap, auto, residual, log);
1270
+ return doMove(battle, player, mslot, auto, residual, log);
1323
1271
  }
1324
1272
 
1325
1273
  fn checkHit(battle: anytype, player: Player, move: Move.Data, log: anytype) !bool {
@@ -1373,24 +1321,23 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
1373
1321
  }
1374
1322
 
1375
1323
  // 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));
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));
1382
1328
  var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
1383
1329
  accuracy = accuracy * boost[0] / boost[1];
1384
1330
  boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
1385
1331
  accuracy = accuracy * boost[0] / boost[1];
1386
1332
  accuracy = @min(255, @max(1, accuracy));
1387
1333
 
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;
1334
+ if (move.effect == .Thrashing or move.effect == .Rage) state.* = accuracy;
1391
1335
 
1392
1336
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1393
- 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;
1394
1341
  };
1395
1342
 
1396
1343
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1522,6 +1469,9 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1522
1469
  damage *= volatiles.toxic;
1523
1470
  }
1524
1471
 
1472
+ // Pokémon Showdown erroneously updates last damage with uncapped Leech Seed damage
1473
+ if (showdown) battle.last_damage = damage;
1474
+
1525
1475
  const amount = @min(damage, stored.hp);
1526
1476
  stored.hp -= amount;
1527
1477
 
@@ -1540,13 +1490,13 @@ fn endTurn(battle: anytype, log: anytype) @TypeOf(log).Error!Result {
1540
1490
  assert(!battle.side(.P1).active.volatiles.MultiHit);
1541
1491
  assert(!battle.side(.P2).active.volatiles.MultiHit);
1542
1492
 
1543
- battle.turn += 1;
1544
-
1545
1493
  if (showdown and options.ebc and checkEBC(battle)) {
1546
1494
  try log.tie();
1547
1495
  return Result.Tie;
1548
1496
  }
1549
1497
 
1498
+ battle.turn += 1;
1499
+
1550
1500
  if (showdown and battle.turn >= 1000) {
1551
1501
  try log.tie();
1552
1502
  return Result.Tie;
@@ -1613,42 +1563,6 @@ fn checkEBC(battle: anytype) bool {
1613
1563
  return false;
1614
1564
  }
1615
1565
 
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
1566
  inline fn onBegin(
1653
1567
  battle: anytype,
1654
1568
  player: Player,
@@ -1739,7 +1653,10 @@ pub const Effects = struct {
1739
1653
  side.active.volatiles.Bide = true;
1740
1654
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1741
1655
  side.active.volatiles.state = 0;
1742
- 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);
1743
1660
 
1744
1661
  try log.start(battle.active(player), .Bide);
1745
1662
  }
@@ -1761,8 +1678,14 @@ pub const Effects = struct {
1761
1678
  return;
1762
1679
  }
1763
1680
 
1764
- if (foe.active.types.includes(move.type)) return;
1765
- 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;
1766
1689
 
1767
1690
  foe_stored.status = Status.init(.BRN);
1768
1691
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
@@ -1786,7 +1709,11 @@ pub const Effects = struct {
1786
1709
  const sub = foe.active.volatiles.Substitute;
1787
1710
 
1788
1711
  if (move.effect == .ConfusionChance) {
1789
- 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;
1790
1717
  } else {
1791
1718
  if (showdown) {
1792
1719
  if (!try checkHit(battle, player, move, log)) {
@@ -1805,7 +1732,10 @@ pub const Effects = struct {
1805
1732
 
1806
1733
  if (foe.active.volatiles.Confusion) return;
1807
1734
  foe.active.volatiles.Confusion = true;
1808
- 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);
1809
1739
 
1810
1740
  try log.start(battle.active(player.foe()), .Confusion);
1811
1741
  }
@@ -1855,8 +1785,11 @@ pub const Effects = struct {
1855
1785
  return Result.Error;
1856
1786
  }
1857
1787
 
1858
- volatiles.disabled_move = @intCast(u3, Rolls.moveSlot(battle, &foe.active.moves, n));
1859
- 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);
1860
1793
 
1861
1794
  try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
1862
1795
  return null;
@@ -1868,7 +1801,7 @@ pub const Effects = struct {
1868
1801
 
1869
1802
  if (battle.last_damage == 0) {
1870
1803
  assert(showdown);
1871
- if (!battle.foe(player).active.volatiles.Substitute) return;
1804
+ return;
1872
1805
  }
1873
1806
 
1874
1807
  const drain = @max(battle.last_damage / 2, 1);
@@ -1895,7 +1828,15 @@ pub const Effects = struct {
1895
1828
  var volatiles = &battle.foe(player).active.volatiles;
1896
1829
 
1897
1830
  if (volatiles.Substitute) return;
1898
- 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;
1899
1840
 
1900
1841
  volatiles.Flinch = true;
1901
1842
  volatiles.Recharging = false;
@@ -1921,8 +1862,12 @@ pub const Effects = struct {
1921
1862
  return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
1922
1863
  }
1923
1864
 
1924
- if (foe.active.types.includes(move.type)) return;
1925
- 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
+
1926
1871
  // Freeze Clause Mod
1927
1872
  if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1928
1873
 
@@ -2059,15 +2004,15 @@ pub const Effects = struct {
2059
2004
  }
2060
2005
  break :has_mimic false;
2061
2006
  };
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;
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) {
2065
2010
  return try log.fail(battle.active(player.foe()), .None);
2066
2011
  }
2067
2012
  }
2068
2013
  if (!try checkHit(battle, player, move, log)) return;
2069
2014
 
2070
- const rslot = Rolls.moveSlot(battle, &foe.active.moves, 0);
2015
+ const rslot = randomMoveSlot(&battle.rng, &foe.active.moves, 0);
2071
2016
  side.active.move(oslot).id = foe.active.move(rslot).id;
2072
2017
 
2073
2018
  try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
@@ -2088,8 +2033,7 @@ pub const Effects = struct {
2088
2033
  assert(!side.active.volatiles.MultiHit);
2089
2034
  side.active.volatiles.MultiHit = true;
2090
2035
 
2091
- side.active.volatiles.attacks =
2092
- if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
2036
+ side.active.volatiles.attacks = if (move.effect == .MultiHit) distribution(battle) else 2;
2093
2037
  }
2094
2038
 
2095
2039
  fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2133,8 +2077,14 @@ pub const Effects = struct {
2133
2077
  }
2134
2078
 
2135
2079
  // 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;
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;
2138
2088
 
2139
2089
  foe_stored.status = Status.init(.PAR);
2140
2090
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
@@ -2177,7 +2127,16 @@ pub const Effects = struct {
2177
2127
 
2178
2128
  if (move.effect == .Poison) {
2179
2129
  if (!showdown and !try checkHit(battle, player, move, log)) return;
2180
- } 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
+ }
2181
2140
 
2182
2141
  foe_stored.status = Status.init(.PSN);
2183
2142
  if (toxic) {
@@ -2191,8 +2150,10 @@ pub const Effects = struct {
2191
2150
 
2192
2151
  fn rage(battle: anytype, player: Player) !void {
2193
2152
  var volatiles = &battle.side(player).active.volatiles;
2194
- assert(!volatiles.Bide);
2153
+ if (volatiles.Rage) return;
2195
2154
  volatiles.Rage = true;
2155
+ assert(!volatiles.Bide);
2156
+ volatiles.state = 0;
2196
2157
  }
2197
2158
 
2198
2159
  fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
@@ -2244,7 +2205,16 @@ pub const Effects = struct {
2244
2205
  }
2245
2206
  foe.active.volatiles.Recharging = false;
2246
2207
 
2247
- 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);
2248
2218
  try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
2249
2219
  }
2250
2220
 
@@ -2297,7 +2267,11 @@ pub const Effects = struct {
2297
2267
  assert(!volatiles.Bide);
2298
2268
 
2299
2269
  volatiles.Thrashing = true;
2300
- 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);
2301
2275
  }
2302
2276
 
2303
2277
  fn transform(battle: anytype, player: Player, log: anytype) !void {
@@ -2329,7 +2303,7 @@ pub const Effects = struct {
2329
2303
  try log.transform(battle.active(player), foe_ident);
2330
2304
  }
2331
2305
 
2332
- fn binding(battle: anytype, player: Player, rewrap: bool) void {
2306
+ fn binding(battle: anytype, player: Player) void {
2333
2307
  var side = battle.side(player);
2334
2308
  var foe = battle.foe(player);
2335
2309
 
@@ -2337,11 +2311,9 @@ pub const Effects = struct {
2337
2311
  side.active.volatiles.Binding = true;
2338
2312
  // GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
2339
2313
  // (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);
2314
+ if (!showdown) foe.active.volatiles.Recharging = false;
2343
2315
 
2344
- side.active.volatiles.attacks = Rolls.distribution(battle) - 1;
2316
+ side.active.volatiles.attacks = distribution(battle) - 1;
2345
2317
  }
2346
2318
 
2347
2319
  fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2368,7 +2340,7 @@ pub const Effects = struct {
2368
2340
  return try log.fail(ident, .None);
2369
2341
  }
2370
2342
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2371
- const stat = unmodifiedStats(battle, side).atk;
2343
+ const stat = unmodifiedStats(battle, player).atk;
2372
2344
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2373
2345
  try log.boost(ident, reason, n);
2374
2346
  // Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
@@ -2388,7 +2360,7 @@ pub const Effects = struct {
2388
2360
  return try log.fail(ident, .None);
2389
2361
  }
2390
2362
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2391
- const stat = unmodifiedStats(battle, side).def;
2363
+ const stat = unmodifiedStats(battle, player).def;
2392
2364
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2393
2365
  try log.boost(ident, .Defense, n);
2394
2366
  },
@@ -2405,7 +2377,7 @@ pub const Effects = struct {
2405
2377
  return try log.fail(ident, .None);
2406
2378
  }
2407
2379
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2408
- const stat = unmodifiedStats(battle, side).spe;
2380
+ const stat = unmodifiedStats(battle, player).spe;
2409
2381
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2410
2382
  try log.boost(ident, .Speed, 2);
2411
2383
  },
@@ -2425,7 +2397,7 @@ pub const Effects = struct {
2425
2397
  return try log.fail(ident, .None);
2426
2398
  }
2427
2399
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2428
- const stat = unmodifiedStats(battle, side).spc;
2400
+ const stat = unmodifiedStats(battle, player).spc;
2429
2401
  stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2430
2402
  try log.boost(ident, .SpecialAttack, n);
2431
2403
  try log.boost(ident, .SpecialDefense, n);
@@ -2452,7 +2424,11 @@ pub const Effects = struct {
2452
2424
  }
2453
2425
 
2454
2426
  if (move.effect.isStatDownChance()) {
2455
- if (!Rolls.unboost(battle) or foe.active.volatiles.Invulnerable) return;
2427
+ const chance = if (showdown)
2428
+ battle.rng.chance(u8, 85, 256)
2429
+ else
2430
+ battle.rng.next() < Gen12.percent(33) + 1;
2431
+ if (!chance or foe.active.volatiles.Invulnerable) return;
2456
2432
  } else if (!showdown and !try checkHit(battle, player, move, log)) {
2457
2433
  return; // checkHit already checks for Invulnerable
2458
2434
  }
@@ -2474,7 +2450,7 @@ pub const Effects = struct {
2474
2450
  return try log.fail(foe_ident, .None);
2475
2451
  }
2476
2452
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2477
- const stat = unmodifiedStats(battle, foe).atk;
2453
+ const stat = unmodifiedStats(battle, player.foe()).atk;
2478
2454
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2479
2455
  try log.boost(foe_ident, .Attack, -1);
2480
2456
  },
@@ -2492,7 +2468,7 @@ pub const Effects = struct {
2492
2468
  return try log.fail(foe_ident, .None);
2493
2469
  }
2494
2470
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2495
- const stat = unmodifiedStats(battle, foe).def;
2471
+ const stat = unmodifiedStats(battle, player.foe()).def;
2496
2472
  stats.def = @max(1, stat * mod[0] / mod[1]);
2497
2473
  try log.boost(foe_ident, .Defense, -@as(i8, n));
2498
2474
  },
@@ -2509,7 +2485,7 @@ pub const Effects = struct {
2509
2485
  return try log.fail(foe_ident, .None);
2510
2486
  }
2511
2487
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2512
- const stat = unmodifiedStats(battle, foe).spe;
2488
+ const stat = unmodifiedStats(battle, player.foe()).spe;
2513
2489
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2514
2490
  try log.boost(foe_ident, .Speed, -1);
2515
2491
  assert(boosts.spe >= -6);
@@ -2529,7 +2505,7 @@ pub const Effects = struct {
2529
2505
  return try log.fail(foe_ident, .None);
2530
2506
  }
2531
2507
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2532
- const stat = unmodifiedStats(battle, foe).spc;
2508
+ const stat = unmodifiedStats(battle, player.foe()).spc;
2533
2509
  stats.spc = @max(1, stat * mod[0] / mod[1]);
2534
2510
  try log.boost(foe_ident, .SpecialAttack, -1);
2535
2511
  try log.boost(foe_ident, .SpecialDefense, -1);
@@ -2548,7 +2524,8 @@ pub const Effects = struct {
2548
2524
  }
2549
2525
  };
2550
2526
 
2551
- fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
2527
+ fn unmodifiedStats(battle: anytype, who: Player) *Stats(u16) {
2528
+ const side = battle.side(who);
2552
2529
  if (!side.active.volatiles.Transform) return &side.stored().stats;
2553
2530
  const id = ID.from(side.active.volatiles.transform);
2554
2531
  return &battle.side(id.player).pokemon[id.id - 1].stats;
@@ -2614,140 +2591,80 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
2614
2591
  }
2615
2592
  }
2616
2593
 
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;
2594
+ fn disabled(side: *Side, ident: ID, log: anytype) !bool {
2595
+ if (side.active.volatiles.disabled_move != 0) {
2596
+ // A Pokémon that transforms after being disabled may end up with less move slots
2597
+ const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
2598
+ // side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
2599
+ const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
2600
+ m
2601
+ else
2602
+ side.last_selected_move;
2603
+ if (m != .None and m == last) {
2604
+ side.active.volatiles.Charging = false;
2605
+ try log.disabled(ident, last);
2606
+ return true;
2633
2607
  }
2634
2608
  }
2609
+ return false;
2610
+ }
2635
2611
 
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);
2612
+ inline fn buildRage(battle: anytype, who: Player, log: anytype) !void {
2613
+ const side = battle.side(who);
2614
+ if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
2615
+ try Effects.boost(battle, who, Move.get(.Rage), log);
2654
2616
  }
2617
+ }
2655
2618
 
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
- }
2619
+ fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
2620
+ var volatiles = &active.volatiles;
2621
+ assert(volatiles.Thrashing);
2622
+ if (volatiles.attacks > 0) return false;
2660
2623
 
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
- }
2624
+ volatiles.Thrashing = false;
2625
+ volatiles.Confusion = true;
2626
+ volatiles.confusion = @intCast(u3, if (showdown)
2627
+ battle.rng.range(u8, 2, 6)
2628
+ else
2629
+ (battle.rng.next() & 3) + 2);
2665
2630
 
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
- }
2631
+ return true;
2632
+ }
2670
2633
 
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
- }
2634
+ const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
2678
2635
 
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
- }
2636
+ fn distribution(battle: anytype) u3 {
2637
+ if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
2638
+ const r = (battle.rng.next() & 3);
2639
+ return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
2640
+ }
2683
2641
 
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
- }
2642
+ fn randomMoveSlot(rand: anytype, moves: []MoveSlot, check_pp: u4) u4 {
2643
+ if (showdown) {
2644
+ if (check_pp == 0) {
2645
+ var i: usize = moves.len;
2646
+ while (i > 0) {
2647
+ i -= 1;
2648
+ if (moves[i].id != .None) return rand.range(u4, 0, @intCast(u4, i + 1)) + 1;
2649
+ }
2650
+ } else {
2651
+ var r = rand.range(u4, 0, @intCast(u4, check_pp)) + 1;
2652
+ var i: usize = 0;
2653
+ while (i < moves.len and r > 0) : (i += 1) {
2654
+ if (moves[i].pp > 0) {
2655
+ r -= 1;
2656
+ if (r == 0) break;
2702
2657
  }
2703
- return @intCast(u4, i + 1);
2704
2658
  }
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;
2659
+ return @intCast(u4, i + 1);
2737
2660
  }
2738
2661
  }
2739
2662
 
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);
2663
+ while (true) {
2664
+ const r = @intCast(u4, rand.next() & 3);
2665
+ if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
2744
2666
  }
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
- };
2667
+ }
2751
2668
 
2752
2669
  test "RNG agreement" {
2753
2670
  if (!showdown) return;
@@ -2847,7 +2764,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
2847
2764
  while (slot <= 4) : (slot += 1) {
2848
2765
  const m = active.moves[slot - 1];
2849
2766
  if (m.id == .None) break;
2850
- if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
2767
+ if (m.id == side.last_selected_move) {
2851
2768
  // Pokémon Showdown displays Struggle if limited to Bide but unable to pick
2852
2769
  const struggle =
2853
2770
  m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);