@pkmn/engine 0.1.0-dev.c4dec0f7 → 0.1.0-dev.cf6f5d2c

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -56,30 +56,34 @@ const BOOSTS = &[_][2]u8{
56
56
 
57
57
  const MAX_STAT_VALUE = 999;
58
58
 
59
+ const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
60
+
59
61
  pub fn update(battle: anytype, c1: Choice, c2: Choice, log: anytype) !Result {
60
62
  assert(c1.type != .Pass or c2.type != .Pass or battle.turn == 0);
61
63
  if (battle.turn == 0) return start(battle, log);
62
64
 
65
+ var s1 = false;
66
+ var s2 = false;
63
67
  var f1: ?Move = null;
64
68
  var f2: ?Move = null;
65
69
 
66
- if (selectMove(battle, .P1, c1, c2, &f1)) |r| return r;
67
- if (selectMove(battle, .P2, c2, c1, &f2)) |r| return r;
70
+ if (selectMove(battle, .P1, c1, c2, &s1, &f1)) |r| return r;
71
+ if (selectMove(battle, .P2, c2, c1, &s2, &f2)) |r| return r;
72
+
73
+ var p1 = battle.side(.P1);
74
+ var p2 = battle.side(.P2);
75
+
76
+ var r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
77
+ var r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
68
78
 
69
79
  if (turnOrder(battle, c1, c2) == .P1) {
70
- if (try doTurn(battle, .P1, c1, f1, .P2, c2, f2, log)) |r| return r;
80
+ if (try doTurn(battle, .P1, c1, r1, s1, f1, .P2, c2, r2, s2, f2, log)) |r| return r;
71
81
  } else {
72
- if (try doTurn(battle, .P2, c2, f2, .P1, c1, f1, log)) |r| return r;
82
+ if (try doTurn(battle, .P2, c2, r2, s2, f2, .P1, c1, r1, s1, f1, log)) |r| return r;
73
83
  }
74
84
 
75
- var p1 = battle.side(.P1);
76
- if (p1.active.volatiles.attacks == 0) {
77
- p1.active.volatiles.Binding = false;
78
- }
79
- var p2 = battle.side(.P2);
80
- if (p2.active.volatiles.attacks == 0) {
81
- p2.active.volatiles.Binding = false;
82
- }
85
+ if (p1.active.volatiles.attacks == 0) p1.active.volatiles.Binding = false;
86
+ if (p2.active.volatiles.attacks == 0) p2.active.volatiles.Binding = false;
83
87
 
84
88
  return endTurn(battle, log);
85
89
  }
@@ -112,6 +116,7 @@ fn selectMove(
112
116
  player: Player,
113
117
  choice: Choice,
114
118
  foe_choice: Choice,
119
+ skip_turn: *bool,
115
120
  from: *?Move,
116
121
  ) ?Result {
117
122
  if (choice.type == .Pass) return null;
@@ -124,17 +129,26 @@ fn selectMove(
124
129
  (choice.type == .Move and choice.data == @boolToInt(showdown)));
125
130
 
126
131
  // pre-battle menu
127
- if (volatiles.Recharging) return null;
132
+ if (volatiles.Recharging) {
133
+ if (showdown and battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
134
+ return null;
135
+ }
128
136
  if (volatiles.Rage) {
129
137
  from.* = side.last_used_move;
130
- if (showdown) saveMove(battle, player, null);
138
+ if (showdown) {
139
+ if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
140
+ saveMove(battle, player, null);
141
+ }
131
142
  return null;
132
143
  }
133
144
  // Pokémon Showdown removes Flinch at the end-of-turn in its residual handler
134
145
  if (!showdown) volatiles.Flinch = false;
135
146
  if (volatiles.Thrashing or volatiles.Charging) {
136
147
  from.* = side.last_selected_move;
137
- if (showdown) saveMove(battle, player, null);
148
+ if (showdown) {
149
+ if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
150
+ saveMove(battle, player, null);
151
+ }
138
152
  return null;
139
153
  }
140
154
 
@@ -144,7 +158,10 @@ fn selectMove(
144
158
  // pre-move select
145
159
  if (Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP) or volatiles.Bide) {
146
160
  assert(showdown or choice.data == 0);
147
- if (showdown) saveMove(battle, player, choice);
161
+ if (showdown) {
162
+ if (volatiles.Bide and battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
163
+ saveMove(battle, player, choice);
164
+ }
148
165
  return null;
149
166
  }
150
167
  if (volatiles.Binding) {
@@ -175,6 +192,7 @@ fn selectMove(
175
192
  }
176
193
 
177
194
  if (battle.foe(player).active.volatiles.Binding) {
195
+ skip_turn.* = true;
178
196
  if (showdown) {
179
197
  saveMove(battle, player, choice);
180
198
  } else {
@@ -185,6 +203,7 @@ fn selectMove(
185
203
  }
186
204
 
187
205
  // move select
206
+ volatiles.state = 0;
188
207
  if (choice.data == 0) {
189
208
  const struggle = ok: {
190
209
  for (side.active.moves, 0..) |move, i| {
@@ -301,11 +320,7 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
301
320
  // Pokémon Showdown's beforeTurnCallback shenanigans
302
321
  if (showdown and m1 == .Counter and m2 == .Counter) battle.rng.advance(1);
303
322
 
304
- const p1 = if (showdown)
305
- battle.rng.range(u8, 0, 2) == 0
306
- else
307
- battle.rng.next() < Gen12.percent(50) + 1;
308
-
323
+ const p1 = Rolls.speedTie(battle);
309
324
  if (!showdown) return if (p1) .P1 else .P2;
310
325
 
311
326
  // Pokémon Showdown's "lockedmove" volatile's onBeforeTurn uses BattleQueue#changeAction,
@@ -315,7 +330,7 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
315
330
  const t1 = battle.side(.P1).active.volatiles.Thrashing;
316
331
  const t2 = battle.side(.P2).active.volatiles.Thrashing;
317
332
  // If *both* sides are thrashing it really should be another speed tie, but we've patched
318
- // that out and enforce host ordering of events, so P1 just goes first reqgardless of who
333
+ // that out and enforce host ordering of events, so P1 just goes first regardless of who
319
334
  // won the original coin flip
320
335
  if (t1 and t2) return .P1;
321
336
  return if (p1) if (t1 and !t2) .P2 else .P1 else if (t2 and !t1) .P1 else .P2;
@@ -328,9 +343,13 @@ fn doTurn(
328
343
  battle: anytype,
329
344
  player: Player,
330
345
  player_choice: Choice,
346
+ player_rewrap: bool,
347
+ player_skip: bool,
331
348
  player_from: ?Move,
332
349
  foe_player: Player,
333
350
  foe_choice: Choice,
351
+ foe_rewrap: bool,
352
+ foe_skip: bool,
334
353
  foe_from: ?Move,
335
354
  log: anytype,
336
355
  ) !?Result {
@@ -338,7 +357,16 @@ fn doTurn(
338
357
 
339
358
  var residual = true;
340
359
  var replace = battle.side(player).stored().hp == 0;
341
- if (try executeMove(battle, player, player_choice, player_from, &residual, log)) |r| return r;
360
+ if (try executeMove(
361
+ battle,
362
+ player,
363
+ player_choice,
364
+ player_rewrap,
365
+ player_skip,
366
+ player_from,
367
+ &residual,
368
+ log,
369
+ )) |r| return r;
342
370
  if (!replace) {
343
371
  if (player_choice.type != .Switch) {
344
372
  if (try checkFaint(battle, foe_player, log)) |r| return r;
@@ -347,8 +375,18 @@ fn doTurn(
347
375
  if (try checkFaint(battle, player, log)) |r| return r;
348
376
  } else if (foe_choice.type == .Pass) return null;
349
377
 
378
+ residual = true;
350
379
  replace = battle.side(foe_player).stored().hp == 0;
351
- if (try executeMove(battle, foe_player, foe_choice, foe_from, &residual, log)) |r| return r;
380
+ if (try executeMove(
381
+ battle,
382
+ foe_player,
383
+ foe_choice,
384
+ foe_rewrap,
385
+ foe_skip,
386
+ foe_from,
387
+ &residual,
388
+ log,
389
+ )) |r| return r;
352
390
  if (!replace) {
353
391
  if (foe_choice.type != .Switch) {
354
392
  if (try checkFaint(battle, player, log)) |r| return r;
@@ -371,6 +409,8 @@ fn executeMove(
371
409
  battle: anytype,
372
410
  player: Player,
373
411
  choice: Choice,
412
+ rewrap: bool,
413
+ skip: bool,
374
414
  from: ?Move,
375
415
  residual: *bool,
376
416
  log: anytype,
@@ -382,9 +422,10 @@ fn executeMove(
382
422
  return null;
383
423
  }
384
424
 
425
+ // This is the correct place to check for SKIP_TURN and abort early, however since Pokémon
426
+ // Showdown overwrites the SKIP_TURN sentinel with its botched move select we need to add an
427
+ // additional skip boolean to accomplish the same thing in the Binding check of BeforeMove
385
428
  if (side.last_selected_move == .SKIP_TURN) {
386
- // Pokémon Showdown overwrites the SKIP_TURN sentinel with its botched move select,
387
- // Binding instead gets handled in beforeMove after sleep and freeze
388
429
  assert(!showdown);
389
430
  if (battle.foe(player).active.volatiles.Binding) {
390
431
  try log.cant(battle.active(player), .Bound);
@@ -434,7 +475,7 @@ fn executeMove(
434
475
 
435
476
  var skip_can = false;
436
477
  var skip_pp = false;
437
- switch (try beforeMove(battle, player, from, log)) {
478
+ switch (try beforeMove(battle, player, skip, from, residual, log)) {
438
479
  .done => return null,
439
480
  .skip_can => skip_can = true,
440
481
  .skip_pp => skip_pp = true,
@@ -446,12 +487,19 @@ fn executeMove(
446
487
  return null;
447
488
  }
448
489
 
449
- return doMove(battle, player, mslot, auto, residual, log);
490
+ return doMove(battle, player, mslot, rewrap, auto, residual, log);
450
491
  }
451
492
 
452
- const BeforeMove = union(enum) { done, skip_can, skip_pp, ok, err };
493
+ const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
453
494
 
454
- fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !BeforeMove {
495
+ fn beforeMove(
496
+ battle: anytype,
497
+ player: Player,
498
+ skip: bool,
499
+ from: ?Move,
500
+ residual: *bool,
501
+ log: anytype,
502
+ ) !BeforeMove {
455
503
  var side = battle.side(player);
456
504
  const foe = battle.foe(player);
457
505
  var active = &side.active;
@@ -479,13 +527,15 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
479
527
  return .done;
480
528
  }
481
529
 
482
- if (foe.active.volatiles.Binding) {
530
+ if (skip or foe.active.volatiles.Binding) {
483
531
  try log.cant(ident, .Bound);
484
532
  return .done;
485
533
  }
486
534
 
487
- if (!showdown and volatiles.Flinch) {
488
- volatiles.Flinch = false;
535
+ if (volatiles.Flinch) {
536
+ // Pokémon Showdown doesn't clear Flinch until its imaginary "residual" phase, meaning
537
+ // Pokémon can sometimes flinch multiple times from the same original hit
538
+ if (!showdown) volatiles.Flinch = false;
489
539
  try log.cant(ident, .Flinch);
490
540
  return .done;
491
541
  }
@@ -506,13 +556,6 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
506
556
  // Pokémon Showdown's disable condition has a single onBeforeMove handler
507
557
  if (showdown and try disabled(side, ident, log)) return .done;
508
558
 
509
- // Pokémon Showdown checks for Flinch *after* instead of before
510
- if (showdown and volatiles.Flinch) {
511
- volatiles.Flinch = false;
512
- try log.cant(ident, .Flinch);
513
- return .done;
514
- }
515
-
516
559
  // This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
517
560
  // before the side had a selected a move - we simply need to assume this leads to a desync
518
561
  if (side.last_selected_move == .None) {
@@ -530,14 +573,9 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
530
573
  } else {
531
574
  try log.activate(ident, .Confusion);
532
575
 
533
- const confused = if (showdown)
534
- !battle.rng.chance(u8, 128, 256)
535
- else
536
- battle.rng.next() >= Gen12.percent(50) + 1;
537
-
538
- if (confused) {
576
+ if (Rolls.confused(battle)) {
539
577
  assert(!volatiles.MultiHit);
540
- volatiles.state = 0;
578
+ if (!volatiles.Rage) volatiles.state = 0;
541
579
  volatiles.Bide = false;
542
580
  volatiles.Thrashing = false;
543
581
  volatiles.MultiHit = false;
@@ -561,8 +599,12 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
561
599
  player
562
600
  else
563
601
  player.foe();
602
+
603
+ const uncapped = battle.last_damage;
564
604
  // Skipping adjustDamage / randomizeDamage / checkHit
565
605
  _ = try applyDamage(battle, player, target, .Confusion, log);
606
+ // Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
607
+ if (showdown) battle.last_damage = uncapped;
566
608
 
567
609
  return .done;
568
610
  }
@@ -571,22 +613,15 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
571
613
 
572
614
  if (!showdown and try disabled(side, ident, log)) return .done;
573
615
 
574
- if (Status.is(stored.status, .PAR)) {
575
- const paralyzed = if (showdown)
576
- battle.rng.chance(u8, 63, 256)
577
- else
578
- battle.rng.next() < Gen12.percent(25);
579
-
580
- if (paralyzed) {
581
- volatiles.state = 0;
582
- volatiles.Bide = false;
583
- volatiles.Thrashing = false;
584
- volatiles.Charging = false;
585
- volatiles.Binding = false;
586
- // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
587
- try log.cant(ident, .Paralysis);
588
- return .done;
589
- }
616
+ if (Status.is(stored.status, .PAR) and Rolls.paralyzed(battle)) {
617
+ if (!volatiles.Rage) volatiles.state = 0;
618
+ volatiles.Bide = false;
619
+ volatiles.Thrashing = false;
620
+ volatiles.Charging = false;
621
+ volatiles.Binding = false;
622
+ // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
623
+ try log.cant(ident, .Paralysis);
624
+ return .done;
590
625
  }
591
626
 
592
627
  if (volatiles.Bide) {
@@ -613,8 +648,17 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
613
648
  return .done;
614
649
  }
615
650
 
651
+ const sub = showdown and foe.active.volatiles.Substitute;
616
652
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
617
- try buildRage(battle, player.foe(), log);
653
+ if (foe.stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
654
+
655
+ // For reasons passing understanding, Pokémon Showdown still inflicts residual damage to
656
+ // Bide's user even if the above damage has caused the foe to faint. It's simpler to always
657
+ // run residual here regardless of whether the foe fainted and opt-out of the default flow
658
+ if (showdown) {
659
+ residual.* = false;
660
+ try handleResidual(battle, player, log);
661
+ }
618
662
 
619
663
  return .done;
620
664
  }
@@ -627,8 +671,13 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
627
671
  try log.start(battle.active(player), .ConfusionSilent);
628
672
  }
629
673
  try log.move(ident, side.last_selected_move, battle.active(player.foe()), from);
630
- if (showdown and handleThrashing(battle, active)) {
631
- try log.start(battle.active(player), .ConfusionSilent);
674
+ if (showdown) {
675
+ // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
676
+ // used in useMove and doesn't have the notion of skipping canMove semantics
677
+ side.last_used_move = side.last_selected_move;
678
+ if (handleThrashing(battle, active)) {
679
+ try log.start(battle.active(player), .ConfusionSilent);
680
+ }
632
681
  }
633
682
  return .skip_can;
634
683
  }
@@ -643,7 +692,11 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
643
692
  side.last_selected_move,
644
693
  );
645
694
  if (showdown or battle.last_damage != 0) {
695
+ const sub = showdown and foe.active.volatiles.Substitute;
646
696
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
697
+ if (battle.foe(player).stored().hp > 0 and !sub) {
698
+ try buildRage(battle, player.foe(), log);
699
+ }
647
700
  }
648
701
  return .done;
649
702
  }
@@ -701,7 +754,7 @@ fn canMove(
701
754
  Effects.thrashing(battle, player);
702
755
  } else if (!showdown and move.effect == .Binding) {
703
756
  // Pokémon Showdown handles this after hit/miss checks and damage calculation
704
- Effects.binding(battle, player);
757
+ Effects.binding(battle, player, false);
705
758
  }
706
759
 
707
760
  return true;
@@ -742,6 +795,7 @@ fn doMove(
742
795
  battle: anytype,
743
796
  player: Player,
744
797
  mslot: u4,
798
+ rewrap: bool,
745
799
  auto: bool,
746
800
  residual: *bool,
747
801
  log: anytype,
@@ -772,12 +826,15 @@ fn doMove(
772
826
  (@enumToInt(move.type.effectiveness(foe.active.types.type1)) == 0 or
773
827
  @enumToInt(move.type.effectiveness(foe.active.types.type2)) == 0);
774
828
  if (immune and move.effect != .Binding) break :miss true;
775
- if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) break :miss true;
829
+ if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) {
830
+ battle.last_damage = 0;
831
+ break :miss true;
832
+ }
776
833
  break :miss move.target != .Self and !moveHit(battle, player, move, &immune, &mist);
777
834
  };
778
835
  assert(!immune or miss or (showdown and move.effect == .Binding));
779
836
 
780
- var late = showdown;
837
+ var late = showdown and move.effect != .Explode;
781
838
  const skip = status or immune;
782
839
  if ((!showdown or (!skip or counter)) and !miss) blk: {
783
840
  if (showdown and move.effect.isMulti()) {
@@ -830,9 +887,9 @@ fn doMove(
830
887
 
831
888
  if (!showdown or !miss) {
832
889
  if (move.effect == .MirrorMove) {
833
- return mirrorMove(battle, player, mslot, auto, residual, log);
890
+ return mirrorMove(battle, player, mslot, rewrap, auto, residual, log);
834
891
  } else if (move.effect == .Metronome) {
835
- return metronome(battle, player, mslot, auto, residual, log);
892
+ return metronome(battle, player, mslot, rewrap, auto, residual, log);
836
893
  } else if (move.effect.onEnd()) {
837
894
  try onEnd(battle, player, move, log);
838
895
  return null;
@@ -847,7 +904,7 @@ fn doMove(
847
904
  move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
848
905
  if (ohko) {
849
906
  try log.immune(foe_ident, .OHKO);
850
- } else if (immune and !invulnerable) {
907
+ } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
851
908
  try log.immune(foe_ident, .None);
852
909
  } else if (mist) {
853
910
  if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
@@ -879,6 +936,9 @@ fn doMove(
879
936
  hits = side.active.volatiles.attacks;
880
937
  }
881
938
 
939
+ // Pokémon Showdown only builds Rage for Disable/Explosion (hit/miss) when attacking into a sub
940
+ const sub = showdown and foe.active.volatiles.Substitute;
941
+
882
942
  var nullified = false;
883
943
  var hit: u4 = 0;
884
944
  while (hit < hits) {
@@ -894,7 +954,7 @@ fn doMove(
894
954
  if (hit == 0 and ohko) try log.ohko();
895
955
  hit += 1;
896
956
  if (foe.stored().hp == 0) break;
897
- if (!late) try buildRage(battle, player.foe(), log);
957
+ if (!late and (!sub or move.effect == .Explode)) try buildRage(battle, player.foe(), log);
898
958
  // If the substitute breaks during a multi-hit attack, the attack ends
899
959
  if (nullified) break;
900
960
  }
@@ -910,11 +970,17 @@ fn doMove(
910
970
  } else if (showdown) {
911
971
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
912
972
  if (move.effect == .Binding) {
913
- Effects.binding(battle, player);
973
+ Effects.binding(battle, player, rewrap);
914
974
  if (immune) {
915
975
  battle.last_damage = 0;
976
+ assert(foe.stored().hp > 0);
916
977
  // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
917
- try log.damage(battle.active(player.foe()), foe.stored(), .None);
978
+ if (sub) {
979
+ try log.activate(battle.active(player.foe()), .Substitute);
980
+ } else {
981
+ try log.damage(battle.active(player.foe()), foe.stored(), .None);
982
+ try buildRage(battle, player.foe(), log);
983
+ }
918
984
  return null;
919
985
  }
920
986
  }
@@ -931,8 +997,8 @@ fn doMove(
931
997
 
932
998
  if (foe.stored().hp == 0) return null;
933
999
 
934
- // Pokémon Showdown builds Rage at the wrong time for non-MultiHit moves
935
- if (late) try buildRage(battle, player.foe(), log);
1000
+ // Pokémon Showdown builds Rage at the wrong time for non-MultiHit move
1001
+ if (late and !sub and move.effect != .Disable) try buildRage(battle, player.foe(), log);
936
1002
 
937
1003
  if (!move.effect.isSpecial()) {
938
1004
  // On the cartridge Rage is not considered to be "special" and thus gets executed for a
@@ -945,7 +1011,9 @@ fn doMove(
945
1011
  if (move.effect == .Twineedle) {
946
1012
  if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), log);
947
1013
  } else if (move.effect == .Disable) {
948
- return Effects.disable(battle, player, move, log);
1014
+ const result = try Effects.disable(battle, player, move, log);
1015
+ if (showdown) try buildRage(battle, player.foe(), log);
1016
+ return result;
949
1017
  } else {
950
1018
  try moveEffect(battle, player, move, log);
951
1019
  }
@@ -963,8 +1031,7 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
963
1031
  chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
964
1032
  chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
965
1033
 
966
- if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
967
- return std.math.rotl(u8, battle.rng.next(), 3) < chance;
1034
+ return Rolls.criticalHit(battle, chance);
968
1035
  }
969
1036
 
970
1037
  fn calcDamage(
@@ -1068,50 +1135,36 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
1068
1135
 
1069
1136
  fn randomizeDamage(battle: anytype) void {
1070
1137
  if (battle.last_damage <= 1) return;
1071
-
1072
- const random = if (showdown)
1073
- battle.rng.range(u8, 217, 256)
1074
- else loop: {
1075
- while (true) {
1076
- const r = std.math.rotr(u8, battle.rng.next(), 1);
1077
- if (r >= 217) break :loop r;
1078
- }
1079
- };
1080
-
1138
+ const random = Rolls.damage(battle);
1081
1139
  battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
1082
1140
  }
1083
1141
 
1084
1142
  fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
1085
1143
  const side = battle.side(player);
1144
+ const foe = battle.foe(player);
1086
1145
 
1087
1146
  if (!try checkHit(battle, player, move, log)) return null;
1088
1147
 
1089
1148
  battle.last_damage = switch (side.last_selected_move) {
1090
- .SuperFang => @max(battle.foe(player).stored().hp / 2, 1),
1149
+ .SuperFang => @max(foe.stored().hp / 2, 1),
1091
1150
  .SeismicToss, .NightShade => side.stored().level,
1092
1151
  .SonicBoom => 20,
1093
1152
  .DragonRage => 40,
1094
1153
  // GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
1095
1154
  .Psywave => power: {
1096
1155
  const max = @intCast(u8, @as(u16, side.stored().level) * 3 / 2);
1097
- if (showdown) {
1098
- break :power battle.rng.range(u8, 0, max);
1099
- } else {
1100
- // GLITCH: Psywave infinite glitch loop
1101
- if (max <= 1) return Result.Error;
1102
- while (true) {
1103
- const r = battle.rng.next();
1104
- if (r < max) break :power r;
1105
- }
1106
- }
1156
+ // GLITCH: Psywave infinite glitch loop
1157
+ if (!showdown and max <= 1) return Result.Error;
1158
+ break :power Rolls.psywave(battle, max);
1107
1159
  },
1108
1160
  else => unreachable,
1109
1161
  };
1110
1162
 
1111
1163
  if (battle.last_damage == 0) return if (showdown) null else Result.Error;
1112
1164
 
1165
+ const sub = showdown and foe.active.volatiles.Substitute;
1113
1166
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
1114
- try buildRage(battle, player.foe(), log);
1167
+ if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
1115
1168
 
1116
1169
  return null;
1117
1170
  }
@@ -1160,7 +1213,9 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
1160
1213
  // Pokémon Showdown calls checkHit before Counter
1161
1214
  if (!showdown and !try checkHit(battle, player, move, log)) return null;
1162
1215
 
1216
+ const sub = showdown and foe.active.volatiles.Substitute;
1163
1217
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
1218
+ if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
1164
1219
  return null;
1165
1220
  }
1166
1221
 
@@ -1204,6 +1259,7 @@ fn mirrorMove(
1204
1259
  battle: anytype,
1205
1260
  player: Player,
1206
1261
  mslot: u4,
1262
+ rewrap: bool,
1207
1263
  auto: bool,
1208
1264
  residual: *bool,
1209
1265
  log: anytype,
@@ -1216,41 +1272,30 @@ fn mirrorMove(
1216
1272
  if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
1217
1273
  try log.fail(battle.active(player), .None);
1218
1274
  return null;
1275
+ } else if (!showdown or foe.last_used_move != .Struggle) {
1276
+ incrementPP(side, mslot);
1219
1277
  }
1220
1278
 
1221
- incrementPP(side, mslot);
1222
-
1223
1279
  if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
1224
- return doMove(battle, player, mslot, auto, residual, log);
1280
+ return doMove(battle, player, mslot, rewrap, auto, residual, log);
1225
1281
  }
1226
1282
 
1227
1283
  fn metronome(
1228
1284
  battle: anytype,
1229
1285
  player: Player,
1230
1286
  mslot: u4,
1287
+ rewrap: bool,
1231
1288
  auto: bool,
1232
1289
  residual: *bool,
1233
1290
  log: anytype,
1234
1291
  ) !?Result {
1235
1292
  var side = battle.side(player);
1236
1293
 
1237
- side.last_selected_move = if (showdown) blk: {
1238
- const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
1239
- const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
1240
- break :blk @intToEnum(Move, r + mod);
1241
- } else loop: {
1242
- while (true) {
1243
- const r = battle.rng.next();
1244
- if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
1245
- if (r >= @enumToInt(Move.Struggle)) continue;
1246
- break :loop @intToEnum(Move, r);
1247
- }
1248
- };
1249
-
1294
+ side.last_selected_move = Rolls.metronome(battle);
1250
1295
  incrementPP(side, mslot);
1251
1296
 
1252
1297
  if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
1253
- return doMove(battle, player, mslot, auto, residual, log);
1298
+ return doMove(battle, player, mslot, rewrap, auto, residual, log);
1254
1299
  }
1255
1300
 
1256
1301
  fn checkHit(battle: anytype, player: Player, move: Move.Data, log: anytype) !bool {
@@ -1304,23 +1349,24 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
1304
1349
  }
1305
1350
 
1306
1351
  // GLITCH: Thrash / Petal Dance / Rage get their accuracy overwritten on subsequent hits
1307
- var state = &side.active.volatiles.state;
1308
- const overwritten = (move.effect == .Thrashing or move.effect == .Rage) and state.* > 0;
1309
- assert(!overwritten or (0 < state.* and state.* <= 255 and !side.active.volatiles.Bide));
1310
- var accuracy = if (overwritten) state.* else @as(u16, Gen12.percent(move.accuracy));
1352
+ const state = side.active.volatiles.state;
1353
+ var overwrite = move.effect == .Rage or move.effect == .Thrashing;
1354
+ const overwritten = overwrite and state > 0;
1355
+ assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
1356
+
1357
+ var accuracy = if (overwritten) state else @as(u16, Gen12.percent(move.accuracy));
1311
1358
  var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
1312
1359
  accuracy = accuracy * boost[0] / boost[1];
1313
1360
  boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
1314
1361
  accuracy = accuracy * boost[0] / boost[1];
1315
1362
  accuracy = @min(255, @max(1, accuracy));
1316
1363
 
1317
- if (move.effect == .Thrashing or move.effect == .Rage) state.* = accuracy;
1364
+ // Pokémon Showdown only overwrites if the volatile is present
1365
+ if (showdown) overwrite = side.active.volatiles.Rage or side.active.volatiles.Thrashing;
1366
+ if (overwrite) side.active.volatiles.state = accuracy;
1318
1367
 
1319
1368
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1320
- break :miss if (showdown)
1321
- !battle.rng.chance(u8, @intCast(u8, accuracy), 256)
1322
- else
1323
- battle.rng.next() >= accuracy;
1369
+ break :miss !Rolls.hit(battle, accuracy);
1324
1370
  };
1325
1371
 
1326
1372
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1435,6 +1481,15 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1435
1481
  }
1436
1482
 
1437
1483
  if (volatiles.LeechSeed) {
1484
+ var foe = battle.foe(player);
1485
+ var foe_stored = foe.stored();
1486
+ const foe_ident = battle.active(player.foe());
1487
+
1488
+ if (foe_stored.hp == 0) {
1489
+ assert(showdown);
1490
+ return;
1491
+ }
1492
+
1438
1493
  var damage = @max(stored.stats.hp / 16, 1);
1439
1494
 
1440
1495
  // GLITCH: Leech Seed + Toxic glitch
@@ -1446,10 +1501,6 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1446
1501
  const amount = @min(damage, stored.hp);
1447
1502
  stored.hp -= amount;
1448
1503
 
1449
- var foe = battle.foe(player);
1450
- var foe_stored = foe.stored();
1451
- const foe_ident = battle.active(player.foe());
1452
-
1453
1504
  // As above, Pokémon Showdown uses damageOf but its not relevant
1454
1505
  if (amount > 0) try log.damage(ident, stored, .LeechSeed);
1455
1506
 
@@ -1538,6 +1589,42 @@ fn checkEBC(battle: anytype) bool {
1538
1589
  return false;
1539
1590
  }
1540
1591
 
1592
+ fn disabled(side: *Side, ident: ID, log: anytype) !bool {
1593
+ if (side.active.volatiles.disabled_move != 0) {
1594
+ // A Pokémon that transforms after being disabled may end up with less move slots
1595
+ const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
1596
+ // side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
1597
+ const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
1598
+ m
1599
+ else
1600
+ side.last_selected_move;
1601
+ if (m != .None and m == last) {
1602
+ side.active.volatiles.Charging = false;
1603
+ try log.disabled(ident, last);
1604
+ return true;
1605
+ }
1606
+ }
1607
+ return false;
1608
+ }
1609
+
1610
+ inline fn buildRage(battle: anytype, who: Player, log: anytype) !void {
1611
+ const side = battle.side(who);
1612
+ if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
1613
+ try Effects.boost(battle, who, Move.get(.Rage), log);
1614
+ }
1615
+ }
1616
+
1617
+ fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
1618
+ var volatiles = &active.volatiles;
1619
+ assert(volatiles.Thrashing);
1620
+ if (volatiles.attacks > 0) return false;
1621
+
1622
+ volatiles.Thrashing = false;
1623
+ volatiles.Confusion = true;
1624
+ volatiles.confusion = Rolls.confusionDuration(battle);
1625
+ return true;
1626
+ }
1627
+
1541
1628
  inline fn onBegin(
1542
1629
  battle: anytype,
1543
1630
  player: Player,
@@ -1628,10 +1715,7 @@ pub const Effects = struct {
1628
1715
  side.active.volatiles.Bide = true;
1629
1716
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1630
1717
  side.active.volatiles.state = 0;
1631
- side.active.volatiles.attacks = @intCast(u3, if (showdown)
1632
- battle.rng.range(u4, 2, 4)
1633
- else
1634
- (battle.rng.next() & 1) + 2);
1718
+ side.active.volatiles.attacks = Rolls.bideThrashDuration(battle);
1635
1719
 
1636
1720
  try log.start(battle.active(player), .Bide);
1637
1721
  }
@@ -1653,14 +1737,8 @@ pub const Effects = struct {
1653
1737
  return;
1654
1738
  }
1655
1739
 
1656
- const chance = !foe.active.types.includes(move.type) and if (showdown)
1657
- battle.rng.chance(u8, @as(u8, if (move.effect == .BurnChance1) 26 else 77), 256)
1658
- else
1659
- battle.rng.next() < 1 + (if (move.effect == .BurnChance1)
1660
- Gen12.percent(10)
1661
- else
1662
- Gen12.percent(30));
1663
- if (!chance) return;
1740
+ if (foe.active.types.includes(move.type)) return;
1741
+ if (!Rolls.secondaryChance(battle, move.effect == .BurnChance1)) return;
1664
1742
 
1665
1743
  foe_stored.status = Status.init(.BRN);
1666
1744
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
@@ -1684,11 +1762,7 @@ pub const Effects = struct {
1684
1762
  const sub = foe.active.volatiles.Substitute;
1685
1763
 
1686
1764
  if (move.effect == .ConfusionChance) {
1687
- const chance = if (showdown)
1688
- battle.rng.chance(u8, 25, 256)
1689
- else
1690
- battle.rng.next() < Gen12.percent(10);
1691
- if (!chance) return;
1765
+ if (!Rolls.confusionChance(battle)) return;
1692
1766
  } else {
1693
1767
  if (showdown) {
1694
1768
  if (!try checkHit(battle, player, move, log)) {
@@ -1707,10 +1781,7 @@ pub const Effects = struct {
1707
1781
 
1708
1782
  if (foe.active.volatiles.Confusion) return;
1709
1783
  foe.active.volatiles.Confusion = true;
1710
- foe.active.volatiles.confusion = @intCast(u3, if (showdown)
1711
- battle.rng.range(u8, 2, 6)
1712
- else
1713
- (battle.rng.next() & 3) + 2);
1784
+ foe.active.volatiles.confusion = Rolls.confusionDuration(battle);
1714
1785
 
1715
1786
  try log.start(battle.active(player.foe()), .Confusion);
1716
1787
  }
@@ -1760,11 +1831,8 @@ pub const Effects = struct {
1760
1831
  return Result.Error;
1761
1832
  }
1762
1833
 
1763
- volatiles.disabled_move = @intCast(u3, randomMoveSlot(&battle.rng, &foe.active.moves, n));
1764
- volatiles.disabled_duration = @intCast(u4, if (showdown)
1765
- battle.rng.range(u8, 1, 9)
1766
- else
1767
- (battle.rng.next() & 7) + 1);
1834
+ volatiles.disabled_move = @intCast(u3, Rolls.moveSlot(battle, &foe.active.moves, n));
1835
+ volatiles.disabled_duration = Rolls.disableDuration(battle);
1768
1836
 
1769
1837
  try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
1770
1838
  return null;
@@ -1776,7 +1844,7 @@ pub const Effects = struct {
1776
1844
 
1777
1845
  if (battle.last_damage == 0) {
1778
1846
  assert(showdown);
1779
- return;
1847
+ if (!battle.foe(player).active.volatiles.Substitute) return;
1780
1848
  }
1781
1849
 
1782
1850
  const drain = @max(battle.last_damage / 2, 1);
@@ -1803,15 +1871,7 @@ pub const Effects = struct {
1803
1871
  var volatiles = &battle.foe(player).active.volatiles;
1804
1872
 
1805
1873
  if (volatiles.Substitute) return;
1806
-
1807
- const chance = if (showdown)
1808
- battle.rng.chance(u8, @as(u8, if (move.effect == .FlinchChance1) 26 else 77), 256)
1809
- else
1810
- battle.rng.next() < 1 + (if (move.effect == .FlinchChance1)
1811
- Gen12.percent(10)
1812
- else
1813
- Gen12.percent(30));
1814
- if (!chance) return;
1874
+ if (!Rolls.secondaryChance(battle, move.effect == .FlinchChance1)) return;
1815
1875
 
1816
1876
  volatiles.Flinch = true;
1817
1877
  volatiles.Recharging = false;
@@ -1837,12 +1897,8 @@ pub const Effects = struct {
1837
1897
  return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
1838
1898
  }
1839
1899
 
1840
- const chance = !foe.active.types.includes(move.type) and if (showdown)
1841
- battle.rng.chance(u8, 26, 256)
1842
- else
1843
- battle.rng.next() < 1 + Gen12.percent(10);
1844
- if (!chance) return;
1845
-
1900
+ if (foe.active.types.includes(move.type)) return;
1901
+ if (!Rolls.secondaryChance(battle, true)) return;
1846
1902
  // Freeze Clause Mod
1847
1903
  if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1848
1904
 
@@ -1979,15 +2035,15 @@ pub const Effects = struct {
1979
2035
  }
1980
2036
  break :has_mimic false;
1981
2037
  };
1982
- // If the foe is Invulnerable we still want to fall through to checkHit to be able to
1983
- // trigger |-miss| instead of |-fail|
1984
- if (!has_mimic and !foe.active.volatiles.Invulnerable) {
2038
+ if (!has_mimic) {
2039
+ // Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
2040
+ if (!try checkHit(battle, player, move, log)) return;
1985
2041
  return try log.fail(battle.active(player.foe()), .None);
1986
2042
  }
1987
2043
  }
1988
2044
  if (!try checkHit(battle, player, move, log)) return;
1989
2045
 
1990
- const rslot = randomMoveSlot(&battle.rng, &foe.active.moves, 0);
2046
+ const rslot = Rolls.moveSlot(battle, &foe.active.moves, 0);
1991
2047
  side.active.move(oslot).id = foe.active.move(rslot).id;
1992
2048
 
1993
2049
  try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
@@ -2008,7 +2064,8 @@ pub const Effects = struct {
2008
2064
  assert(!side.active.volatiles.MultiHit);
2009
2065
  side.active.volatiles.MultiHit = true;
2010
2066
 
2011
- side.active.volatiles.attacks = if (move.effect == .MultiHit) distribution(battle) else 2;
2067
+ side.active.volatiles.attacks =
2068
+ if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
2012
2069
  }
2013
2070
 
2014
2071
  fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2052,14 +2109,8 @@ pub const Effects = struct {
2052
2109
  }
2053
2110
 
2054
2111
  // Body Slam can't paralyze a Normal type Pokémon
2055
- const chance = !foe.active.types.includes(move.type) and if (showdown)
2056
- battle.rng.chance(u8, @as(u8, if (move.effect == .ParalyzeChance1) 26 else 77), 256)
2057
- else
2058
- battle.rng.next() < 1 + (if (move.effect == .ParalyzeChance1)
2059
- Gen12.percent(10)
2060
- else
2061
- Gen12.percent(30));
2062
- if (!chance) return;
2112
+ if (foe.active.types.includes(move.type)) return;
2113
+ if (!Rolls.secondaryChance(battle, move.effect == .ParalyzeChance1)) return;
2063
2114
 
2064
2115
  foe_stored.status = Status.init(.PAR);
2065
2116
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
@@ -2102,16 +2153,7 @@ pub const Effects = struct {
2102
2153
 
2103
2154
  if (move.effect == .Poison) {
2104
2155
  if (!showdown and !try checkHit(battle, player, move, log)) return;
2105
- } else {
2106
- const chance = if (showdown)
2107
- battle.rng.chance(u8, @as(u8, if (move.effect == .PoisonChance1) 52 else 103), 256)
2108
- else
2109
- battle.rng.next() < 1 + (if (move.effect == .PoisonChance1)
2110
- Gen12.percent(20)
2111
- else
2112
- Gen12.percent(40));
2113
- if (!chance) return;
2114
- }
2156
+ } else if (!Rolls.poisonChance(battle, move.effect == .PoisonChance1)) return;
2115
2157
 
2116
2158
  foe_stored.status = Status.init(.PSN);
2117
2159
  if (toxic) {
@@ -2125,10 +2167,8 @@ pub const Effects = struct {
2125
2167
 
2126
2168
  fn rage(battle: anytype, player: Player) !void {
2127
2169
  var volatiles = &battle.side(player).active.volatiles;
2128
- if (volatiles.Rage) return;
2129
- volatiles.Rage = true;
2130
2170
  assert(!volatiles.Bide);
2131
- volatiles.state = 0;
2171
+ volatiles.Rage = true;
2132
2172
  }
2133
2173
 
2134
2174
  fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
@@ -2180,16 +2220,7 @@ pub const Effects = struct {
2180
2220
  }
2181
2221
  foe.active.volatiles.Recharging = false;
2182
2222
 
2183
- const duration = @intCast(u3, if (showdown)
2184
- battle.rng.range(u8, 1, 8)
2185
- else loop: {
2186
- while (true) {
2187
- const r = battle.rng.next() & 7;
2188
- if (r != 0) break :loop r;
2189
- }
2190
- });
2191
-
2192
- foe_stored.status = Status.slp(duration);
2223
+ foe_stored.status = Status.slp(Rolls.sleepDuration(battle));
2193
2224
  try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
2194
2225
  }
2195
2226
 
@@ -2231,8 +2262,9 @@ pub const Effects = struct {
2231
2262
  fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
2232
2263
  if (!showdown or battle.side(player).last_selected_move == .Teleport) return;
2233
2264
 
2234
- // Whirlwind/Roar should not roll to hit but Pokémon Showdown does anyway
2265
+ // Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
2235
2266
  _ = try checkHit(battle, player, move, log);
2267
+ battle.last_damage = 0;
2236
2268
  }
2237
2269
 
2238
2270
  fn thrashing(battle: anytype, player: Player) void {
@@ -2241,11 +2273,7 @@ pub const Effects = struct {
2241
2273
  assert(!volatiles.Bide);
2242
2274
 
2243
2275
  volatiles.Thrashing = true;
2244
- volatiles.state = 0;
2245
- volatiles.attacks = @intCast(u3, if (showdown)
2246
- battle.rng.range(u8, 2, 4)
2247
- else
2248
- (battle.rng.next() & 1) + 2);
2276
+ volatiles.attacks = Rolls.bideThrashDuration(battle);
2249
2277
  }
2250
2278
 
2251
2279
  fn transform(battle: anytype, player: Player, log: anytype) !void {
@@ -2277,7 +2305,7 @@ pub const Effects = struct {
2277
2305
  try log.transform(battle.active(player), foe_ident);
2278
2306
  }
2279
2307
 
2280
- fn binding(battle: anytype, player: Player) void {
2308
+ fn binding(battle: anytype, player: Player, rewrap: bool) void {
2281
2309
  var side = battle.side(player);
2282
2310
  var foe = battle.foe(player);
2283
2311
 
@@ -2285,9 +2313,11 @@ pub const Effects = struct {
2285
2313
  side.active.volatiles.Binding = true;
2286
2314
  // GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
2287
2315
  // (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
2288
- if (!showdown) foe.active.volatiles.Recharging = false;
2316
+ if (!showdown) {
2317
+ foe.active.volatiles.Recharging = false;
2318
+ } else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
2289
2319
 
2290
- side.active.volatiles.attacks = distribution(battle) - 1;
2320
+ side.active.volatiles.attacks = Rolls.distribution(battle) - 1;
2291
2321
  }
2292
2322
 
2293
2323
  fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2314,9 +2344,11 @@ pub const Effects = struct {
2314
2344
  return try log.fail(ident, .None);
2315
2345
  }
2316
2346
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2317
- const stat = unmodifiedStats(battle, player).atk;
2347
+ const stat = unmodifiedStats(battle, side).atk;
2318
2348
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2319
2349
  try log.boost(ident, reason, n);
2350
+ // Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
2351
+ if (showdown and move.effect == .Rage) return;
2320
2352
  },
2321
2353
  .DefenseUp1, .DefenseUp2 => {
2322
2354
  assert(boosts.def >= -6 and boosts.def <= 6);
@@ -2332,7 +2364,7 @@ pub const Effects = struct {
2332
2364
  return try log.fail(ident, .None);
2333
2365
  }
2334
2366
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2335
- const stat = unmodifiedStats(battle, player).def;
2367
+ const stat = unmodifiedStats(battle, side).def;
2336
2368
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2337
2369
  try log.boost(ident, .Defense, n);
2338
2370
  },
@@ -2349,7 +2381,7 @@ pub const Effects = struct {
2349
2381
  return try log.fail(ident, .None);
2350
2382
  }
2351
2383
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2352
- const stat = unmodifiedStats(battle, player).spe;
2384
+ const stat = unmodifiedStats(battle, side).spe;
2353
2385
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2354
2386
  try log.boost(ident, .Speed, 2);
2355
2387
  },
@@ -2369,7 +2401,7 @@ pub const Effects = struct {
2369
2401
  return try log.fail(ident, .None);
2370
2402
  }
2371
2403
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2372
- const stat = unmodifiedStats(battle, player).spc;
2404
+ const stat = unmodifiedStats(battle, side).spc;
2373
2405
  stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2374
2406
  try log.boost(ident, .SpecialAttack, n);
2375
2407
  try log.boost(ident, .SpecialDefense, n);
@@ -2396,11 +2428,7 @@ pub const Effects = struct {
2396
2428
  }
2397
2429
 
2398
2430
  if (move.effect.isStatDownChance()) {
2399
- const chance = if (showdown)
2400
- battle.rng.chance(u8, 85, 256)
2401
- else
2402
- battle.rng.next() < Gen12.percent(33) + 1;
2403
- if (!chance or foe.active.volatiles.Invulnerable) return;
2431
+ if (!Rolls.unboost(battle) or foe.active.volatiles.Invulnerable) return;
2404
2432
  } else if (!showdown and !try checkHit(battle, player, move, log)) {
2405
2433
  return; // checkHit already checks for Invulnerable
2406
2434
  }
@@ -2422,7 +2450,7 @@ pub const Effects = struct {
2422
2450
  return try log.fail(foe_ident, .None);
2423
2451
  }
2424
2452
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2425
- const stat = unmodifiedStats(battle, player.foe()).atk;
2453
+ const stat = unmodifiedStats(battle, foe).atk;
2426
2454
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2427
2455
  try log.boost(foe_ident, .Attack, -1);
2428
2456
  },
@@ -2440,7 +2468,7 @@ pub const Effects = struct {
2440
2468
  return try log.fail(foe_ident, .None);
2441
2469
  }
2442
2470
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2443
- const stat = unmodifiedStats(battle, player.foe()).def;
2471
+ const stat = unmodifiedStats(battle, foe).def;
2444
2472
  stats.def = @max(1, stat * mod[0] / mod[1]);
2445
2473
  try log.boost(foe_ident, .Defense, -@as(i8, n));
2446
2474
  },
@@ -2457,7 +2485,7 @@ pub const Effects = struct {
2457
2485
  return try log.fail(foe_ident, .None);
2458
2486
  }
2459
2487
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2460
- const stat = unmodifiedStats(battle, player.foe()).spe;
2488
+ const stat = unmodifiedStats(battle, foe).spe;
2461
2489
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2462
2490
  try log.boost(foe_ident, .Speed, -1);
2463
2491
  assert(boosts.spe >= -6);
@@ -2477,7 +2505,7 @@ pub const Effects = struct {
2477
2505
  return try log.fail(foe_ident, .None);
2478
2506
  }
2479
2507
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2480
- const stat = unmodifiedStats(battle, player.foe()).spc;
2508
+ const stat = unmodifiedStats(battle, foe).spc;
2481
2509
  stats.spc = @max(1, stat * mod[0] / mod[1]);
2482
2510
  try log.boost(foe_ident, .SpecialAttack, -1);
2483
2511
  try log.boost(foe_ident, .SpecialDefense, -1);
@@ -2496,8 +2524,7 @@ pub const Effects = struct {
2496
2524
  }
2497
2525
  };
2498
2526
 
2499
- fn unmodifiedStats(battle: anytype, who: Player) *Stats(u16) {
2500
- const side = battle.side(who);
2527
+ fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
2501
2528
  if (!side.active.volatiles.Transform) return &side.stored().stats;
2502
2529
  const id = ID.from(side.active.volatiles.transform);
2503
2530
  return &battle.side(id.player).pokemon[id.id - 1].stats;
@@ -2563,75 +2590,140 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
2563
2590
  }
2564
2591
  }
2565
2592
 
2566
- fn disabled(side: *Side, ident: ID, log: anytype) !bool {
2567
- if (side.active.volatiles.disabled_move != 0) {
2568
- // A Pokémon that transforms after being disabled may end up with less move slots
2569
- const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
2570
- if (m != .None and m == side.last_selected_move) {
2571
- side.active.volatiles.Charging = false;
2572
- try log.disabled(ident, side.last_selected_move);
2573
- return true;
2593
+ pub const Rolls = struct {
2594
+ inline fn speedTie(battle: anytype) bool {
2595
+ if (showdown) return battle.rng.range(u8, 0, 2) == 0;
2596
+ return battle.rng.next() < Gen12.percent(50) + 1;
2597
+ }
2598
+
2599
+ inline fn criticalHit(battle: anytype, chance: u16) bool {
2600
+ if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
2601
+ return std.math.rotl(u8, battle.rng.next(), 3) < chance;
2602
+ }
2603
+
2604
+ inline fn damage(battle: anytype) u8 {
2605
+ if (showdown) return battle.rng.range(u8, 217, 256);
2606
+ while (true) {
2607
+ const r = std.math.rotr(u8, battle.rng.next(), 1);
2608
+ if (r >= 217) return r;
2574
2609
  }
2575
2610
  }
2576
- return false;
2577
- }
2578
2611
 
2579
- inline fn buildRage(battle: anytype, who: Player, log: anytype) !void {
2580
- const side = battle.side(who);
2581
- if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
2582
- try Effects.boost(battle, who, Move.get(.Rage), log);
2612
+ inline fn hit(battle: anytype, accuracy: u16) bool {
2613
+ if (showdown) return battle.rng.chance(u8, @intCast(u8, accuracy), 256);
2614
+ return battle.rng.next() < accuracy;
2583
2615
  }
2584
- }
2585
2616
 
2586
- fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
2587
- var volatiles = &active.volatiles;
2588
- assert(volatiles.Thrashing);
2589
- if (volatiles.attacks > 0) return false;
2617
+ inline fn confusionDuration(battle: anytype) u3 {
2618
+ if (showdown) return @intCast(u3, battle.rng.range(u8, 2, 6));
2619
+ return @intCast(u3, (battle.rng.next() & 3) + 2);
2620
+ }
2590
2621
 
2591
- volatiles.Thrashing = false;
2592
- volatiles.Confusion = true;
2593
- volatiles.confusion = @intCast(u3, if (showdown)
2594
- battle.rng.range(u8, 2, 6)
2595
- else
2596
- (battle.rng.next() & 3) + 2);
2622
+ inline fn confused(battle: anytype) bool {
2623
+ if (showdown) return !battle.rng.chance(u8, 128, 256);
2624
+ return battle.rng.next() >= Gen12.percent(50) + 1;
2625
+ }
2597
2626
 
2598
- return true;
2599
- }
2627
+ inline fn paralyzed(battle: anytype) bool {
2628
+ if (showdown) return battle.rng.chance(u8, 63, 256);
2629
+ return battle.rng.next() < Gen12.percent(25);
2630
+ }
2600
2631
 
2601
- const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
2632
+ inline fn confusionChance(battle: anytype) bool {
2633
+ if (showdown) return battle.rng.chance(u8, 25, 256);
2634
+ return battle.rng.next() < Gen12.percent(10);
2635
+ }
2602
2636
 
2603
- fn distribution(battle: anytype) u3 {
2604
- if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
2605
- const r = (battle.rng.next() & 3);
2606
- return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
2607
- }
2637
+ inline fn secondaryChance(battle: anytype, low: bool) bool {
2638
+ if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 26 else 77), 256);
2639
+ return battle.rng.next() < 1 + (if (low) Gen12.percent(10) else Gen12.percent(30));
2640
+ }
2608
2641
 
2609
- fn randomMoveSlot(rand: anytype, moves: []MoveSlot, check_pp: u4) u4 {
2610
- if (showdown) {
2611
- if (check_pp == 0) {
2612
- var i: usize = moves.len;
2613
- while (i > 0) {
2614
- i -= 1;
2615
- if (moves[i].id != .None) return rand.range(u4, 0, @intCast(u4, i + 1)) + 1;
2616
- }
2617
- } else {
2618
- var r = rand.range(u4, 0, @intCast(u4, check_pp)) + 1;
2619
- var i: usize = 0;
2620
- while (i < moves.len and r > 0) : (i += 1) {
2621
- if (moves[i].pp > 0) {
2622
- r -= 1;
2623
- if (r == 0) break;
2642
+ inline fn poisonChance(battle: anytype, low: bool) bool {
2643
+ if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 52 else 103), 256);
2644
+ return battle.rng.next() < 1 + (if (low) Gen12.percent(20) else Gen12.percent(40));
2645
+ }
2646
+
2647
+ inline fn sleepDuration(battle: anytype) u3 {
2648
+ if (showdown) return @intCast(u3, battle.rng.range(u8, 1, 8));
2649
+ while (true) {
2650
+ const r = battle.rng.next() & 7;
2651
+ if (r != 0) return @intCast(u3, r);
2652
+ }
2653
+ }
2654
+
2655
+ inline fn bideThrashDuration(battle: anytype) u3 {
2656
+ if (showdown) return @intCast(u3, battle.rng.range(u4, 2, 4));
2657
+ return @intCast(u3, (battle.rng.next() & 1) + 2);
2658
+ }
2659
+
2660
+ fn moveSlot(battle: anytype, moves: []MoveSlot, check_pp: u4) u4 {
2661
+ if (showdown) {
2662
+ if (check_pp == 0) {
2663
+ var i: usize = moves.len;
2664
+ while (i > 0) {
2665
+ i -= 1;
2666
+ if (moves[i].id != .None) {
2667
+ return battle.rng.range(u4, 0, @intCast(u4, i + 1)) + 1;
2668
+ }
2669
+ }
2670
+ } else {
2671
+ var r = battle.rng.range(u4, 0, @intCast(u4, check_pp)) + 1;
2672
+ var i: usize = 0;
2673
+ while (i < moves.len and r > 0) : (i += 1) {
2674
+ if (moves[i].pp > 0) {
2675
+ r -= 1;
2676
+ if (r == 0) break;
2677
+ }
2624
2678
  }
2679
+ return @intCast(u4, i + 1);
2625
2680
  }
2626
- return @intCast(u4, i + 1);
2681
+ }
2682
+
2683
+ while (true) {
2684
+ const r = @intCast(u4, battle.rng.next() & 3);
2685
+ if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
2627
2686
  }
2628
2687
  }
2629
2688
 
2630
- while (true) {
2631
- const r = @intCast(u4, rand.next() & 3);
2632
- if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
2689
+ inline fn disableDuration(battle: anytype) u4 {
2690
+ if (showdown) return @intCast(u4, battle.rng.range(u8, 1, 9));
2691
+ return @intCast(u4, (battle.rng.next() & 7) + 1);
2633
2692
  }
2634
- }
2693
+
2694
+ inline fn metronome(battle: anytype) Move {
2695
+ if (showdown) {
2696
+ const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
2697
+ const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
2698
+ return @intToEnum(Move, r + mod);
2699
+ }
2700
+ while (true) {
2701
+ const r = battle.rng.next();
2702
+ if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
2703
+ if (r >= @enumToInt(Move.Struggle)) continue;
2704
+ return @intToEnum(Move, r);
2705
+ }
2706
+ }
2707
+
2708
+ inline fn psywave(battle: anytype, max: u8) u8 {
2709
+ if (showdown) return battle.rng.range(u8, 0, max);
2710
+ while (true) {
2711
+ const r = battle.rng.next();
2712
+ if (r < max) return r;
2713
+ }
2714
+ }
2715
+
2716
+ inline fn distribution(battle: anytype) u3 {
2717
+ if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
2718
+ const r = (battle.rng.next() & 3);
2719
+ return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
2720
+ }
2721
+
2722
+ inline fn unboost(battle: anytype) bool {
2723
+ if (showdown) return battle.rng.chance(u8, 85, 256);
2724
+ return battle.rng.next() < Gen12.percent(33) + 1;
2725
+ }
2726
+ };
2635
2727
 
2636
2728
  test "RNG agreement" {
2637
2729
  if (!showdown) return;
@@ -2731,9 +2823,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
2731
2823
  while (slot <= 4) : (slot += 1) {
2732
2824
  const m = active.moves[slot - 1];
2733
2825
  if (m.id == .None) break;
2734
- if (m.id == side.last_selected_move) {
2735
- // Pokémon Showdown displays Struggle if limited to Bide when out of PP
2736
- const s = if (m.pp == 0 and m.id == .Bide) 0 else slot;
2826
+ if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
2827
+ // Pokémon Showdown displays Struggle if limited to Bide but unable to pick
2828
+ const struggle =
2829
+ m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);
2830
+ const s = if (struggle) 0 else slot;
2737
2831
  out[n] = .{ .type = .Move, .data = s };
2738
2832
  n += 1;
2739
2833
  return n;