@pkmn/engine 0.1.0-dev.729b1e87 → 0.1.0-dev.7679dad9

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) {
@@ -163,9 +180,9 @@ fn selectMove(
163
180
  // GLITCH: https://glitchcity.wiki/Partial_trapping_move_Mirror_Move_link_battle_glitch
164
181
  if (foe_choice.type == .Switch) {
165
182
  const slot = if (player == .P1)
166
- battle.last_selected_indexes.p1
183
+ battle.last_moves.p1_index
167
184
  else
168
- battle.last_selected_indexes.p2;
185
+ battle.last_moves.p2_index;
169
186
  const last = side.active.move(@intCast(u4, slot));
170
187
  if (last.id == .Metronome) side.last_selected_move = last.id;
171
188
  if (last.id == .MirrorMove) return Result.Error;
@@ -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| {
@@ -218,9 +237,9 @@ fn saveMove(battle: anytype, player: Player, choice: ?Choice) void {
218
237
 
219
238
  side.last_selected_move = move.id;
220
239
  if (player == .P1) {
221
- battle.last_selected_indexes.p1 = @intCast(u4, c.data);
240
+ battle.last_moves.p1_index = @intCast(u3, c.data);
222
241
  } else {
223
- battle.last_selected_indexes.p2 = @intCast(u4, c.data);
242
+ battle.last_moves.p2_index = @intCast(u3, c.data);
224
243
  }
225
244
  }
226
245
  }
@@ -240,9 +259,9 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, log: anyty
240
259
  side.order[slot - 1] = out;
241
260
 
242
261
  if (player == .P1) {
243
- battle.last_selected_indexes.p1 = 1;
262
+ battle.last_moves.p1_index = 1;
244
263
  } else {
245
- battle.last_selected_indexes.p2 = 1;
264
+ battle.last_moves.p2_index = 1;
246
265
  }
247
266
 
248
267
  side.last_used_move = .None;
@@ -299,13 +318,11 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
299
318
  const spe2 = battle.side(.P2).active.stats.spe;
300
319
  if (spe1 == spe2) {
301
320
  // Pokémon Showdown's beforeTurnCallback shenanigans
302
- if (showdown and m1 == .Counter and m2 == .Counter) battle.rng.advance(1);
303
-
304
- const p1 = if (showdown)
305
- battle.rng.range(u8, 0, 2) == 0
306
- else
307
- battle.rng.next() < Gen12.percent(50) + 1;
321
+ if (showdown and !double_switch and m1 == .Counter and m2 == .Counter) {
322
+ battle.rng.advance(1);
323
+ }
308
324
 
325
+ const p1 = Rolls.speedTie(battle);
309
326
  if (!showdown) return if (p1) .P1 else .P2;
310
327
 
311
328
  // Pokémon Showdown's "lockedmove" volatile's onBeforeTurn uses BattleQueue#changeAction,
@@ -315,7 +332,7 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
315
332
  const t1 = battle.side(.P1).active.volatiles.Thrashing;
316
333
  const t2 = battle.side(.P2).active.volatiles.Thrashing;
317
334
  // 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
335
+ // that out and enforce host ordering of events, so P1 just goes first regardless of who
319
336
  // won the original coin flip
320
337
  if (t1 and t2) return .P1;
321
338
  return if (p1) if (t1 and !t2) .P2 else .P1 else if (t2 and !t1) .P1 else .P2;
@@ -328,9 +345,13 @@ fn doTurn(
328
345
  battle: anytype,
329
346
  player: Player,
330
347
  player_choice: Choice,
348
+ player_rewrap: bool,
349
+ player_skip: bool,
331
350
  player_from: ?Move,
332
351
  foe_player: Player,
333
352
  foe_choice: Choice,
353
+ foe_rewrap: bool,
354
+ foe_skip: bool,
334
355
  foe_from: ?Move,
335
356
  log: anytype,
336
357
  ) !?Result {
@@ -338,7 +359,16 @@ fn doTurn(
338
359
 
339
360
  var residual = true;
340
361
  var replace = battle.side(player).stored().hp == 0;
341
- if (try executeMove(battle, player, player_choice, player_from, &residual, log)) |r| return r;
362
+ if (try executeMove(
363
+ battle,
364
+ player,
365
+ player_choice,
366
+ player_rewrap,
367
+ player_skip,
368
+ player_from,
369
+ &residual,
370
+ log,
371
+ )) |r| return r;
342
372
  if (!replace) {
343
373
  if (player_choice.type != .Switch) {
344
374
  if (try checkFaint(battle, foe_player, log)) |r| return r;
@@ -347,8 +377,18 @@ fn doTurn(
347
377
  if (try checkFaint(battle, player, log)) |r| return r;
348
378
  } else if (foe_choice.type == .Pass) return null;
349
379
 
380
+ residual = true;
350
381
  replace = battle.side(foe_player).stored().hp == 0;
351
- if (try executeMove(battle, foe_player, foe_choice, foe_from, &residual, log)) |r| return r;
382
+ if (try executeMove(
383
+ battle,
384
+ foe_player,
385
+ foe_choice,
386
+ foe_rewrap,
387
+ foe_skip,
388
+ foe_from,
389
+ &residual,
390
+ log,
391
+ )) |r| return r;
352
392
  if (!replace) {
353
393
  if (foe_choice.type != .Switch) {
354
394
  if (try checkFaint(battle, player, log)) |r| return r;
@@ -371,6 +411,8 @@ fn executeMove(
371
411
  battle: anytype,
372
412
  player: Player,
373
413
  choice: Choice,
414
+ rewrap: bool,
415
+ skip: bool,
374
416
  from: ?Move,
375
417
  residual: *bool,
376
418
  log: anytype,
@@ -382,9 +424,10 @@ fn executeMove(
382
424
  return null;
383
425
  }
384
426
 
427
+ // This is the correct place to check for SKIP_TURN and abort early, however since Pokémon
428
+ // Showdown overwrites the SKIP_TURN sentinel with its botched move select we need to add an
429
+ // additional skip boolean to accomplish the same thing in the Binding check of BeforeMove
385
430
  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
431
  assert(!showdown);
389
432
  if (battle.foe(player).active.volatiles.Binding) {
390
433
  try log.cant(battle.active(player), .Bound);
@@ -403,9 +446,9 @@ fn executeMove(
403
446
  // choice.data == 0 only happens with Struggle on Pokémon Showdown
404
447
  assert(!showdown);
405
448
  mslot = @intCast(u4, if (player == .P1)
406
- battle.last_selected_indexes.p1
449
+ battle.last_moves.p1_index
407
450
  else
408
- battle.last_selected_indexes.p2);
451
+ battle.last_moves.p2_index);
409
452
  const stored = side.stored();
410
453
  // GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
411
454
  auto = side.last_selected_move == .HyperBeam or side.active.volatiles.Bide or
@@ -427,14 +470,14 @@ fn executeMove(
427
470
  // Incorrect mslot with Pokémon Showdown choice semantics so we need to recover from index
428
471
  assert(mslot == 1);
429
472
  mslot = @intCast(u4, if (player == .P1)
430
- battle.last_selected_indexes.p1
473
+ battle.last_moves.p1_index
431
474
  else
432
- battle.last_selected_indexes.p2);
475
+ battle.last_moves.p2_index);
433
476
  }
434
477
 
435
478
  var skip_can = false;
436
479
  var skip_pp = false;
437
- switch (try beforeMove(battle, player, from, log)) {
480
+ switch (try beforeMove(battle, player, skip, from, residual, log)) {
438
481
  .done => return null,
439
482
  .skip_can => skip_can = true,
440
483
  .skip_pp => skip_pp = true,
@@ -442,14 +485,23 @@ fn executeMove(
442
485
  .err => return @as(?Result, Result.Error),
443
486
  }
444
487
 
445
- if (!skip_can and !try canMove(battle, player, mslot, auto, skip_pp, from, log)) return null;
488
+ if (!skip_can and !try canMove(battle, player, mslot, auto, skip_pp, from, residual, log)) {
489
+ return null;
490
+ }
446
491
 
447
- return doMove(battle, player, mslot, auto, residual, log);
492
+ return doMove(battle, player, mslot, rewrap, auto, residual, log);
448
493
  }
449
494
 
450
- const BeforeMove = union(enum) { done, skip_can, skip_pp, ok, err };
495
+ const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
451
496
 
452
- fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !BeforeMove {
497
+ fn beforeMove(
498
+ battle: anytype,
499
+ player: Player,
500
+ skip: bool,
501
+ from: ?Move,
502
+ residual: *bool,
503
+ log: anytype,
504
+ ) !BeforeMove {
453
505
  var side = battle.side(player);
454
506
  const foe = battle.foe(player);
455
507
  var active = &side.active;
@@ -477,13 +529,15 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
477
529
  return .done;
478
530
  }
479
531
 
480
- if (foe.active.volatiles.Binding) {
532
+ if (skip or foe.active.volatiles.Binding) {
481
533
  try log.cant(ident, .Bound);
482
534
  return .done;
483
535
  }
484
536
 
485
- if (!showdown and volatiles.Flinch) {
486
- volatiles.Flinch = false;
537
+ if (volatiles.Flinch) {
538
+ // Pokémon Showdown doesn't clear Flinch until its imaginary "residual" phase, meaning
539
+ // Pokémon can sometimes flinch multiple times from the same original hit
540
+ if (!showdown) volatiles.Flinch = false;
487
541
  try log.cant(ident, .Flinch);
488
542
  return .done;
489
543
  }
@@ -504,13 +558,6 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
504
558
  // Pokémon Showdown's disable condition has a single onBeforeMove handler
505
559
  if (showdown and try disabled(side, ident, log)) return .done;
506
560
 
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
561
  // This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
515
562
  // before the side had a selected a move - we simply need to assume this leads to a desync
516
563
  if (side.last_selected_move == .None) {
@@ -528,14 +575,9 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
528
575
  } else {
529
576
  try log.activate(ident, .Confusion);
530
577
 
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) {
578
+ if (Rolls.confused(battle)) {
537
579
  assert(!volatiles.MultiHit);
538
- volatiles.state = 0;
580
+ if (!volatiles.Rage) volatiles.state = 0;
539
581
  volatiles.Bide = false;
540
582
  volatiles.Thrashing = false;
541
583
  volatiles.MultiHit = false;
@@ -559,8 +601,12 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
559
601
  player
560
602
  else
561
603
  player.foe();
604
+
605
+ const uncapped = battle.last_damage;
562
606
  // Skipping adjustDamage / randomizeDamage / checkHit
563
607
  _ = try applyDamage(battle, player, target, .Confusion, log);
608
+ // Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
609
+ if (showdown) battle.last_damage = uncapped;
564
610
 
565
611
  return .done;
566
612
  }
@@ -569,28 +615,28 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
569
615
 
570
616
  if (!showdown and try disabled(side, ident, log)) return .done;
571
617
 
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
- }
618
+ if (Status.is(stored.status, .PAR) and Rolls.paralyzed(battle)) {
619
+ if (!volatiles.Rage) volatiles.state = 0;
620
+ volatiles.Bide = false;
621
+ volatiles.Thrashing = false;
622
+ volatiles.Charging = false;
623
+ volatiles.Binding = false;
624
+ // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
625
+ try log.cant(ident, .Paralysis);
626
+ return .done;
588
627
  }
589
628
 
590
629
  if (volatiles.Bide) {
591
630
  assert(!volatiles.Thrashing and !volatiles.Rage);
592
631
 
593
- volatiles.state +%= battle.last_damage;
632
+ if (showdown) {
633
+ // Pokémon Showdown doesn't implement Bide potentially overflowing in the event of
634
+ // OHKO-move damage, but we can fake this incorrect behavior by simply saturating the
635
+ // addition because 65535 is sufficient to faint any Pokémon anyway
636
+ volatiles.state +|= battle.last_damage;
637
+ } else {
638
+ volatiles.state +%= battle.last_damage;
639
+ }
594
640
 
595
641
  assert(volatiles.attacks > 0);
596
642
 
@@ -611,7 +657,18 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
611
657
  return .done;
612
658
  }
613
659
 
660
+ const sub = showdown and foe.active.volatiles.Substitute;
614
661
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
662
+ if (foe.stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
663
+
664
+ // For reasons passing understanding, Pokémon Showdown still inflicts residual damage to
665
+ // Bide's user even if the above damage has caused the foe to faint. It's simpler to always
666
+ // run residual here regardless of whether the foe fainted and opt-out of the default flow
667
+ if (showdown) {
668
+ residual.* = false;
669
+ try handleResidual(battle, player, log);
670
+ }
671
+
615
672
  return .done;
616
673
  }
617
674
 
@@ -623,8 +680,13 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
623
680
  try log.start(battle.active(player), .ConfusionSilent);
624
681
  }
625
682
  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);
683
+ if (showdown) {
684
+ // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
685
+ // used in useMove and doesn't have the notion of skipping canMove semantics
686
+ side.last_used_move = side.last_selected_move;
687
+ if (handleThrashing(battle, active)) {
688
+ try log.start(battle.active(player), .ConfusionSilent);
689
+ }
628
690
  }
629
691
  return .skip_can;
630
692
  }
@@ -639,7 +701,11 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
639
701
  side.last_selected_move,
640
702
  );
641
703
  if (showdown or battle.last_damage != 0) {
704
+ const sub = showdown and foe.active.volatiles.Substitute;
642
705
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
706
+ if (battle.foe(player).stored().hp > 0 and !sub) {
707
+ try buildRage(battle, player.foe(), log);
708
+ }
643
709
  }
644
710
  return .done;
645
711
  }
@@ -654,6 +720,7 @@ fn canMove(
654
720
  auto: bool,
655
721
  skip_pp: bool,
656
722
  from: ?Move,
723
+ residual: *bool,
657
724
  log: anytype,
658
725
  ) !bool {
659
726
  var side = battle.side(player);
@@ -670,6 +737,7 @@ fn canMove(
670
737
  side.active.volatiles.Invulnerable = false;
671
738
  } else if (move.effect == .Charge) {
672
739
  try log.move(player_ident, side.last_selected_move, .{}, from);
740
+ setCounterable(battle, player, side, move);
673
741
  try Effects.charge(battle, player, log);
674
742
  return false;
675
743
  }
@@ -686,9 +754,10 @@ fn canMove(
686
754
  // regrettable, but this information is more "nice to have" than required
687
755
  const f = if (from != null and from.? == .None) null else from;
688
756
  try log.move(player_ident, side.last_selected_move, battle.active(target), f);
757
+ setCounterable(battle, player, side, move);
689
758
 
690
759
  if (move.effect.onBegin()) {
691
- try onBegin(battle, player, move, mslot, log);
760
+ try onBegin(battle, player, move, mslot, residual, log);
692
761
  return false;
693
762
  }
694
763
 
@@ -696,12 +765,27 @@ fn canMove(
696
765
  Effects.thrashing(battle, player);
697
766
  } else if (!showdown and move.effect == .Binding) {
698
767
  // Pokémon Showdown handles this after hit/miss checks and damage calculation
699
- Effects.binding(battle, player);
768
+ Effects.binding(battle, player, false);
700
769
  }
701
770
 
702
771
  return true;
703
772
  }
704
773
 
774
+ fn setCounterable(battle: anytype, player: Player, side: *Side, move: Move.Data) void {
775
+ // The Counter desync is caused by the cartridge not calling GetCurrentMove until now, meaning
776
+ // in cases where an early return happens the data for a players last selected move does not get
777
+ // reloaded and HandleCounterMove actually bases its success/failure off of stale information.
778
+ // This boolean state we track here doesn't exist on the cartridge because it instead manifests
779
+ // as the desync results from actually having two separate battle states that subtly disagree
780
+ const counterable = side.last_selected_move != .Counter and move.bp > 0 and
781
+ (move.type == .Normal or move.type == .Fighting);
782
+ if (player == .P1) {
783
+ battle.last_moves.p1_counterable = @boolToInt(counterable);
784
+ } else {
785
+ battle.last_moves.p2_counterable = @boolToInt(counterable);
786
+ }
787
+ }
788
+
705
789
  fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
706
790
  if (side.last_selected_move == .Struggle) return;
707
791
 
@@ -737,6 +821,7 @@ fn doMove(
737
821
  battle: anytype,
738
822
  player: Player,
739
823
  mslot: u4,
824
+ rewrap: bool,
740
825
  auto: bool,
741
826
  residual: *bool,
742
827
  log: anytype,
@@ -767,16 +852,21 @@ fn doMove(
767
852
  (@enumToInt(move.type.effectiveness(foe.active.types.type1)) == 0 or
768
853
  @enumToInt(move.type.effectiveness(foe.active.types.type2)) == 0);
769
854
  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;
855
+ if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) {
856
+ battle.last_damage = 0;
857
+ break :miss true;
858
+ }
771
859
  break :miss move.target != .Self and !moveHit(battle, player, move, &immune, &mist);
772
860
  };
773
861
  assert(!immune or miss or (showdown and move.effect == .Binding));
774
862
 
863
+ var late = showdown and move.effect != .Explode;
775
864
  const skip = status or immune;
776
865
  if ((!showdown or (!skip or counter)) and !miss) blk: {
777
866
  if (showdown and move.effect.isMulti()) {
778
867
  Effects.multiHit(battle, player, move);
779
868
  hits = side.active.volatiles.attacks;
869
+ late = false;
780
870
  }
781
871
 
782
872
  // Cartridge rolls for crit even for moves that can't crit (Counter/Metronome/status/OHKO)
@@ -810,7 +900,7 @@ fn doMove(
810
900
  }
811
901
 
812
902
  // 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;
903
+ if (showdown and skip) battle.last_damage = 0;
814
904
 
815
905
  miss = if (showdown or skip)
816
906
  miss
@@ -823,9 +913,9 @@ fn doMove(
823
913
 
824
914
  if (!showdown or !miss) {
825
915
  if (move.effect == .MirrorMove) {
826
- return mirrorMove(battle, player, mslot, auto, residual, log);
916
+ return mirrorMove(battle, player, mslot, rewrap, auto, residual, log);
827
917
  } else if (move.effect == .Metronome) {
828
- return metronome(battle, player, mslot, auto, residual, log);
918
+ return metronome(battle, player, mslot, rewrap, auto, residual, log);
829
919
  } else if (move.effect.onEnd()) {
830
920
  try onEnd(battle, player, move, log);
831
921
  return null;
@@ -840,7 +930,7 @@ fn doMove(
840
930
  move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
841
931
  if (ohko) {
842
932
  try log.immune(foe_ident, .OHKO);
843
- } else if (immune and !invulnerable) {
933
+ } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
844
934
  try log.immune(foe_ident, .None);
845
935
  } else if (mist) {
846
936
  if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
@@ -857,15 +947,9 @@ fn doMove(
857
947
  if (showdown and side.stored().hp == 0) residual.* = false;
858
948
  } else if (move.effect == .Explode) {
859
949
  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
- }
950
+ try buildRage(battle, player.foe(), log);
951
+ } else if (showdown and move.effect == .Disable) {
952
+ try buildRage(battle, player.foe(), log);
869
953
  }
870
954
  return null;
871
955
  }
@@ -878,6 +962,9 @@ fn doMove(
878
962
  hits = side.active.volatiles.attacks;
879
963
  }
880
964
 
965
+ // Pokémon Showdown only builds Rage for Disable/Explosion (hit/miss) when attacking into a sub
966
+ const sub = showdown and foe.active.volatiles.Substitute;
967
+
881
968
  var nullified = false;
882
969
  var hit: u4 = 0;
883
970
  while (hit < hits) {
@@ -893,9 +980,7 @@ fn doMove(
893
980
  if (hit == 0 and ohko) try log.ohko();
894
981
  hit += 1;
895
982
  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
- }
983
+ if (!late and (!sub or move.effect == .Explode)) try buildRage(battle, player.foe(), log);
899
984
  // If the substitute breaks during a multi-hit attack, the attack ends
900
985
  if (nullified) break;
901
986
  }
@@ -911,11 +996,17 @@ fn doMove(
911
996
  } else if (showdown) {
912
997
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
913
998
  if (move.effect == .Binding) {
914
- Effects.binding(battle, player);
999
+ Effects.binding(battle, player, rewrap);
915
1000
  if (immune) {
916
1001
  battle.last_damage = 0;
1002
+ assert(foe.stored().hp > 0);
917
1003
  // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
918
- try log.damage(battle.active(player.foe()), foe.stored(), .None);
1004
+ if (sub) {
1005
+ try log.activate(battle.active(player.foe()), .Substitute);
1006
+ } else {
1007
+ try log.damage(battle.active(player.foe()), foe.stored(), .None);
1008
+ try buildRage(battle, player.foe(), log);
1009
+ }
919
1010
  return null;
920
1011
  }
921
1012
  }
@@ -932,6 +1023,9 @@ fn doMove(
932
1023
 
933
1024
  if (foe.stored().hp == 0) return null;
934
1025
 
1026
+ // Pokémon Showdown builds Rage at the wrong time for non-MultiHit move
1027
+ if (late and !sub and move.effect != .Disable) try buildRage(battle, player.foe(), log);
1028
+
935
1029
  if (!move.effect.isSpecial()) {
936
1030
  // On the cartridge Rage is not considered to be "special" and thus gets executed for a
937
1031
  // second time here (after being executed in the "always happens" block above) but that
@@ -943,7 +1037,9 @@ fn doMove(
943
1037
  if (move.effect == .Twineedle) {
944
1038
  if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), log);
945
1039
  } else if (move.effect == .Disable) {
946
- return Effects.disable(battle, player, move, log);
1040
+ const result = try Effects.disable(battle, player, move, log);
1041
+ if (showdown) try buildRage(battle, player.foe(), log);
1042
+ return result;
947
1043
  } else {
948
1044
  try moveEffect(battle, player, move, log);
949
1045
  }
@@ -961,8 +1057,7 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
961
1057
  chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
962
1058
  chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
963
1059
 
964
- if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
965
- return std.math.rotl(u8, battle.rng.next(), 3) < chance;
1060
+ return Rolls.criticalHit(battle, chance);
966
1061
  }
967
1062
 
968
1063
  fn calcDamage(
@@ -1066,49 +1161,37 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
1066
1161
 
1067
1162
  fn randomizeDamage(battle: anytype) void {
1068
1163
  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
-
1164
+ const random = Rolls.damage(battle);
1079
1165
  battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
1080
1166
  }
1081
1167
 
1082
1168
  fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
1083
1169
  const side = battle.side(player);
1170
+ const foe = battle.foe(player);
1084
1171
 
1085
1172
  if (!try checkHit(battle, player, move, log)) return null;
1086
1173
 
1087
1174
  battle.last_damage = switch (side.last_selected_move) {
1088
- .SuperFang => @max(battle.foe(player).stored().hp / 2, 1),
1175
+ .SuperFang => @max(foe.stored().hp / 2, 1),
1089
1176
  .SeismicToss, .NightShade => side.stored().level,
1090
1177
  .SonicBoom => 20,
1091
1178
  .DragonRage => 40,
1092
1179
  // GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
1093
1180
  .Psywave => power: {
1094
1181
  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
- }
1182
+ // GLITCH: Psywave infinite glitch loop
1183
+ if (!showdown and max <= 1) return Result.Error;
1184
+ break :power Rolls.psywave(battle, max);
1105
1185
  },
1106
1186
  else => unreachable,
1107
1187
  };
1108
1188
 
1109
1189
  if (battle.last_damage == 0) return if (showdown) null else Result.Error;
1110
1190
 
1191
+ const sub = showdown and foe.active.volatiles.Substitute;
1111
1192
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
1193
+ if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
1194
+
1112
1195
  return null;
1113
1196
  }
1114
1197
 
@@ -1120,19 +1203,17 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
1120
1203
  return null;
1121
1204
  }
1122
1205
 
1123
- // Pretend Counter was used as a stand-in when no move has been used to fail below with 0 BP
1124
- const foe_last_used_move =
1125
- Move.get(if (foe.last_used_move == .None) .Counter else foe.last_used_move);
1206
+ // Pretend Splash was used as a stand-in when no move has been used to fail below with 0 BP
1126
1207
  const foe_last_selected_move =
1127
1208
  Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
1128
- .Counter
1209
+ .Splash
1129
1210
  else
1130
1211
  foe.last_selected_move);
1131
1212
 
1132
- const used = foe_last_used_move.bp > 0 and
1133
- foe.last_used_move != .Counter and
1134
- (foe_last_used_move.type == .Normal or
1135
- foe_last_used_move.type == .Fighting);
1213
+ const used = (if (player.foe() == .P1)
1214
+ battle.last_moves.p1_counterable
1215
+ else
1216
+ battle.last_moves.p2_counterable) != 0;
1136
1217
 
1137
1218
  const selected = foe_last_selected_move.bp > 0 and
1138
1219
  foe.last_selected_move != .Counter and
@@ -1151,12 +1232,14 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
1151
1232
  return null;
1152
1233
  }
1153
1234
 
1154
- battle.last_damage = if (battle.last_damage > 0x7FFF) 0xFFFF else battle.last_damage * 2;
1235
+ battle.last_damage *|= 2;
1155
1236
 
1156
1237
  // Pokémon Showdown calls checkHit before Counter
1157
1238
  if (!showdown and !try checkHit(battle, player, move, log)) return null;
1158
1239
 
1240
+ const sub = showdown and foe.active.volatiles.Substitute;
1159
1241
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
1242
+ if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
1160
1243
  return null;
1161
1244
  }
1162
1245
 
@@ -1200,6 +1283,7 @@ fn mirrorMove(
1200
1283
  battle: anytype,
1201
1284
  player: Player,
1202
1285
  mslot: u4,
1286
+ rewrap: bool,
1203
1287
  auto: bool,
1204
1288
  residual: *bool,
1205
1289
  log: anytype,
@@ -1212,41 +1296,30 @@ fn mirrorMove(
1212
1296
  if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
1213
1297
  try log.fail(battle.active(player), .None);
1214
1298
  return null;
1299
+ } else if (!showdown or foe.last_used_move != .Struggle) {
1300
+ incrementPP(side, mslot);
1215
1301
  }
1216
1302
 
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);
1303
+ if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
1304
+ return doMove(battle, player, mslot, rewrap, auto, residual, log);
1221
1305
  }
1222
1306
 
1223
1307
  fn metronome(
1224
1308
  battle: anytype,
1225
1309
  player: Player,
1226
1310
  mslot: u4,
1311
+ rewrap: bool,
1227
1312
  auto: bool,
1228
1313
  residual: *bool,
1229
1314
  log: anytype,
1230
1315
  ) !?Result {
1231
1316
  var side = battle.side(player);
1232
1317
 
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
-
1318
+ side.last_selected_move = Rolls.metronome(battle);
1246
1319
  incrementPP(side, mslot);
1247
1320
 
1248
- if (!try canMove(battle, player, mslot, auto, false, .Metronome, log)) return null;
1249
- return doMove(battle, player, mslot, auto, residual, log);
1321
+ if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
1322
+ return doMove(battle, player, mslot, rewrap, auto, residual, log);
1250
1323
  }
1251
1324
 
1252
1325
  fn checkHit(battle: anytype, player: Player, move: Move.Data, log: anytype) !bool {
@@ -1300,23 +1373,24 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
1300
1373
  }
1301
1374
 
1302
1375
  // 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));
1376
+ const state = side.active.volatiles.state;
1377
+ var overwrite = move.effect == .Rage or move.effect == .Thrashing;
1378
+ const overwritten = overwrite and state > 0;
1379
+ assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
1380
+
1381
+ var accuracy = if (overwritten) state else @as(u16, Gen12.percent(move.accuracy));
1307
1382
  var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
1308
1383
  accuracy = accuracy * boost[0] / boost[1];
1309
1384
  boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
1310
1385
  accuracy = accuracy * boost[0] / boost[1];
1311
1386
  accuracy = @min(255, @max(1, accuracy));
1312
1387
 
1313
- if (move.effect == .Thrashing or move.effect == .Rage) state.* = accuracy;
1388
+ // Pokémon Showdown only overwrites if the volatile is present
1389
+ if (showdown) overwrite = side.active.volatiles.Rage or side.active.volatiles.Thrashing;
1390
+ if (overwrite) side.active.volatiles.state = accuracy;
1314
1391
 
1315
1392
  // 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;
1393
+ break :miss !Rolls.hit(battle, accuracy);
1320
1394
  };
1321
1395
 
1322
1396
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1431,6 +1505,15 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1431
1505
  }
1432
1506
 
1433
1507
  if (volatiles.LeechSeed) {
1508
+ var foe = battle.foe(player);
1509
+ var foe_stored = foe.stored();
1510
+ const foe_ident = battle.active(player.foe());
1511
+
1512
+ if (foe_stored.hp == 0) {
1513
+ assert(showdown);
1514
+ return;
1515
+ }
1516
+
1434
1517
  var damage = @max(stored.stats.hp / 16, 1);
1435
1518
 
1436
1519
  // GLITCH: Leech Seed + Toxic glitch
@@ -1442,10 +1525,6 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1442
1525
  const amount = @min(damage, stored.hp);
1443
1526
  stored.hp -= amount;
1444
1527
 
1445
- var foe = battle.foe(player);
1446
- var foe_stored = foe.stored();
1447
- const foe_ident = battle.active(player.foe());
1448
-
1449
1528
  // As above, Pokémon Showdown uses damageOf but its not relevant
1450
1529
  if (amount > 0) try log.damage(ident, stored, .LeechSeed);
1451
1530
 
@@ -1461,13 +1540,13 @@ fn endTurn(battle: anytype, log: anytype) @TypeOf(log).Error!Result {
1461
1540
  assert(!battle.side(.P1).active.volatiles.MultiHit);
1462
1541
  assert(!battle.side(.P2).active.volatiles.MultiHit);
1463
1542
 
1543
+ battle.turn += 1;
1544
+
1464
1545
  if (showdown and options.ebc and checkEBC(battle)) {
1465
1546
  try log.tie();
1466
1547
  return Result.Tie;
1467
1548
  }
1468
1549
 
1469
- battle.turn += 1;
1470
-
1471
1550
  if (showdown and battle.turn >= 1000) {
1472
1551
  try log.tie();
1473
1552
  return Result.Tie;
@@ -1534,7 +1613,50 @@ fn checkEBC(battle: anytype) bool {
1534
1613
  return false;
1535
1614
  }
1536
1615
 
1537
- inline fn onBegin(battle: anytype, player: Player, move: Move.Data, mslot: u8, log: anytype) !void {
1616
+ fn disabled(side: *Side, ident: ID, log: anytype) !bool {
1617
+ if (side.active.volatiles.disabled_move != 0) {
1618
+ // A Pokémon that transforms after being disabled may end up with less move slots
1619
+ const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
1620
+ // side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
1621
+ const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
1622
+ m
1623
+ else
1624
+ side.last_selected_move;
1625
+ if (m != .None and m == last) {
1626
+ side.active.volatiles.Charging = false;
1627
+ try log.disabled(ident, last);
1628
+ return true;
1629
+ }
1630
+ }
1631
+ return false;
1632
+ }
1633
+
1634
+ inline fn buildRage(battle: anytype, who: Player, log: anytype) !void {
1635
+ const side = battle.side(who);
1636
+ if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
1637
+ try Effects.boost(battle, who, Move.get(.Rage), log);
1638
+ }
1639
+ }
1640
+
1641
+ fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
1642
+ var volatiles = &active.volatiles;
1643
+ assert(volatiles.Thrashing);
1644
+ if (volatiles.attacks > 0) return false;
1645
+
1646
+ volatiles.Thrashing = false;
1647
+ volatiles.Confusion = true;
1648
+ volatiles.confusion = Rolls.confusionDuration(battle);
1649
+ return true;
1650
+ }
1651
+
1652
+ inline fn onBegin(
1653
+ battle: anytype,
1654
+ player: Player,
1655
+ move: Move.Data,
1656
+ mslot: u8,
1657
+ residual: *bool,
1658
+ log: anytype,
1659
+ ) !void {
1538
1660
  assert(move.effect.onBegin());
1539
1661
  return switch (move.effect) {
1540
1662
  .Confusion => Effects.confusion(battle, player, move, log),
@@ -1550,7 +1672,7 @@ inline fn onBegin(battle: anytype, player: Player, move: Move.Data, mslot: u8, l
1550
1672
  .Poison => Effects.poison(battle, player, move, log),
1551
1673
  .Reflect => Effects.reflect(battle, player, log),
1552
1674
  .Splash => Effects.splash(battle, player, log),
1553
- .Substitute => Effects.substitute(battle, player, log),
1675
+ .Substitute => Effects.substitute(battle, player, residual, log),
1554
1676
  .SwitchAndTeleport => Effects.switchAndTeleport(battle, player, move, log),
1555
1677
  .Transform => Effects.transform(battle, player, log),
1556
1678
  else => unreachable,
@@ -1617,10 +1739,7 @@ pub const Effects = struct {
1617
1739
  side.active.volatiles.Bide = true;
1618
1740
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1619
1741
  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);
1742
+ side.active.volatiles.attacks = Rolls.bideThrashDuration(battle);
1624
1743
 
1625
1744
  try log.start(battle.active(player), .Bide);
1626
1745
  }
@@ -1642,14 +1761,8 @@ pub const Effects = struct {
1642
1761
  return;
1643
1762
  }
1644
1763
 
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;
1764
+ if (foe.active.types.includes(move.type)) return;
1765
+ if (!Rolls.secondaryChance(battle, move.effect == .BurnChance1)) return;
1653
1766
 
1654
1767
  foe_stored.status = Status.init(.BRN);
1655
1768
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
@@ -1673,11 +1786,7 @@ pub const Effects = struct {
1673
1786
  const sub = foe.active.volatiles.Substitute;
1674
1787
 
1675
1788
  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;
1789
+ if (!Rolls.confusionChance(battle)) return;
1681
1790
  } else {
1682
1791
  if (showdown) {
1683
1792
  if (!try checkHit(battle, player, move, log)) {
@@ -1696,10 +1805,7 @@ pub const Effects = struct {
1696
1805
 
1697
1806
  if (foe.active.volatiles.Confusion) return;
1698
1807
  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);
1808
+ foe.active.volatiles.confusion = Rolls.confusionDuration(battle);
1703
1809
 
1704
1810
  try log.start(battle.active(player.foe()), .Confusion);
1705
1811
  }
@@ -1749,11 +1855,8 @@ pub const Effects = struct {
1749
1855
  return Result.Error;
1750
1856
  }
1751
1857
 
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);
1858
+ volatiles.disabled_move = @intCast(u3, Rolls.moveSlot(battle, &foe.active.moves, n));
1859
+ volatiles.disabled_duration = Rolls.disableDuration(battle);
1757
1860
 
1758
1861
  try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
1759
1862
  return null;
@@ -1765,7 +1868,7 @@ pub const Effects = struct {
1765
1868
 
1766
1869
  if (battle.last_damage == 0) {
1767
1870
  assert(showdown);
1768
- return;
1871
+ if (!battle.foe(player).active.volatiles.Substitute) return;
1769
1872
  }
1770
1873
 
1771
1874
  const drain = @max(battle.last_damage / 2, 1);
@@ -1792,15 +1895,7 @@ pub const Effects = struct {
1792
1895
  var volatiles = &battle.foe(player).active.volatiles;
1793
1896
 
1794
1897
  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;
1898
+ if (!Rolls.secondaryChance(battle, move.effect == .FlinchChance1)) return;
1804
1899
 
1805
1900
  volatiles.Flinch = true;
1806
1901
  volatiles.Recharging = false;
@@ -1826,12 +1921,8 @@ pub const Effects = struct {
1826
1921
  return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
1827
1922
  }
1828
1923
 
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
-
1924
+ if (foe.active.types.includes(move.type)) return;
1925
+ if (!Rolls.secondaryChance(battle, true)) return;
1835
1926
  // Freeze Clause Mod
1836
1927
  if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1837
1928
 
@@ -1968,15 +2059,15 @@ pub const Effects = struct {
1968
2059
  }
1969
2060
  break :has_mimic false;
1970
2061
  };
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) {
2062
+ if (!has_mimic) {
2063
+ // Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
2064
+ if (!try checkHit(battle, player, move, log)) return;
1974
2065
  return try log.fail(battle.active(player.foe()), .None);
1975
2066
  }
1976
2067
  }
1977
2068
  if (!try checkHit(battle, player, move, log)) return;
1978
2069
 
1979
- const rslot = randomMoveSlot(&battle.rng, &foe.active.moves, 0);
2070
+ const rslot = Rolls.moveSlot(battle, &foe.active.moves, 0);
1980
2071
  side.active.move(oslot).id = foe.active.move(rslot).id;
1981
2072
 
1982
2073
  try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
@@ -1997,7 +2088,8 @@ pub const Effects = struct {
1997
2088
  assert(!side.active.volatiles.MultiHit);
1998
2089
  side.active.volatiles.MultiHit = true;
1999
2090
 
2000
- side.active.volatiles.attacks = if (move.effect == .MultiHit) distribution(battle) else 2;
2091
+ side.active.volatiles.attacks =
2092
+ if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
2001
2093
  }
2002
2094
 
2003
2095
  fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2041,14 +2133,8 @@ pub const Effects = struct {
2041
2133
  }
2042
2134
 
2043
2135
  // 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;
2136
+ if (foe.active.types.includes(move.type)) return;
2137
+ if (!Rolls.secondaryChance(battle, move.effect == .ParalyzeChance1)) return;
2052
2138
 
2053
2139
  foe_stored.status = Status.init(.PAR);
2054
2140
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
@@ -2091,16 +2177,7 @@ pub const Effects = struct {
2091
2177
 
2092
2178
  if (move.effect == .Poison) {
2093
2179
  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
- }
2180
+ } else if (!Rolls.poisonChance(battle, move.effect == .PoisonChance1)) return;
2104
2181
 
2105
2182
  foe_stored.status = Status.init(.PSN);
2106
2183
  if (toxic) {
@@ -2114,16 +2191,17 @@ pub const Effects = struct {
2114
2191
 
2115
2192
  fn rage(battle: anytype, player: Player) !void {
2116
2193
  var volatiles = &battle.side(player).active.volatiles;
2117
- if (volatiles.Rage) return;
2118
- volatiles.Rage = true;
2119
2194
  assert(!volatiles.Bide);
2120
- volatiles.state = 0;
2195
+ volatiles.Rage = true;
2121
2196
  }
2122
2197
 
2123
2198
  fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
2124
2199
  var side = battle.side(player);
2125
2200
  var stored = side.stored();
2126
2201
 
2202
+ assert(showdown or battle.last_damage > 0);
2203
+ if (showdown and battle.last_damage == 0) return;
2204
+
2127
2205
  const damage = @intCast(i16, @max(battle.last_damage /
2128
2206
  @as(u8, if (side.last_selected_move == .Struggle) 2 else 4), 1));
2129
2207
  stored.hp = @intCast(u16, @max(@intCast(i16, stored.hp) - damage, 0));
@@ -2169,16 +2247,7 @@ pub const Effects = struct {
2169
2247
  }
2170
2248
  foe.active.volatiles.Recharging = false;
2171
2249
 
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);
2250
+ foe_stored.status = Status.slp(Rolls.sleepDuration(battle));
2182
2251
  try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
2183
2252
  }
2184
2253
 
@@ -2186,7 +2255,7 @@ pub const Effects = struct {
2186
2255
  try log.activate(battle.active(player), .Splash);
2187
2256
  }
2188
2257
 
2189
- fn substitute(battle: anytype, player: Player, log: anytype) !void {
2258
+ fn substitute(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
2190
2259
  var side = battle.side(player);
2191
2260
  if (side.active.volatiles.Substitute) {
2192
2261
  try log.fail(battle.active(player), .Substitute);
@@ -2211,14 +2280,18 @@ pub const Effects = struct {
2211
2280
  side.active.volatiles.substitute = hp + 1;
2212
2281
  side.active.volatiles.Substitute = true;
2213
2282
  try log.start(battle.active(player), .Substitute);
2214
- if (hp > 0) try log.damage(battle.active(player), side.stored(), .None);
2283
+ if (hp > 0) {
2284
+ try log.damage(battle.active(player), side.stored(), .None);
2285
+ if (showdown and side.stored().hp == 0) residual.* = false;
2286
+ }
2215
2287
  }
2216
2288
 
2217
2289
  fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
2218
2290
  if (!showdown or battle.side(player).last_selected_move == .Teleport) return;
2219
2291
 
2220
- // Whirlwind/Roar should not roll to hit but Pokémon Showdown does anyway
2292
+ // Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
2221
2293
  _ = try checkHit(battle, player, move, log);
2294
+ battle.last_damage = 0;
2222
2295
  }
2223
2296
 
2224
2297
  fn thrashing(battle: anytype, player: Player) void {
@@ -2227,11 +2300,7 @@ pub const Effects = struct {
2227
2300
  assert(!volatiles.Bide);
2228
2301
 
2229
2302
  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);
2303
+ volatiles.attacks = Rolls.bideThrashDuration(battle);
2235
2304
  }
2236
2305
 
2237
2306
  fn transform(battle: anytype, player: Player, log: anytype) !void {
@@ -2263,7 +2332,7 @@ pub const Effects = struct {
2263
2332
  try log.transform(battle.active(player), foe_ident);
2264
2333
  }
2265
2334
 
2266
- fn binding(battle: anytype, player: Player) void {
2335
+ fn binding(battle: anytype, player: Player, rewrap: bool) void {
2267
2336
  var side = battle.side(player);
2268
2337
  var foe = battle.foe(player);
2269
2338
 
@@ -2271,9 +2340,11 @@ pub const Effects = struct {
2271
2340
  side.active.volatiles.Binding = true;
2272
2341
  // GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
2273
2342
  // (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
2274
- if (!showdown) foe.active.volatiles.Recharging = false;
2343
+ if (!showdown) {
2344
+ foe.active.volatiles.Recharging = false;
2345
+ } else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
2275
2346
 
2276
- side.active.volatiles.attacks = distribution(battle) - 1;
2347
+ side.active.volatiles.attacks = Rolls.distribution(battle) - 1;
2277
2348
  }
2278
2349
 
2279
2350
  fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2300,9 +2371,11 @@ pub const Effects = struct {
2300
2371
  return try log.fail(ident, .None);
2301
2372
  }
2302
2373
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2303
- const stat = unmodifiedStats(battle, player).atk;
2374
+ const stat = unmodifiedStats(battle, side).atk;
2304
2375
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2305
2376
  try log.boost(ident, reason, n);
2377
+ // Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
2378
+ if (showdown and move.effect == .Rage) return;
2306
2379
  },
2307
2380
  .DefenseUp1, .DefenseUp2 => {
2308
2381
  assert(boosts.def >= -6 and boosts.def <= 6);
@@ -2318,7 +2391,7 @@ pub const Effects = struct {
2318
2391
  return try log.fail(ident, .None);
2319
2392
  }
2320
2393
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2321
- const stat = unmodifiedStats(battle, player).def;
2394
+ const stat = unmodifiedStats(battle, side).def;
2322
2395
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2323
2396
  try log.boost(ident, .Defense, n);
2324
2397
  },
@@ -2335,7 +2408,7 @@ pub const Effects = struct {
2335
2408
  return try log.fail(ident, .None);
2336
2409
  }
2337
2410
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2338
- const stat = unmodifiedStats(battle, player).spe;
2411
+ const stat = unmodifiedStats(battle, side).spe;
2339
2412
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2340
2413
  try log.boost(ident, .Speed, 2);
2341
2414
  },
@@ -2355,7 +2428,7 @@ pub const Effects = struct {
2355
2428
  return try log.fail(ident, .None);
2356
2429
  }
2357
2430
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2358
- const stat = unmodifiedStats(battle, player).spc;
2431
+ const stat = unmodifiedStats(battle, side).spc;
2359
2432
  stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2360
2433
  try log.boost(ident, .SpecialAttack, n);
2361
2434
  try log.boost(ident, .SpecialDefense, n);
@@ -2376,28 +2449,24 @@ pub const Effects = struct {
2376
2449
  fn unboost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
2377
2450
  var foe = battle.foe(player);
2378
2451
  const foe_ident = battle.active(player.foe());
2452
+ const secondary = move.effect.isStatDownChance();
2379
2453
 
2380
- if (foe.active.volatiles.Substitute) {
2381
- return if (!move.effect.isStatDownChance()) log.fail(foe_ident, .None);
2382
- }
2454
+ if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(foe_ident, .None);
2383
2455
 
2384
- 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;
2456
+ if (secondary) {
2457
+ if (!Rolls.unboost(battle) or foe.active.volatiles.Invulnerable) return;
2390
2458
  } else if (!showdown and !try checkHit(battle, player, move, log)) {
2391
2459
  return; // checkHit already checks for Invulnerable
2392
2460
  }
2393
2461
 
2394
2462
  var stats = &foe.active.stats;
2395
2463
  var boosts = &foe.active.boosts;
2464
+ const fail = showdown or !secondary;
2396
2465
 
2397
2466
  switch (move.effect) {
2398
2467
  .AttackDown1, .AttackDownChance => {
2399
2468
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2400
- if (boosts.atk == -6) return try log.fail(foe_ident, .None);
2469
+ if (boosts.atk == -6) return if (fail) try log.fail(foe_ident, .None);
2401
2470
  boosts.atk = @intCast(i4, @max(-6, @as(i8, boosts.atk) - 1));
2402
2471
  if (stats.atk == 1) {
2403
2472
  boosts.atk += 1;
@@ -2408,13 +2477,13 @@ pub const Effects = struct {
2408
2477
  return try log.fail(foe_ident, .None);
2409
2478
  }
2410
2479
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2411
- const stat = unmodifiedStats(battle, player.foe()).atk;
2480
+ const stat = unmodifiedStats(battle, foe).atk;
2412
2481
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2413
2482
  try log.boost(foe_ident, .Attack, -1);
2414
2483
  },
2415
2484
  .DefenseDown1, .DefenseDown2, .DefenseDownChance => {
2416
2485
  assert(boosts.def >= -6 and boosts.def <= 6);
2417
- if (boosts.def == -6) return try log.fail(foe_ident, .None);
2486
+ if (boosts.def == -6) return if (fail) try log.fail(foe_ident, .None);
2418
2487
  const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
2419
2488
  boosts.def = @intCast(i4, @max(-6, @as(i8, boosts.def) - n));
2420
2489
  if (stats.def == 1) {
@@ -2426,13 +2495,13 @@ pub const Effects = struct {
2426
2495
  return try log.fail(foe_ident, .None);
2427
2496
  }
2428
2497
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2429
- const stat = unmodifiedStats(battle, player.foe()).def;
2498
+ const stat = unmodifiedStats(battle, foe).def;
2430
2499
  stats.def = @max(1, stat * mod[0] / mod[1]);
2431
2500
  try log.boost(foe_ident, .Defense, -@as(i8, n));
2432
2501
  },
2433
2502
  .SpeedDown1, .SpeedDownChance => {
2434
2503
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2435
- if (boosts.spe == -6) return try log.fail(foe_ident, .None);
2504
+ if (boosts.spe == -6) return if (fail) try log.fail(foe_ident, .None);
2436
2505
  boosts.spe = @intCast(i4, @max(-6, @as(i8, boosts.spe) - 1));
2437
2506
  if (stats.spe == 1) {
2438
2507
  boosts.spe += 1;
@@ -2443,14 +2512,14 @@ pub const Effects = struct {
2443
2512
  return try log.fail(foe_ident, .None);
2444
2513
  }
2445
2514
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2446
- const stat = unmodifiedStats(battle, player.foe()).spe;
2515
+ const stat = unmodifiedStats(battle, foe).spe;
2447
2516
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2448
2517
  try log.boost(foe_ident, .Speed, -1);
2449
2518
  assert(boosts.spe >= -6);
2450
2519
  },
2451
2520
  .SpecialDownChance => {
2452
2521
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2453
- if (boosts.spc == -6) return try log.fail(foe_ident, .None);
2522
+ if (boosts.spc == -6) return if (fail) try log.fail(foe_ident, .None);
2454
2523
  boosts.spc = @intCast(i4, @max(-6, @as(i8, boosts.spc) - 1));
2455
2524
  if (stats.spc == 1) {
2456
2525
  boosts.spc += 1;
@@ -2463,14 +2532,14 @@ pub const Effects = struct {
2463
2532
  return try log.fail(foe_ident, .None);
2464
2533
  }
2465
2534
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2466
- const stat = unmodifiedStats(battle, player.foe()).spc;
2535
+ const stat = unmodifiedStats(battle, foe).spc;
2467
2536
  stats.spc = @max(1, stat * mod[0] / mod[1]);
2468
2537
  try log.boost(foe_ident, .SpecialAttack, -1);
2469
2538
  try log.boost(foe_ident, .SpecialDefense, -1);
2470
2539
  },
2471
2540
  .AccuracyDown1 => {
2472
2541
  assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
2473
- if (boosts.accuracy == -6) return try log.fail(foe_ident, .None);
2542
+ if (boosts.accuracy == -6) return if (fail) try log.fail(foe_ident, .None);
2474
2543
  boosts.accuracy = @intCast(i4, @max(-6, @as(i8, boosts.accuracy) - 1));
2475
2544
  try log.boost(foe_ident, .Accuracy, -1);
2476
2545
  },
@@ -2482,8 +2551,7 @@ pub const Effects = struct {
2482
2551
  }
2483
2552
  };
2484
2553
 
2485
- fn unmodifiedStats(battle: anytype, who: Player) *Stats(u16) {
2486
- const side = battle.side(who);
2554
+ fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
2487
2555
  if (!side.active.volatiles.Transform) return &side.stored().stats;
2488
2556
  const id = ID.from(side.active.volatiles.transform);
2489
2557
  return &battle.side(id.player).pokemon[id.id - 1].stats;
@@ -2497,7 +2565,7 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
2497
2565
  }
2498
2566
  }
2499
2567
 
2500
- fn isForced(active: ActivePokemon) bool {
2568
+ inline fn isForced(active: ActivePokemon) bool {
2501
2569
  return active.volatiles.Recharging or active.volatiles.Rage or
2502
2570
  active.volatiles.Thrashing or active.volatiles.Charging;
2503
2571
  }
@@ -2549,68 +2617,140 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
2549
2617
  }
2550
2618
  }
2551
2619
 
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;
2620
+ pub const Rolls = struct {
2621
+ inline fn speedTie(battle: anytype) bool {
2622
+ if (showdown) return battle.rng.range(u8, 0, 2) == 0;
2623
+ return battle.rng.next() < Gen12.percent(50) + 1;
2624
+ }
2625
+
2626
+ inline fn criticalHit(battle: anytype, chance: u16) bool {
2627
+ if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
2628
+ return std.math.rotl(u8, battle.rng.next(), 3) < chance;
2629
+ }
2630
+
2631
+ inline fn damage(battle: anytype) u8 {
2632
+ if (showdown) return battle.rng.range(u8, 217, 256);
2633
+ while (true) {
2634
+ const r = std.math.rotr(u8, battle.rng.next(), 1);
2635
+ if (r >= 217) return r;
2560
2636
  }
2561
2637
  }
2562
- return false;
2563
- }
2564
2638
 
2565
- fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
2566
- var volatiles = &active.volatiles;
2567
- assert(volatiles.Thrashing);
2568
- if (volatiles.attacks > 0) return false;
2639
+ inline fn hit(battle: anytype, accuracy: u16) bool {
2640
+ if (showdown) return battle.rng.chance(u8, @intCast(u8, accuracy), 256);
2641
+ return battle.rng.next() < accuracy;
2642
+ }
2569
2643
 
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);
2644
+ inline fn confusionDuration(battle: anytype) u3 {
2645
+ if (showdown) return @intCast(u3, battle.rng.range(u8, 2, 6));
2646
+ return @intCast(u3, (battle.rng.next() & 3) + 2);
2647
+ }
2576
2648
 
2577
- return true;
2578
- }
2649
+ inline fn confused(battle: anytype) bool {
2650
+ if (showdown) return !battle.rng.chance(u8, 128, 256);
2651
+ return battle.rng.next() >= Gen12.percent(50) + 1;
2652
+ }
2579
2653
 
2580
- const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
2654
+ inline fn paralyzed(battle: anytype) bool {
2655
+ if (showdown) return battle.rng.chance(u8, 63, 256);
2656
+ return battle.rng.next() < Gen12.percent(25);
2657
+ }
2581
2658
 
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
- }
2659
+ inline fn confusionChance(battle: anytype) bool {
2660
+ if (showdown) return battle.rng.chance(u8, 25, 256);
2661
+ return battle.rng.next() < Gen12.percent(10);
2662
+ }
2587
2663
 
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;
2664
+ inline fn secondaryChance(battle: anytype, low: bool) bool {
2665
+ if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 26 else 77), 256);
2666
+ return battle.rng.next() < 1 + (if (low) Gen12.percent(10) else Gen12.percent(30));
2667
+ }
2668
+
2669
+ inline fn poisonChance(battle: anytype, low: bool) bool {
2670
+ if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 52 else 103), 256);
2671
+ return battle.rng.next() < 1 + (if (low) Gen12.percent(20) else Gen12.percent(40));
2672
+ }
2673
+
2674
+ inline fn sleepDuration(battle: anytype) u3 {
2675
+ if (showdown) return @intCast(u3, battle.rng.range(u8, 1, 8));
2676
+ while (true) {
2677
+ const r = battle.rng.next() & 7;
2678
+ if (r != 0) return @intCast(u3, r);
2679
+ }
2680
+ }
2681
+
2682
+ inline fn bideThrashDuration(battle: anytype) u3 {
2683
+ if (showdown) return @intCast(u3, battle.rng.range(u4, 2, 4));
2684
+ return @intCast(u3, (battle.rng.next() & 1) + 2);
2685
+ }
2686
+
2687
+ fn moveSlot(battle: anytype, moves: []MoveSlot, check_pp: u4) u4 {
2688
+ if (showdown) {
2689
+ if (check_pp == 0) {
2690
+ var i: usize = moves.len;
2691
+ while (i > 0) {
2692
+ i -= 1;
2693
+ if (moves[i].id != .None) {
2694
+ return battle.rng.range(u4, 0, @intCast(u4, i + 1)) + 1;
2695
+ }
2603
2696
  }
2697
+ } else {
2698
+ var r = battle.rng.range(u4, 0, @intCast(u4, check_pp)) + 1;
2699
+ var i: usize = 0;
2700
+ while (i < moves.len and r > 0) : (i += 1) {
2701
+ if (moves[i].pp > 0) {
2702
+ r -= 1;
2703
+ if (r == 0) break;
2704
+ }
2705
+ }
2706
+ return @intCast(u4, i + 1);
2604
2707
  }
2605
- return @intCast(u4, i + 1);
2708
+ }
2709
+
2710
+ while (true) {
2711
+ const r = @intCast(u4, battle.rng.next() & 3);
2712
+ if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
2606
2713
  }
2607
2714
  }
2608
2715
 
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;
2716
+ inline fn disableDuration(battle: anytype) u4 {
2717
+ if (showdown) return @intCast(u4, battle.rng.range(u8, 1, 9));
2718
+ return @intCast(u4, (battle.rng.next() & 7) + 1);
2612
2719
  }
2613
- }
2720
+
2721
+ inline fn metronome(battle: anytype) Move {
2722
+ if (showdown) {
2723
+ const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
2724
+ const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
2725
+ return @intToEnum(Move, r + mod);
2726
+ }
2727
+ while (true) {
2728
+ const r = battle.rng.next();
2729
+ if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
2730
+ if (r >= @enumToInt(Move.Struggle)) continue;
2731
+ return @intToEnum(Move, r);
2732
+ }
2733
+ }
2734
+
2735
+ inline fn psywave(battle: anytype, max: u8) u8 {
2736
+ if (showdown) return battle.rng.range(u8, 0, max);
2737
+ while (true) {
2738
+ const r = battle.rng.next();
2739
+ if (r < max) return r;
2740
+ }
2741
+ }
2742
+
2743
+ inline fn distribution(battle: anytype) u3 {
2744
+ if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
2745
+ const r = (battle.rng.next() & 3);
2746
+ return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
2747
+ }
2748
+
2749
+ inline fn unboost(battle: anytype) bool {
2750
+ if (showdown) return battle.rng.chance(u8, 85, 256);
2751
+ return battle.rng.next() < Gen12.percent(33) + 1;
2752
+ }
2753
+ };
2614
2754
 
2615
2755
  test "RNG agreement" {
2616
2756
  if (!showdown) return;
@@ -2710,9 +2850,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
2710
2850
  while (slot <= 4) : (slot += 1) {
2711
2851
  const m = active.moves[slot - 1];
2712
2852
  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;
2853
+ if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
2854
+ // Pokémon Showdown displays Struggle if limited to Bide but unable to pick
2855
+ const struggle =
2856
+ m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);
2857
+ const s = if (struggle) 0 else slot;
2716
2858
  out[n] = .{ .type = .Move, .data = s };
2717
2859
  n += 1;
2718
2860
  return n;