@pkmn/engine 0.1.0-dev.729b1e87 → 0.1.0-dev.7b099504

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,
@@ -442,14 +483,23 @@ fn executeMove(
442
483
  .err => return @as(?Result, Result.Error),
443
484
  }
444
485
 
445
- if (!skip_can and !try canMove(battle, player, mslot, auto, skip_pp, from, log)) return null;
486
+ if (!skip_can and !try canMove(battle, player, mslot, auto, skip_pp, from, residual, log)) {
487
+ return null;
488
+ }
446
489
 
447
- return doMove(battle, player, mslot, auto, residual, log);
490
+ return doMove(battle, player, mslot, rewrap, auto, residual, log);
448
491
  }
449
492
 
450
- const BeforeMove = union(enum) { done, skip_can, skip_pp, ok, err };
493
+ const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
451
494
 
452
- 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 {
453
503
  var side = battle.side(player);
454
504
  const foe = battle.foe(player);
455
505
  var active = &side.active;
@@ -477,13 +527,15 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
477
527
  return .done;
478
528
  }
479
529
 
480
- if (foe.active.volatiles.Binding) {
530
+ if (skip or foe.active.volatiles.Binding) {
481
531
  try log.cant(ident, .Bound);
482
532
  return .done;
483
533
  }
484
534
 
485
- if (!showdown and volatiles.Flinch) {
486
- 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;
487
539
  try log.cant(ident, .Flinch);
488
540
  return .done;
489
541
  }
@@ -504,13 +556,6 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
504
556
  // Pokémon Showdown's disable condition has a single onBeforeMove handler
505
557
  if (showdown and try disabled(side, ident, log)) return .done;
506
558
 
507
- // Pokémon Showdown checks for Flinch *after* instead of before
508
- if (showdown and volatiles.Flinch) {
509
- volatiles.Flinch = false;
510
- try log.cant(ident, .Flinch);
511
- return .done;
512
- }
513
-
514
559
  // This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
515
560
  // before the side had a selected a move - we simply need to assume this leads to a desync
516
561
  if (side.last_selected_move == .None) {
@@ -528,14 +573,9 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
528
573
  } else {
529
574
  try log.activate(ident, .Confusion);
530
575
 
531
- const confused = if (showdown)
532
- !battle.rng.chance(u8, 128, 256)
533
- else
534
- battle.rng.next() >= Gen12.percent(50) + 1;
535
-
536
- if (confused) {
576
+ if (Rolls.confused(battle)) {
537
577
  assert(!volatiles.MultiHit);
538
- volatiles.state = 0;
578
+ if (!volatiles.Rage) volatiles.state = 0;
539
579
  volatiles.Bide = false;
540
580
  volatiles.Thrashing = false;
541
581
  volatiles.MultiHit = false;
@@ -559,8 +599,12 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
559
599
  player
560
600
  else
561
601
  player.foe();
602
+
603
+ const uncapped = battle.last_damage;
562
604
  // Skipping adjustDamage / randomizeDamage / checkHit
563
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;
564
608
 
565
609
  return .done;
566
610
  }
@@ -569,22 +613,15 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
569
613
 
570
614
  if (!showdown and try disabled(side, ident, log)) return .done;
571
615
 
572
- if (Status.is(stored.status, .PAR)) {
573
- const paralyzed = if (showdown)
574
- battle.rng.chance(u8, 63, 256)
575
- else
576
- battle.rng.next() < Gen12.percent(25);
577
-
578
- if (paralyzed) {
579
- volatiles.state = 0;
580
- volatiles.Bide = false;
581
- volatiles.Thrashing = false;
582
- volatiles.Charging = false;
583
- volatiles.Binding = false;
584
- // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
585
- try log.cant(ident, .Paralysis);
586
- return .done;
587
- }
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;
588
625
  }
589
626
 
590
627
  if (volatiles.Bide) {
@@ -611,7 +648,18 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
611
648
  return .done;
612
649
  }
613
650
 
651
+ const sub = showdown and foe.active.volatiles.Substitute;
614
652
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, 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
+ }
662
+
615
663
  return .done;
616
664
  }
617
665
 
@@ -623,8 +671,13 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
623
671
  try log.start(battle.active(player), .ConfusionSilent);
624
672
  }
625
673
  try log.move(ident, side.last_selected_move, battle.active(player.foe()), from);
626
- if (showdown and handleThrashing(battle, active)) {
627
- 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
+ }
628
681
  }
629
682
  return .skip_can;
630
683
  }
@@ -639,7 +692,11 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
639
692
  side.last_selected_move,
640
693
  );
641
694
  if (showdown or battle.last_damage != 0) {
695
+ const sub = showdown and foe.active.volatiles.Substitute;
642
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
+ }
643
700
  }
644
701
  return .done;
645
702
  }
@@ -654,6 +711,7 @@ fn canMove(
654
711
  auto: bool,
655
712
  skip_pp: bool,
656
713
  from: ?Move,
714
+ residual: *bool,
657
715
  log: anytype,
658
716
  ) !bool {
659
717
  var side = battle.side(player);
@@ -688,7 +746,7 @@ fn canMove(
688
746
  try log.move(player_ident, side.last_selected_move, battle.active(target), f);
689
747
 
690
748
  if (move.effect.onBegin()) {
691
- try onBegin(battle, player, move, mslot, log);
749
+ try onBegin(battle, player, move, mslot, residual, log);
692
750
  return false;
693
751
  }
694
752
 
@@ -696,7 +754,7 @@ fn canMove(
696
754
  Effects.thrashing(battle, player);
697
755
  } else if (!showdown and move.effect == .Binding) {
698
756
  // Pokémon Showdown handles this after hit/miss checks and damage calculation
699
- Effects.binding(battle, player);
757
+ Effects.binding(battle, player, false);
700
758
  }
701
759
 
702
760
  return true;
@@ -737,6 +795,7 @@ fn doMove(
737
795
  battle: anytype,
738
796
  player: Player,
739
797
  mslot: u4,
798
+ rewrap: bool,
740
799
  auto: bool,
741
800
  residual: *bool,
742
801
  log: anytype,
@@ -767,16 +826,21 @@ fn doMove(
767
826
  (@enumToInt(move.type.effectiveness(foe.active.types.type1)) == 0 or
768
827
  @enumToInt(move.type.effectiveness(foe.active.types.type2)) == 0);
769
828
  if (immune and move.effect != .Binding) break :miss true;
770
- 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
+ }
771
833
  break :miss move.target != .Self and !moveHit(battle, player, move, &immune, &mist);
772
834
  };
773
835
  assert(!immune or miss or (showdown and move.effect == .Binding));
774
836
 
837
+ var late = showdown and move.effect != .Explode;
775
838
  const skip = status or immune;
776
839
  if ((!showdown or (!skip or counter)) and !miss) blk: {
777
840
  if (showdown and move.effect.isMulti()) {
778
841
  Effects.multiHit(battle, player, move);
779
842
  hits = side.active.volatiles.attacks;
843
+ late = false;
780
844
  }
781
845
 
782
846
  // Cartridge rolls for crit even for moves that can't crit (Counter/Metronome/status/OHKO)
@@ -810,7 +874,7 @@ fn doMove(
810
874
  }
811
875
 
812
876
  // Due to control flow shenanigans we need to clear last_damage for Pokémon Showdown
813
- if (showdown and skip and move.effect != .Bide) battle.last_damage = 0;
877
+ if (showdown and skip) battle.last_damage = 0;
814
878
 
815
879
  miss = if (showdown or skip)
816
880
  miss
@@ -823,9 +887,9 @@ fn doMove(
823
887
 
824
888
  if (!showdown or !miss) {
825
889
  if (move.effect == .MirrorMove) {
826
- return mirrorMove(battle, player, mslot, auto, residual, log);
890
+ return mirrorMove(battle, player, mslot, rewrap, auto, residual, log);
827
891
  } else if (move.effect == .Metronome) {
828
- return metronome(battle, player, mslot, auto, residual, log);
892
+ return metronome(battle, player, mslot, rewrap, auto, residual, log);
829
893
  } else if (move.effect.onEnd()) {
830
894
  try onEnd(battle, player, move, log);
831
895
  return null;
@@ -840,7 +904,7 @@ fn doMove(
840
904
  move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
841
905
  if (ohko) {
842
906
  try log.immune(foe_ident, .OHKO);
843
- } else if (immune and !invulnerable) {
907
+ } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
844
908
  try log.immune(foe_ident, .None);
845
909
  } else if (mist) {
846
910
  if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
@@ -857,15 +921,9 @@ fn doMove(
857
921
  if (showdown and side.stored().hp == 0) residual.* = false;
858
922
  } else if (move.effect == .Explode) {
859
923
  try Effects.explode(battle, player);
860
- if (foe.active.volatiles.Rage and foe.active.boosts.atk < 6) {
861
- try Effects.boost(battle, player.foe(), Move.get(.Rage), log);
862
- }
863
- } else if (showdown) {
864
- if (move.effect == .Disable) {
865
- if (foe.active.volatiles.Rage and foe.active.boosts.atk < 6) {
866
- try Effects.boost(battle, player.foe(), Move.get(.Rage), log);
867
- }
868
- }
924
+ try buildRage(battle, player.foe(), log);
925
+ } else if (showdown and move.effect == .Disable) {
926
+ try buildRage(battle, player.foe(), log);
869
927
  }
870
928
  return null;
871
929
  }
@@ -878,6 +936,9 @@ fn doMove(
878
936
  hits = side.active.volatiles.attacks;
879
937
  }
880
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
+
881
942
  var nullified = false;
882
943
  var hit: u4 = 0;
883
944
  while (hit < hits) {
@@ -893,9 +954,7 @@ fn doMove(
893
954
  if (hit == 0 and ohko) try log.ohko();
894
955
  hit += 1;
895
956
  if (foe.stored().hp == 0) break;
896
- if (foe.active.volatiles.Rage and foe.active.boosts.atk < 6) {
897
- try Effects.boost(battle, player.foe(), Move.get(.Rage), log);
898
- }
957
+ if (!late and (!sub or move.effect == .Explode)) try buildRage(battle, player.foe(), log);
899
958
  // If the substitute breaks during a multi-hit attack, the attack ends
900
959
  if (nullified) break;
901
960
  }
@@ -911,11 +970,17 @@ fn doMove(
911
970
  } else if (showdown) {
912
971
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
913
972
  if (move.effect == .Binding) {
914
- Effects.binding(battle, player);
973
+ Effects.binding(battle, player, rewrap);
915
974
  if (immune) {
916
975
  battle.last_damage = 0;
976
+ assert(foe.stored().hp > 0);
917
977
  // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
918
- 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
+ }
919
984
  return null;
920
985
  }
921
986
  }
@@ -932,6 +997,9 @@ fn doMove(
932
997
 
933
998
  if (foe.stored().hp == 0) return null;
934
999
 
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);
1002
+
935
1003
  if (!move.effect.isSpecial()) {
936
1004
  // On the cartridge Rage is not considered to be "special" and thus gets executed for a
937
1005
  // second time here (after being executed in the "always happens" block above) but that
@@ -943,7 +1011,9 @@ fn doMove(
943
1011
  if (move.effect == .Twineedle) {
944
1012
  if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), log);
945
1013
  } else if (move.effect == .Disable) {
946
- 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;
947
1017
  } else {
948
1018
  try moveEffect(battle, player, move, log);
949
1019
  }
@@ -961,8 +1031,7 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
961
1031
  chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
962
1032
  chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
963
1033
 
964
- if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
965
- return std.math.rotl(u8, battle.rng.next(), 3) < chance;
1034
+ return Rolls.criticalHit(battle, chance);
966
1035
  }
967
1036
 
968
1037
  fn calcDamage(
@@ -1066,49 +1135,37 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
1066
1135
 
1067
1136
  fn randomizeDamage(battle: anytype) void {
1068
1137
  if (battle.last_damage <= 1) return;
1069
-
1070
- const random = if (showdown)
1071
- battle.rng.range(u8, 217, 256)
1072
- else loop: {
1073
- while (true) {
1074
- const r = std.math.rotr(u8, battle.rng.next(), 1);
1075
- if (r >= 217) break :loop r;
1076
- }
1077
- };
1078
-
1138
+ const random = Rolls.damage(battle);
1079
1139
  battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
1080
1140
  }
1081
1141
 
1082
1142
  fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
1083
1143
  const side = battle.side(player);
1144
+ const foe = battle.foe(player);
1084
1145
 
1085
1146
  if (!try checkHit(battle, player, move, log)) return null;
1086
1147
 
1087
1148
  battle.last_damage = switch (side.last_selected_move) {
1088
- .SuperFang => @max(battle.foe(player).stored().hp / 2, 1),
1149
+ .SuperFang => @max(foe.stored().hp / 2, 1),
1089
1150
  .SeismicToss, .NightShade => side.stored().level,
1090
1151
  .SonicBoom => 20,
1091
1152
  .DragonRage => 40,
1092
1153
  // GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
1093
1154
  .Psywave => power: {
1094
1155
  const max = @intCast(u8, @as(u16, side.stored().level) * 3 / 2);
1095
- if (showdown) {
1096
- break :power battle.rng.range(u8, 0, max);
1097
- } else {
1098
- // GLITCH: Psywave infinite glitch loop
1099
- if (max <= 1) return Result.Error;
1100
- while (true) {
1101
- const r = battle.rng.next();
1102
- if (r < max) break :power r;
1103
- }
1104
- }
1156
+ // GLITCH: Psywave infinite glitch loop
1157
+ if (!showdown and max <= 1) return Result.Error;
1158
+ break :power Rolls.psywave(battle, max);
1105
1159
  },
1106
1160
  else => unreachable,
1107
1161
  };
1108
1162
 
1109
1163
  if (battle.last_damage == 0) return if (showdown) null else Result.Error;
1110
1164
 
1165
+ const sub = showdown and foe.active.volatiles.Substitute;
1111
1166
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
1167
+ if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
1168
+
1112
1169
  return null;
1113
1170
  }
1114
1171
 
@@ -1156,7 +1213,9 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
1156
1213
  // Pokémon Showdown calls checkHit before Counter
1157
1214
  if (!showdown and !try checkHit(battle, player, move, log)) return null;
1158
1215
 
1216
+ const sub = showdown and foe.active.volatiles.Substitute;
1159
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);
1160
1219
  return null;
1161
1220
  }
1162
1221
 
@@ -1200,6 +1259,7 @@ fn mirrorMove(
1200
1259
  battle: anytype,
1201
1260
  player: Player,
1202
1261
  mslot: u4,
1262
+ rewrap: bool,
1203
1263
  auto: bool,
1204
1264
  residual: *bool,
1205
1265
  log: anytype,
@@ -1212,41 +1272,30 @@ fn mirrorMove(
1212
1272
  if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
1213
1273
  try log.fail(battle.active(player), .None);
1214
1274
  return null;
1275
+ } else if (!showdown or foe.last_used_move != .Struggle) {
1276
+ incrementPP(side, mslot);
1215
1277
  }
1216
1278
 
1217
- incrementPP(side, mslot);
1218
-
1219
- if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, log)) return null;
1220
- return doMove(battle, player, mslot, auto, residual, log);
1279
+ if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
1280
+ return doMove(battle, player, mslot, rewrap, auto, residual, log);
1221
1281
  }
1222
1282
 
1223
1283
  fn metronome(
1224
1284
  battle: anytype,
1225
1285
  player: Player,
1226
1286
  mslot: u4,
1287
+ rewrap: bool,
1227
1288
  auto: bool,
1228
1289
  residual: *bool,
1229
1290
  log: anytype,
1230
1291
  ) !?Result {
1231
1292
  var side = battle.side(player);
1232
1293
 
1233
- side.last_selected_move = if (showdown) blk: {
1234
- const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
1235
- const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
1236
- break :blk @intToEnum(Move, r + mod);
1237
- } else loop: {
1238
- while (true) {
1239
- const r = battle.rng.next();
1240
- if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
1241
- if (r >= @enumToInt(Move.Struggle)) continue;
1242
- break :loop @intToEnum(Move, r);
1243
- }
1244
- };
1245
-
1294
+ side.last_selected_move = Rolls.metronome(battle);
1246
1295
  incrementPP(side, mslot);
1247
1296
 
1248
- if (!try canMove(battle, player, mslot, auto, false, .Metronome, log)) return null;
1249
- return doMove(battle, player, mslot, auto, residual, log);
1297
+ if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
1298
+ return doMove(battle, player, mslot, rewrap, auto, residual, log);
1250
1299
  }
1251
1300
 
1252
1301
  fn checkHit(battle: anytype, player: Player, move: Move.Data, log: anytype) !bool {
@@ -1300,23 +1349,24 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
1300
1349
  }
1301
1350
 
1302
1351
  // GLITCH: Thrash / Petal Dance / Rage get their accuracy overwritten on subsequent hits
1303
- var state = &side.active.volatiles.state;
1304
- const overwritten = (move.effect == .Thrashing or move.effect == .Rage) and state.* > 0;
1305
- assert(!overwritten or (0 < state.* and state.* <= 255 and !side.active.volatiles.Bide));
1306
- 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));
1307
1358
  var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
1308
1359
  accuracy = accuracy * boost[0] / boost[1];
1309
1360
  boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
1310
1361
  accuracy = accuracy * boost[0] / boost[1];
1311
1362
  accuracy = @min(255, @max(1, accuracy));
1312
1363
 
1313
- 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;
1314
1367
 
1315
1368
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1316
- break :miss if (showdown)
1317
- !battle.rng.chance(u8, @intCast(u8, accuracy), 256)
1318
- else
1319
- battle.rng.next() >= accuracy;
1369
+ break :miss !Rolls.hit(battle, accuracy);
1320
1370
  };
1321
1371
 
1322
1372
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1431,6 +1481,15 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1431
1481
  }
1432
1482
 
1433
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
+
1434
1493
  var damage = @max(stored.stats.hp / 16, 1);
1435
1494
 
1436
1495
  // GLITCH: Leech Seed + Toxic glitch
@@ -1442,10 +1501,6 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1442
1501
  const amount = @min(damage, stored.hp);
1443
1502
  stored.hp -= amount;
1444
1503
 
1445
- var foe = battle.foe(player);
1446
- var foe_stored = foe.stored();
1447
- const foe_ident = battle.active(player.foe());
1448
-
1449
1504
  // As above, Pokémon Showdown uses damageOf but its not relevant
1450
1505
  if (amount > 0) try log.damage(ident, stored, .LeechSeed);
1451
1506
 
@@ -1534,7 +1589,50 @@ fn checkEBC(battle: anytype) bool {
1534
1589
  return false;
1535
1590
  }
1536
1591
 
1537
- inline fn onBegin(battle: anytype, player: Player, move: Move.Data, mslot: u8, log: anytype) !void {
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
+
1628
+ inline fn onBegin(
1629
+ battle: anytype,
1630
+ player: Player,
1631
+ move: Move.Data,
1632
+ mslot: u8,
1633
+ residual: *bool,
1634
+ log: anytype,
1635
+ ) !void {
1538
1636
  assert(move.effect.onBegin());
1539
1637
  return switch (move.effect) {
1540
1638
  .Confusion => Effects.confusion(battle, player, move, log),
@@ -1550,7 +1648,7 @@ inline fn onBegin(battle: anytype, player: Player, move: Move.Data, mslot: u8, l
1550
1648
  .Poison => Effects.poison(battle, player, move, log),
1551
1649
  .Reflect => Effects.reflect(battle, player, log),
1552
1650
  .Splash => Effects.splash(battle, player, log),
1553
- .Substitute => Effects.substitute(battle, player, log),
1651
+ .Substitute => Effects.substitute(battle, player, residual, log),
1554
1652
  .SwitchAndTeleport => Effects.switchAndTeleport(battle, player, move, log),
1555
1653
  .Transform => Effects.transform(battle, player, log),
1556
1654
  else => unreachable,
@@ -1617,10 +1715,7 @@ pub const Effects = struct {
1617
1715
  side.active.volatiles.Bide = true;
1618
1716
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1619
1717
  side.active.volatiles.state = 0;
1620
- side.active.volatiles.attacks = @intCast(u3, if (showdown)
1621
- battle.rng.range(u4, 2, 4)
1622
- else
1623
- (battle.rng.next() & 1) + 2);
1718
+ side.active.volatiles.attacks = Rolls.bideThrashDuration(battle);
1624
1719
 
1625
1720
  try log.start(battle.active(player), .Bide);
1626
1721
  }
@@ -1642,14 +1737,8 @@ pub const Effects = struct {
1642
1737
  return;
1643
1738
  }
1644
1739
 
1645
- const chance = !foe.active.types.includes(move.type) and if (showdown)
1646
- battle.rng.chance(u8, @as(u8, if (move.effect == .BurnChance1) 26 else 77), 256)
1647
- else
1648
- battle.rng.next() < 1 + (if (move.effect == .BurnChance1)
1649
- Gen12.percent(10)
1650
- else
1651
- Gen12.percent(30));
1652
- if (!chance) return;
1740
+ if (foe.active.types.includes(move.type)) return;
1741
+ if (!Rolls.secondaryChance(battle, move.effect == .BurnChance1)) return;
1653
1742
 
1654
1743
  foe_stored.status = Status.init(.BRN);
1655
1744
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
@@ -1673,11 +1762,7 @@ pub const Effects = struct {
1673
1762
  const sub = foe.active.volatiles.Substitute;
1674
1763
 
1675
1764
  if (move.effect == .ConfusionChance) {
1676
- const chance = if (showdown)
1677
- battle.rng.chance(u8, 25, 256)
1678
- else
1679
- battle.rng.next() < Gen12.percent(10);
1680
- if (!chance) return;
1765
+ if (!Rolls.confusionChance(battle)) return;
1681
1766
  } else {
1682
1767
  if (showdown) {
1683
1768
  if (!try checkHit(battle, player, move, log)) {
@@ -1696,10 +1781,7 @@ pub const Effects = struct {
1696
1781
 
1697
1782
  if (foe.active.volatiles.Confusion) return;
1698
1783
  foe.active.volatiles.Confusion = true;
1699
- foe.active.volatiles.confusion = @intCast(u3, if (showdown)
1700
- battle.rng.range(u8, 2, 6)
1701
- else
1702
- (battle.rng.next() & 3) + 2);
1784
+ foe.active.volatiles.confusion = Rolls.confusionDuration(battle);
1703
1785
 
1704
1786
  try log.start(battle.active(player.foe()), .Confusion);
1705
1787
  }
@@ -1749,11 +1831,8 @@ pub const Effects = struct {
1749
1831
  return Result.Error;
1750
1832
  }
1751
1833
 
1752
- volatiles.disabled_move = @intCast(u3, randomMoveSlot(&battle.rng, &foe.active.moves, n));
1753
- volatiles.disabled_duration = @intCast(u4, if (showdown)
1754
- battle.rng.range(u8, 1, 9)
1755
- else
1756
- (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);
1757
1836
 
1758
1837
  try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
1759
1838
  return null;
@@ -1765,7 +1844,7 @@ pub const Effects = struct {
1765
1844
 
1766
1845
  if (battle.last_damage == 0) {
1767
1846
  assert(showdown);
1768
- return;
1847
+ if (!battle.foe(player).active.volatiles.Substitute) return;
1769
1848
  }
1770
1849
 
1771
1850
  const drain = @max(battle.last_damage / 2, 1);
@@ -1792,15 +1871,7 @@ pub const Effects = struct {
1792
1871
  var volatiles = &battle.foe(player).active.volatiles;
1793
1872
 
1794
1873
  if (volatiles.Substitute) return;
1795
-
1796
- const chance = if (showdown)
1797
- battle.rng.chance(u8, @as(u8, if (move.effect == .FlinchChance1) 26 else 77), 256)
1798
- else
1799
- battle.rng.next() < 1 + (if (move.effect == .FlinchChance1)
1800
- Gen12.percent(10)
1801
- else
1802
- Gen12.percent(30));
1803
- if (!chance) return;
1874
+ if (!Rolls.secondaryChance(battle, move.effect == .FlinchChance1)) return;
1804
1875
 
1805
1876
  volatiles.Flinch = true;
1806
1877
  volatiles.Recharging = false;
@@ -1826,12 +1897,8 @@ pub const Effects = struct {
1826
1897
  return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
1827
1898
  }
1828
1899
 
1829
- const chance = !foe.active.types.includes(move.type) and if (showdown)
1830
- battle.rng.chance(u8, 26, 256)
1831
- else
1832
- battle.rng.next() < 1 + Gen12.percent(10);
1833
- if (!chance) return;
1834
-
1900
+ if (foe.active.types.includes(move.type)) return;
1901
+ if (!Rolls.secondaryChance(battle, true)) return;
1835
1902
  // Freeze Clause Mod
1836
1903
  if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1837
1904
 
@@ -1968,15 +2035,15 @@ pub const Effects = struct {
1968
2035
  }
1969
2036
  break :has_mimic false;
1970
2037
  };
1971
- // If the foe is Invulnerable we still want to fall through to checkHit to be able to
1972
- // trigger |-miss| instead of |-fail|
1973
- 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;
1974
2041
  return try log.fail(battle.active(player.foe()), .None);
1975
2042
  }
1976
2043
  }
1977
2044
  if (!try checkHit(battle, player, move, log)) return;
1978
2045
 
1979
- const rslot = randomMoveSlot(&battle.rng, &foe.active.moves, 0);
2046
+ const rslot = Rolls.moveSlot(battle, &foe.active.moves, 0);
1980
2047
  side.active.move(oslot).id = foe.active.move(rslot).id;
1981
2048
 
1982
2049
  try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
@@ -1997,7 +2064,8 @@ pub const Effects = struct {
1997
2064
  assert(!side.active.volatiles.MultiHit);
1998
2065
  side.active.volatiles.MultiHit = true;
1999
2066
 
2000
- 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;
2001
2069
  }
2002
2070
 
2003
2071
  fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2041,14 +2109,8 @@ pub const Effects = struct {
2041
2109
  }
2042
2110
 
2043
2111
  // Body Slam can't paralyze a Normal type Pokémon
2044
- const chance = !foe.active.types.includes(move.type) and if (showdown)
2045
- battle.rng.chance(u8, @as(u8, if (move.effect == .ParalyzeChance1) 26 else 77), 256)
2046
- else
2047
- battle.rng.next() < 1 + (if (move.effect == .ParalyzeChance1)
2048
- Gen12.percent(10)
2049
- else
2050
- Gen12.percent(30));
2051
- if (!chance) return;
2112
+ if (foe.active.types.includes(move.type)) return;
2113
+ if (!Rolls.secondaryChance(battle, move.effect == .ParalyzeChance1)) return;
2052
2114
 
2053
2115
  foe_stored.status = Status.init(.PAR);
2054
2116
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
@@ -2091,16 +2153,7 @@ pub const Effects = struct {
2091
2153
 
2092
2154
  if (move.effect == .Poison) {
2093
2155
  if (!showdown and !try checkHit(battle, player, move, log)) return;
2094
- } else {
2095
- const chance = if (showdown)
2096
- battle.rng.chance(u8, @as(u8, if (move.effect == .PoisonChance1) 52 else 103), 256)
2097
- else
2098
- battle.rng.next() < 1 + (if (move.effect == .PoisonChance1)
2099
- Gen12.percent(20)
2100
- else
2101
- Gen12.percent(40));
2102
- if (!chance) return;
2103
- }
2156
+ } else if (!Rolls.poisonChance(battle, move.effect == .PoisonChance1)) return;
2104
2157
 
2105
2158
  foe_stored.status = Status.init(.PSN);
2106
2159
  if (toxic) {
@@ -2114,10 +2167,8 @@ pub const Effects = struct {
2114
2167
 
2115
2168
  fn rage(battle: anytype, player: Player) !void {
2116
2169
  var volatiles = &battle.side(player).active.volatiles;
2117
- if (volatiles.Rage) return;
2118
- volatiles.Rage = true;
2119
2170
  assert(!volatiles.Bide);
2120
- volatiles.state = 0;
2171
+ volatiles.Rage = true;
2121
2172
  }
2122
2173
 
2123
2174
  fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
@@ -2169,16 +2220,7 @@ pub const Effects = struct {
2169
2220
  }
2170
2221
  foe.active.volatiles.Recharging = false;
2171
2222
 
2172
- const duration = @intCast(u3, if (showdown)
2173
- battle.rng.range(u8, 1, 8)
2174
- else loop: {
2175
- while (true) {
2176
- const r = battle.rng.next() & 7;
2177
- if (r != 0) break :loop r;
2178
- }
2179
- });
2180
-
2181
- foe_stored.status = Status.slp(duration);
2223
+ foe_stored.status = Status.slp(Rolls.sleepDuration(battle));
2182
2224
  try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
2183
2225
  }
2184
2226
 
@@ -2186,7 +2228,7 @@ pub const Effects = struct {
2186
2228
  try log.activate(battle.active(player), .Splash);
2187
2229
  }
2188
2230
 
2189
- fn substitute(battle: anytype, player: Player, log: anytype) !void {
2231
+ fn substitute(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
2190
2232
  var side = battle.side(player);
2191
2233
  if (side.active.volatiles.Substitute) {
2192
2234
  try log.fail(battle.active(player), .Substitute);
@@ -2211,14 +2253,18 @@ pub const Effects = struct {
2211
2253
  side.active.volatiles.substitute = hp + 1;
2212
2254
  side.active.volatiles.Substitute = true;
2213
2255
  try log.start(battle.active(player), .Substitute);
2214
- if (hp > 0) try log.damage(battle.active(player), side.stored(), .None);
2256
+ if (hp > 0) {
2257
+ try log.damage(battle.active(player), side.stored(), .None);
2258
+ if (showdown and side.stored().hp == 0) residual.* = false;
2259
+ }
2215
2260
  }
2216
2261
 
2217
2262
  fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
2218
2263
  if (!showdown or battle.side(player).last_selected_move == .Teleport) return;
2219
2264
 
2220
- // 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
2221
2266
  _ = try checkHit(battle, player, move, log);
2267
+ battle.last_damage = 0;
2222
2268
  }
2223
2269
 
2224
2270
  fn thrashing(battle: anytype, player: Player) void {
@@ -2227,11 +2273,7 @@ pub const Effects = struct {
2227
2273
  assert(!volatiles.Bide);
2228
2274
 
2229
2275
  volatiles.Thrashing = true;
2230
- volatiles.state = 0;
2231
- volatiles.attacks = @intCast(u3, if (showdown)
2232
- battle.rng.range(u8, 2, 4)
2233
- else
2234
- (battle.rng.next() & 1) + 2);
2276
+ volatiles.attacks = Rolls.bideThrashDuration(battle);
2235
2277
  }
2236
2278
 
2237
2279
  fn transform(battle: anytype, player: Player, log: anytype) !void {
@@ -2263,7 +2305,7 @@ pub const Effects = struct {
2263
2305
  try log.transform(battle.active(player), foe_ident);
2264
2306
  }
2265
2307
 
2266
- fn binding(battle: anytype, player: Player) void {
2308
+ fn binding(battle: anytype, player: Player, rewrap: bool) void {
2267
2309
  var side = battle.side(player);
2268
2310
  var foe = battle.foe(player);
2269
2311
 
@@ -2271,9 +2313,11 @@ pub const Effects = struct {
2271
2313
  side.active.volatiles.Binding = true;
2272
2314
  // GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
2273
2315
  // (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
2274
- 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);
2275
2319
 
2276
- side.active.volatiles.attacks = distribution(battle) - 1;
2320
+ side.active.volatiles.attacks = Rolls.distribution(battle) - 1;
2277
2321
  }
2278
2322
 
2279
2323
  fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2300,9 +2344,11 @@ pub const Effects = struct {
2300
2344
  return try log.fail(ident, .None);
2301
2345
  }
2302
2346
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2303
- const stat = unmodifiedStats(battle, player).atk;
2347
+ const stat = unmodifiedStats(battle, side).atk;
2304
2348
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2305
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;
2306
2352
  },
2307
2353
  .DefenseUp1, .DefenseUp2 => {
2308
2354
  assert(boosts.def >= -6 and boosts.def <= 6);
@@ -2318,7 +2364,7 @@ pub const Effects = struct {
2318
2364
  return try log.fail(ident, .None);
2319
2365
  }
2320
2366
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2321
- const stat = unmodifiedStats(battle, player).def;
2367
+ const stat = unmodifiedStats(battle, side).def;
2322
2368
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2323
2369
  try log.boost(ident, .Defense, n);
2324
2370
  },
@@ -2335,7 +2381,7 @@ pub const Effects = struct {
2335
2381
  return try log.fail(ident, .None);
2336
2382
  }
2337
2383
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2338
- const stat = unmodifiedStats(battle, player).spe;
2384
+ const stat = unmodifiedStats(battle, side).spe;
2339
2385
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2340
2386
  try log.boost(ident, .Speed, 2);
2341
2387
  },
@@ -2355,7 +2401,7 @@ pub const Effects = struct {
2355
2401
  return try log.fail(ident, .None);
2356
2402
  }
2357
2403
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2358
- const stat = unmodifiedStats(battle, player).spc;
2404
+ const stat = unmodifiedStats(battle, side).spc;
2359
2405
  stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2360
2406
  try log.boost(ident, .SpecialAttack, n);
2361
2407
  try log.boost(ident, .SpecialDefense, n);
@@ -2382,11 +2428,7 @@ pub const Effects = struct {
2382
2428
  }
2383
2429
 
2384
2430
  if (move.effect.isStatDownChance()) {
2385
- const chance = if (showdown)
2386
- battle.rng.chance(u8, 85, 256)
2387
- else
2388
- battle.rng.next() < Gen12.percent(33) + 1;
2389
- if (!chance or foe.active.volatiles.Invulnerable) return;
2431
+ if (!Rolls.unboost(battle) or foe.active.volatiles.Invulnerable) return;
2390
2432
  } else if (!showdown and !try checkHit(battle, player, move, log)) {
2391
2433
  return; // checkHit already checks for Invulnerable
2392
2434
  }
@@ -2408,7 +2450,7 @@ pub const Effects = struct {
2408
2450
  return try log.fail(foe_ident, .None);
2409
2451
  }
2410
2452
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2411
- const stat = unmodifiedStats(battle, player.foe()).atk;
2453
+ const stat = unmodifiedStats(battle, foe).atk;
2412
2454
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2413
2455
  try log.boost(foe_ident, .Attack, -1);
2414
2456
  },
@@ -2426,7 +2468,7 @@ pub const Effects = struct {
2426
2468
  return try log.fail(foe_ident, .None);
2427
2469
  }
2428
2470
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2429
- const stat = unmodifiedStats(battle, player.foe()).def;
2471
+ const stat = unmodifiedStats(battle, foe).def;
2430
2472
  stats.def = @max(1, stat * mod[0] / mod[1]);
2431
2473
  try log.boost(foe_ident, .Defense, -@as(i8, n));
2432
2474
  },
@@ -2443,7 +2485,7 @@ pub const Effects = struct {
2443
2485
  return try log.fail(foe_ident, .None);
2444
2486
  }
2445
2487
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2446
- const stat = unmodifiedStats(battle, player.foe()).spe;
2488
+ const stat = unmodifiedStats(battle, foe).spe;
2447
2489
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2448
2490
  try log.boost(foe_ident, .Speed, -1);
2449
2491
  assert(boosts.spe >= -6);
@@ -2463,7 +2505,7 @@ pub const Effects = struct {
2463
2505
  return try log.fail(foe_ident, .None);
2464
2506
  }
2465
2507
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2466
- const stat = unmodifiedStats(battle, player.foe()).spc;
2508
+ const stat = unmodifiedStats(battle, foe).spc;
2467
2509
  stats.spc = @max(1, stat * mod[0] / mod[1]);
2468
2510
  try log.boost(foe_ident, .SpecialAttack, -1);
2469
2511
  try log.boost(foe_ident, .SpecialDefense, -1);
@@ -2482,8 +2524,7 @@ pub const Effects = struct {
2482
2524
  }
2483
2525
  };
2484
2526
 
2485
- fn unmodifiedStats(battle: anytype, who: Player) *Stats(u16) {
2486
- const side = battle.side(who);
2527
+ fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
2487
2528
  if (!side.active.volatiles.Transform) return &side.stored().stats;
2488
2529
  const id = ID.from(side.active.volatiles.transform);
2489
2530
  return &battle.side(id.player).pokemon[id.id - 1].stats;
@@ -2497,7 +2538,7 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
2497
2538
  }
2498
2539
  }
2499
2540
 
2500
- fn isForced(active: ActivePokemon) bool {
2541
+ inline fn isForced(active: ActivePokemon) bool {
2501
2542
  return active.volatiles.Recharging or active.volatiles.Rage or
2502
2543
  active.volatiles.Thrashing or active.volatiles.Charging;
2503
2544
  }
@@ -2549,68 +2590,140 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
2549
2590
  }
2550
2591
  }
2551
2592
 
2552
- fn disabled(side: *Side, ident: ID, log: anytype) !bool {
2553
- if (side.active.volatiles.disabled_move != 0) {
2554
- // A Pokémon that transforms after being disabled may end up with less move slots
2555
- const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
2556
- if (m != .None and m == side.last_selected_move) {
2557
- side.active.volatiles.Charging = false;
2558
- try log.disabled(ident, side.last_selected_move);
2559
- 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;
2560
2609
  }
2561
2610
  }
2562
- return false;
2563
- }
2564
2611
 
2565
- fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
2566
- var volatiles = &active.volatiles;
2567
- assert(volatiles.Thrashing);
2568
- if (volatiles.attacks > 0) return false;
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;
2615
+ }
2569
2616
 
2570
- volatiles.Thrashing = false;
2571
- volatiles.Confusion = true;
2572
- volatiles.confusion = @intCast(u3, if (showdown)
2573
- battle.rng.range(u8, 2, 6)
2574
- else
2575
- (battle.rng.next() & 3) + 2);
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
+ }
2576
2621
 
2577
- return true;
2578
- }
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
+ }
2579
2626
 
2580
- const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
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
+ }
2581
2631
 
2582
- fn distribution(battle: anytype) u3 {
2583
- if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
2584
- const r = (battle.rng.next() & 3);
2585
- return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
2586
- }
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
+ }
2587
2636
 
2588
- fn randomMoveSlot(rand: anytype, moves: []MoveSlot, check_pp: u4) u4 {
2589
- if (showdown) {
2590
- if (check_pp == 0) {
2591
- var i: usize = moves.len;
2592
- while (i > 0) {
2593
- i -= 1;
2594
- if (moves[i].id != .None) return rand.range(u4, 0, @intCast(u4, i + 1)) + 1;
2595
- }
2596
- } else {
2597
- var r = rand.range(u4, 0, @intCast(u4, check_pp)) + 1;
2598
- var i: usize = 0;
2599
- while (i < moves.len and r > 0) : (i += 1) {
2600
- if (moves[i].pp > 0) {
2601
- r -= 1;
2602
- if (r == 0) break;
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
+ }
2641
+
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
+ }
2603
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
+ }
2678
+ }
2679
+ return @intCast(u4, i + 1);
2604
2680
  }
2605
- 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;
2606
2686
  }
2607
2687
  }
2608
2688
 
2609
- while (true) {
2610
- const r = @intCast(u4, rand.next() & 3);
2611
- 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);
2612
2692
  }
2613
- }
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
+ };
2614
2727
 
2615
2728
  test "RNG agreement" {
2616
2729
  if (!showdown) return;
@@ -2710,9 +2823,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
2710
2823
  while (slot <= 4) : (slot += 1) {
2711
2824
  const m = active.moves[slot - 1];
2712
2825
  if (m.id == .None) break;
2713
- if (m.id == side.last_selected_move) {
2714
- // Pokémon Showdown displays Struggle if limited to Bide when out of PP
2715
- 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;
2716
2831
  out[n] = .{ .type = .Move, .data = s };
2717
2832
  n += 1;
2718
2833
  return n;