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

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,
@@ -446,12 +489,19 @@ fn executeMove(
446
489
  return null;
447
490
  }
448
491
 
449
- return doMove(battle, player, mslot, auto, residual, log);
492
+ return doMove(battle, player, mslot, rewrap, auto, residual, log);
450
493
  }
451
494
 
452
- const BeforeMove = union(enum) { done, skip_can, skip_pp, ok, err };
495
+ const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
453
496
 
454
- 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 {
455
505
  var side = battle.side(player);
456
506
  const foe = battle.foe(player);
457
507
  var active = &side.active;
@@ -479,13 +529,15 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
479
529
  return .done;
480
530
  }
481
531
 
482
- if (foe.active.volatiles.Binding) {
532
+ if (skip or foe.active.volatiles.Binding) {
483
533
  try log.cant(ident, .Bound);
484
534
  return .done;
485
535
  }
486
536
 
487
- if (!showdown and volatiles.Flinch) {
488
- 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;
489
541
  try log.cant(ident, .Flinch);
490
542
  return .done;
491
543
  }
@@ -506,13 +558,6 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
506
558
  // Pokémon Showdown's disable condition has a single onBeforeMove handler
507
559
  if (showdown and try disabled(side, ident, log)) return .done;
508
560
 
509
- // Pokémon Showdown checks for Flinch *after* instead of before
510
- if (showdown and volatiles.Flinch) {
511
- volatiles.Flinch = false;
512
- try log.cant(ident, .Flinch);
513
- return .done;
514
- }
515
-
516
561
  // This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
517
562
  // before the side had a selected a move - we simply need to assume this leads to a desync
518
563
  if (side.last_selected_move == .None) {
@@ -530,14 +575,9 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
530
575
  } else {
531
576
  try log.activate(ident, .Confusion);
532
577
 
533
- const confused = if (showdown)
534
- !battle.rng.chance(u8, 128, 256)
535
- else
536
- battle.rng.next() >= Gen12.percent(50) + 1;
537
-
538
- if (confused) {
578
+ if (Rolls.confused(battle)) {
539
579
  assert(!volatiles.MultiHit);
540
- volatiles.state = 0;
580
+ if (!volatiles.Rage) volatiles.state = 0;
541
581
  volatiles.Bide = false;
542
582
  volatiles.Thrashing = false;
543
583
  volatiles.MultiHit = false;
@@ -561,8 +601,12 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
561
601
  player
562
602
  else
563
603
  player.foe();
604
+
605
+ const uncapped = battle.last_damage;
564
606
  // Skipping adjustDamage / randomizeDamage / checkHit
565
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;
566
610
 
567
611
  return .done;
568
612
  }
@@ -571,28 +615,28 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
571
615
 
572
616
  if (!showdown and try disabled(side, ident, log)) return .done;
573
617
 
574
- if (Status.is(stored.status, .PAR)) {
575
- const paralyzed = if (showdown)
576
- battle.rng.chance(u8, 63, 256)
577
- else
578
- battle.rng.next() < Gen12.percent(25);
579
-
580
- if (paralyzed) {
581
- volatiles.state = 0;
582
- volatiles.Bide = false;
583
- volatiles.Thrashing = false;
584
- volatiles.Charging = false;
585
- volatiles.Binding = false;
586
- // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
587
- try log.cant(ident, .Paralysis);
588
- return .done;
589
- }
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;
590
627
  }
591
628
 
592
629
  if (volatiles.Bide) {
593
630
  assert(!volatiles.Thrashing and !volatiles.Rage);
594
631
 
595
- 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
+ }
596
640
 
597
641
  assert(volatiles.attacks > 0);
598
642
 
@@ -613,8 +657,17 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
613
657
  return .done;
614
658
  }
615
659
 
660
+ const sub = showdown and foe.active.volatiles.Substitute;
616
661
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
617
- try buildRage(battle, player.foe(), 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
+ }
618
671
 
619
672
  return .done;
620
673
  }
@@ -627,8 +680,13 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
627
680
  try log.start(battle.active(player), .ConfusionSilent);
628
681
  }
629
682
  try log.move(ident, side.last_selected_move, battle.active(player.foe()), from);
630
- if (showdown and handleThrashing(battle, active)) {
631
- try log.start(battle.active(player), .ConfusionSilent);
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
+ }
632
690
  }
633
691
  return .skip_can;
634
692
  }
@@ -643,7 +701,11 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
643
701
  side.last_selected_move,
644
702
  );
645
703
  if (showdown or battle.last_damage != 0) {
704
+ const sub = showdown and foe.active.volatiles.Substitute;
646
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
+ }
647
709
  }
648
710
  return .done;
649
711
  }
@@ -675,6 +737,7 @@ fn canMove(
675
737
  side.active.volatiles.Invulnerable = false;
676
738
  } else if (move.effect == .Charge) {
677
739
  try log.move(player_ident, side.last_selected_move, .{}, from);
740
+ setCounterable(battle, player, side, move);
678
741
  try Effects.charge(battle, player, log);
679
742
  return false;
680
743
  }
@@ -691,6 +754,7 @@ fn canMove(
691
754
  // regrettable, but this information is more "nice to have" than required
692
755
  const f = if (from != null and from.? == .None) null else from;
693
756
  try log.move(player_ident, side.last_selected_move, battle.active(target), f);
757
+ setCounterable(battle, player, side, move);
694
758
 
695
759
  if (move.effect.onBegin()) {
696
760
  try onBegin(battle, player, move, mslot, residual, log);
@@ -701,12 +765,27 @@ fn canMove(
701
765
  Effects.thrashing(battle, player);
702
766
  } else if (!showdown and move.effect == .Binding) {
703
767
  // Pokémon Showdown handles this after hit/miss checks and damage calculation
704
- Effects.binding(battle, player);
768
+ Effects.binding(battle, player, false);
705
769
  }
706
770
 
707
771
  return true;
708
772
  }
709
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
+
710
789
  fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
711
790
  if (side.last_selected_move == .Struggle) return;
712
791
 
@@ -742,6 +821,7 @@ fn doMove(
742
821
  battle: anytype,
743
822
  player: Player,
744
823
  mslot: u4,
824
+ rewrap: bool,
745
825
  auto: bool,
746
826
  residual: *bool,
747
827
  log: anytype,
@@ -772,12 +852,15 @@ fn doMove(
772
852
  (@enumToInt(move.type.effectiveness(foe.active.types.type1)) == 0 or
773
853
  @enumToInt(move.type.effectiveness(foe.active.types.type2)) == 0);
774
854
  if (immune and move.effect != .Binding) break :miss true;
775
- if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) break :miss true;
855
+ if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) {
856
+ battle.last_damage = 0;
857
+ break :miss true;
858
+ }
776
859
  break :miss move.target != .Self and !moveHit(battle, player, move, &immune, &mist);
777
860
  };
778
861
  assert(!immune or miss or (showdown and move.effect == .Binding));
779
862
 
780
- var late = showdown;
863
+ var late = showdown and move.effect != .Explode;
781
864
  const skip = status or immune;
782
865
  if ((!showdown or (!skip or counter)) and !miss) blk: {
783
866
  if (showdown and move.effect.isMulti()) {
@@ -830,9 +913,9 @@ fn doMove(
830
913
 
831
914
  if (!showdown or !miss) {
832
915
  if (move.effect == .MirrorMove) {
833
- return mirrorMove(battle, player, mslot, auto, residual, log);
916
+ return mirrorMove(battle, player, mslot, rewrap, auto, residual, log);
834
917
  } else if (move.effect == .Metronome) {
835
- return metronome(battle, player, mslot, auto, residual, log);
918
+ return metronome(battle, player, mslot, rewrap, auto, residual, log);
836
919
  } else if (move.effect.onEnd()) {
837
920
  try onEnd(battle, player, move, log);
838
921
  return null;
@@ -847,7 +930,7 @@ fn doMove(
847
930
  move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
848
931
  if (ohko) {
849
932
  try log.immune(foe_ident, .OHKO);
850
- } else if (immune and !invulnerable) {
933
+ } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
851
934
  try log.immune(foe_ident, .None);
852
935
  } else if (mist) {
853
936
  if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
@@ -879,6 +962,9 @@ fn doMove(
879
962
  hits = side.active.volatiles.attacks;
880
963
  }
881
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
+
882
968
  var nullified = false;
883
969
  var hit: u4 = 0;
884
970
  while (hit < hits) {
@@ -894,7 +980,7 @@ fn doMove(
894
980
  if (hit == 0 and ohko) try log.ohko();
895
981
  hit += 1;
896
982
  if (foe.stored().hp == 0) break;
897
- if (!late) try buildRage(battle, player.foe(), log);
983
+ if (!late and (!sub or move.effect == .Explode)) try buildRage(battle, player.foe(), log);
898
984
  // If the substitute breaks during a multi-hit attack, the attack ends
899
985
  if (nullified) break;
900
986
  }
@@ -910,11 +996,17 @@ fn doMove(
910
996
  } else if (showdown) {
911
997
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
912
998
  if (move.effect == .Binding) {
913
- Effects.binding(battle, player);
999
+ Effects.binding(battle, player, rewrap);
914
1000
  if (immune) {
915
1001
  battle.last_damage = 0;
1002
+ assert(foe.stored().hp > 0);
916
1003
  // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
917
- 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
+ }
918
1010
  return null;
919
1011
  }
920
1012
  }
@@ -931,8 +1023,8 @@ fn doMove(
931
1023
 
932
1024
  if (foe.stored().hp == 0) return null;
933
1025
 
934
- // Pokémon Showdown builds Rage at the wrong time for non-MultiHit moves
935
- if (late) try buildRage(battle, player.foe(), log);
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);
936
1028
 
937
1029
  if (!move.effect.isSpecial()) {
938
1030
  // On the cartridge Rage is not considered to be "special" and thus gets executed for a
@@ -945,7 +1037,9 @@ fn doMove(
945
1037
  if (move.effect == .Twineedle) {
946
1038
  if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), log);
947
1039
  } else if (move.effect == .Disable) {
948
- 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;
949
1043
  } else {
950
1044
  try moveEffect(battle, player, move, log);
951
1045
  }
@@ -963,8 +1057,7 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
963
1057
  chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
964
1058
  chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
965
1059
 
966
- if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
967
- return std.math.rotl(u8, battle.rng.next(), 3) < chance;
1060
+ return Rolls.criticalHit(battle, chance);
968
1061
  }
969
1062
 
970
1063
  fn calcDamage(
@@ -1068,50 +1161,36 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
1068
1161
 
1069
1162
  fn randomizeDamage(battle: anytype) void {
1070
1163
  if (battle.last_damage <= 1) return;
1071
-
1072
- const random = if (showdown)
1073
- battle.rng.range(u8, 217, 256)
1074
- else loop: {
1075
- while (true) {
1076
- const r = std.math.rotr(u8, battle.rng.next(), 1);
1077
- if (r >= 217) break :loop r;
1078
- }
1079
- };
1080
-
1164
+ const random = Rolls.damage(battle);
1081
1165
  battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
1082
1166
  }
1083
1167
 
1084
1168
  fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
1085
1169
  const side = battle.side(player);
1170
+ const foe = battle.foe(player);
1086
1171
 
1087
1172
  if (!try checkHit(battle, player, move, log)) return null;
1088
1173
 
1089
1174
  battle.last_damage = switch (side.last_selected_move) {
1090
- .SuperFang => @max(battle.foe(player).stored().hp / 2, 1),
1175
+ .SuperFang => @max(foe.stored().hp / 2, 1),
1091
1176
  .SeismicToss, .NightShade => side.stored().level,
1092
1177
  .SonicBoom => 20,
1093
1178
  .DragonRage => 40,
1094
1179
  // GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
1095
1180
  .Psywave => power: {
1096
1181
  const max = @intCast(u8, @as(u16, side.stored().level) * 3 / 2);
1097
- if (showdown) {
1098
- break :power battle.rng.range(u8, 0, max);
1099
- } else {
1100
- // GLITCH: Psywave infinite glitch loop
1101
- if (max <= 1) return Result.Error;
1102
- while (true) {
1103
- const r = battle.rng.next();
1104
- if (r < max) break :power r;
1105
- }
1106
- }
1182
+ // GLITCH: Psywave infinite glitch loop
1183
+ if (!showdown and max <= 1) return Result.Error;
1184
+ break :power Rolls.psywave(battle, max);
1107
1185
  },
1108
1186
  else => unreachable,
1109
1187
  };
1110
1188
 
1111
1189
  if (battle.last_damage == 0) return if (showdown) null else Result.Error;
1112
1190
 
1191
+ const sub = showdown and foe.active.volatiles.Substitute;
1113
1192
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
1114
- try buildRage(battle, player.foe(), log);
1193
+ if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
1115
1194
 
1116
1195
  return null;
1117
1196
  }
@@ -1124,19 +1203,17 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
1124
1203
  return null;
1125
1204
  }
1126
1205
 
1127
- // Pretend Counter was used as a stand-in when no move has been used to fail below with 0 BP
1128
- const foe_last_used_move =
1129
- 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
1130
1207
  const foe_last_selected_move =
1131
1208
  Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
1132
- .Counter
1209
+ .Splash
1133
1210
  else
1134
1211
  foe.last_selected_move);
1135
1212
 
1136
- const used = foe_last_used_move.bp > 0 and
1137
- foe.last_used_move != .Counter and
1138
- (foe_last_used_move.type == .Normal or
1139
- 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;
1140
1217
 
1141
1218
  const selected = foe_last_selected_move.bp > 0 and
1142
1219
  foe.last_selected_move != .Counter and
@@ -1155,12 +1232,14 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
1155
1232
  return null;
1156
1233
  }
1157
1234
 
1158
- battle.last_damage = if (battle.last_damage > 0x7FFF) 0xFFFF else battle.last_damage * 2;
1235
+ battle.last_damage *|= 2;
1159
1236
 
1160
1237
  // Pokémon Showdown calls checkHit before Counter
1161
1238
  if (!showdown and !try checkHit(battle, player, move, log)) return null;
1162
1239
 
1240
+ const sub = showdown and foe.active.volatiles.Substitute;
1163
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);
1164
1243
  return null;
1165
1244
  }
1166
1245
 
@@ -1204,6 +1283,7 @@ fn mirrorMove(
1204
1283
  battle: anytype,
1205
1284
  player: Player,
1206
1285
  mslot: u4,
1286
+ rewrap: bool,
1207
1287
  auto: bool,
1208
1288
  residual: *bool,
1209
1289
  log: anytype,
@@ -1216,41 +1296,30 @@ fn mirrorMove(
1216
1296
  if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
1217
1297
  try log.fail(battle.active(player), .None);
1218
1298
  return null;
1299
+ } else if (!showdown or foe.last_used_move != .Struggle) {
1300
+ incrementPP(side, mslot);
1219
1301
  }
1220
1302
 
1221
- incrementPP(side, mslot);
1222
-
1223
1303
  if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
1224
- return doMove(battle, player, mslot, auto, residual, log);
1304
+ return doMove(battle, player, mslot, rewrap, auto, residual, log);
1225
1305
  }
1226
1306
 
1227
1307
  fn metronome(
1228
1308
  battle: anytype,
1229
1309
  player: Player,
1230
1310
  mslot: u4,
1311
+ rewrap: bool,
1231
1312
  auto: bool,
1232
1313
  residual: *bool,
1233
1314
  log: anytype,
1234
1315
  ) !?Result {
1235
1316
  var side = battle.side(player);
1236
1317
 
1237
- side.last_selected_move = if (showdown) blk: {
1238
- const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
1239
- const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
1240
- break :blk @intToEnum(Move, r + mod);
1241
- } else loop: {
1242
- while (true) {
1243
- const r = battle.rng.next();
1244
- if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
1245
- if (r >= @enumToInt(Move.Struggle)) continue;
1246
- break :loop @intToEnum(Move, r);
1247
- }
1248
- };
1249
-
1318
+ side.last_selected_move = Rolls.metronome(battle);
1250
1319
  incrementPP(side, mslot);
1251
1320
 
1252
1321
  if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
1253
- return doMove(battle, player, mslot, auto, residual, log);
1322
+ return doMove(battle, player, mslot, rewrap, auto, residual, log);
1254
1323
  }
1255
1324
 
1256
1325
  fn checkHit(battle: anytype, player: Player, move: Move.Data, log: anytype) !bool {
@@ -1304,23 +1373,24 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
1304
1373
  }
1305
1374
 
1306
1375
  // GLITCH: Thrash / Petal Dance / Rage get their accuracy overwritten on subsequent hits
1307
- var state = &side.active.volatiles.state;
1308
- const overwritten = (move.effect == .Thrashing or move.effect == .Rage) and state.* > 0;
1309
- assert(!overwritten or (0 < state.* and state.* <= 255 and !side.active.volatiles.Bide));
1310
- var accuracy = if (overwritten) state.* else @as(u16, Gen12.percent(move.accuracy));
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));
1311
1382
  var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
1312
1383
  accuracy = accuracy * boost[0] / boost[1];
1313
1384
  boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
1314
1385
  accuracy = accuracy * boost[0] / boost[1];
1315
1386
  accuracy = @min(255, @max(1, accuracy));
1316
1387
 
1317
- 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;
1318
1391
 
1319
1392
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1320
- break :miss if (showdown)
1321
- !battle.rng.chance(u8, @intCast(u8, accuracy), 256)
1322
- else
1323
- battle.rng.next() >= accuracy;
1393
+ break :miss !Rolls.hit(battle, accuracy);
1324
1394
  };
1325
1395
 
1326
1396
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1435,6 +1505,15 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1435
1505
  }
1436
1506
 
1437
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
+
1438
1517
  var damage = @max(stored.stats.hp / 16, 1);
1439
1518
 
1440
1519
  // GLITCH: Leech Seed + Toxic glitch
@@ -1446,10 +1525,6 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1446
1525
  const amount = @min(damage, stored.hp);
1447
1526
  stored.hp -= amount;
1448
1527
 
1449
- var foe = battle.foe(player);
1450
- var foe_stored = foe.stored();
1451
- const foe_ident = battle.active(player.foe());
1452
-
1453
1528
  // As above, Pokémon Showdown uses damageOf but its not relevant
1454
1529
  if (amount > 0) try log.damage(ident, stored, .LeechSeed);
1455
1530
 
@@ -1465,13 +1540,13 @@ fn endTurn(battle: anytype, log: anytype) @TypeOf(log).Error!Result {
1465
1540
  assert(!battle.side(.P1).active.volatiles.MultiHit);
1466
1541
  assert(!battle.side(.P2).active.volatiles.MultiHit);
1467
1542
 
1543
+ battle.turn += 1;
1544
+
1468
1545
  if (showdown and options.ebc and checkEBC(battle)) {
1469
1546
  try log.tie();
1470
1547
  return Result.Tie;
1471
1548
  }
1472
1549
 
1473
- battle.turn += 1;
1474
-
1475
1550
  if (showdown and battle.turn >= 1000) {
1476
1551
  try log.tie();
1477
1552
  return Result.Tie;
@@ -1538,6 +1613,42 @@ fn checkEBC(battle: anytype) bool {
1538
1613
  return false;
1539
1614
  }
1540
1615
 
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
+
1541
1652
  inline fn onBegin(
1542
1653
  battle: anytype,
1543
1654
  player: Player,
@@ -1628,10 +1739,7 @@ pub const Effects = struct {
1628
1739
  side.active.volatiles.Bide = true;
1629
1740
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1630
1741
  side.active.volatiles.state = 0;
1631
- side.active.volatiles.attacks = @intCast(u3, if (showdown)
1632
- battle.rng.range(u4, 2, 4)
1633
- else
1634
- (battle.rng.next() & 1) + 2);
1742
+ side.active.volatiles.attacks = Rolls.bideThrashDuration(battle);
1635
1743
 
1636
1744
  try log.start(battle.active(player), .Bide);
1637
1745
  }
@@ -1653,14 +1761,8 @@ pub const Effects = struct {
1653
1761
  return;
1654
1762
  }
1655
1763
 
1656
- const chance = !foe.active.types.includes(move.type) and if (showdown)
1657
- battle.rng.chance(u8, @as(u8, if (move.effect == .BurnChance1) 26 else 77), 256)
1658
- else
1659
- battle.rng.next() < 1 + (if (move.effect == .BurnChance1)
1660
- Gen12.percent(10)
1661
- else
1662
- Gen12.percent(30));
1663
- if (!chance) return;
1764
+ if (foe.active.types.includes(move.type)) return;
1765
+ if (!Rolls.secondaryChance(battle, move.effect == .BurnChance1)) return;
1664
1766
 
1665
1767
  foe_stored.status = Status.init(.BRN);
1666
1768
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
@@ -1684,11 +1786,7 @@ pub const Effects = struct {
1684
1786
  const sub = foe.active.volatiles.Substitute;
1685
1787
 
1686
1788
  if (move.effect == .ConfusionChance) {
1687
- const chance = if (showdown)
1688
- battle.rng.chance(u8, 25, 256)
1689
- else
1690
- battle.rng.next() < Gen12.percent(10);
1691
- if (!chance) return;
1789
+ if (!Rolls.confusionChance(battle)) return;
1692
1790
  } else {
1693
1791
  if (showdown) {
1694
1792
  if (!try checkHit(battle, player, move, log)) {
@@ -1707,10 +1805,7 @@ pub const Effects = struct {
1707
1805
 
1708
1806
  if (foe.active.volatiles.Confusion) return;
1709
1807
  foe.active.volatiles.Confusion = true;
1710
- foe.active.volatiles.confusion = @intCast(u3, if (showdown)
1711
- battle.rng.range(u8, 2, 6)
1712
- else
1713
- (battle.rng.next() & 3) + 2);
1808
+ foe.active.volatiles.confusion = Rolls.confusionDuration(battle);
1714
1809
 
1715
1810
  try log.start(battle.active(player.foe()), .Confusion);
1716
1811
  }
@@ -1760,11 +1855,8 @@ pub const Effects = struct {
1760
1855
  return Result.Error;
1761
1856
  }
1762
1857
 
1763
- volatiles.disabled_move = @intCast(u3, randomMoveSlot(&battle.rng, &foe.active.moves, n));
1764
- volatiles.disabled_duration = @intCast(u4, if (showdown)
1765
- battle.rng.range(u8, 1, 9)
1766
- else
1767
- (battle.rng.next() & 7) + 1);
1858
+ volatiles.disabled_move = @intCast(u3, Rolls.moveSlot(battle, &foe.active.moves, n));
1859
+ volatiles.disabled_duration = Rolls.disableDuration(battle);
1768
1860
 
1769
1861
  try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
1770
1862
  return null;
@@ -1776,7 +1868,7 @@ pub const Effects = struct {
1776
1868
 
1777
1869
  if (battle.last_damage == 0) {
1778
1870
  assert(showdown);
1779
- return;
1871
+ if (!battle.foe(player).active.volatiles.Substitute) return;
1780
1872
  }
1781
1873
 
1782
1874
  const drain = @max(battle.last_damage / 2, 1);
@@ -1803,15 +1895,7 @@ pub const Effects = struct {
1803
1895
  var volatiles = &battle.foe(player).active.volatiles;
1804
1896
 
1805
1897
  if (volatiles.Substitute) return;
1806
-
1807
- const chance = if (showdown)
1808
- battle.rng.chance(u8, @as(u8, if (move.effect == .FlinchChance1) 26 else 77), 256)
1809
- else
1810
- battle.rng.next() < 1 + (if (move.effect == .FlinchChance1)
1811
- Gen12.percent(10)
1812
- else
1813
- Gen12.percent(30));
1814
- if (!chance) return;
1898
+ if (!Rolls.secondaryChance(battle, move.effect == .FlinchChance1)) return;
1815
1899
 
1816
1900
  volatiles.Flinch = true;
1817
1901
  volatiles.Recharging = false;
@@ -1837,12 +1921,8 @@ pub const Effects = struct {
1837
1921
  return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
1838
1922
  }
1839
1923
 
1840
- const chance = !foe.active.types.includes(move.type) and if (showdown)
1841
- battle.rng.chance(u8, 26, 256)
1842
- else
1843
- battle.rng.next() < 1 + Gen12.percent(10);
1844
- if (!chance) return;
1845
-
1924
+ if (foe.active.types.includes(move.type)) return;
1925
+ if (!Rolls.secondaryChance(battle, true)) return;
1846
1926
  // Freeze Clause Mod
1847
1927
  if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1848
1928
 
@@ -1979,15 +2059,15 @@ pub const Effects = struct {
1979
2059
  }
1980
2060
  break :has_mimic false;
1981
2061
  };
1982
- // If the foe is Invulnerable we still want to fall through to checkHit to be able to
1983
- // trigger |-miss| instead of |-fail|
1984
- if (!has_mimic and !foe.active.volatiles.Invulnerable) {
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;
1985
2065
  return try log.fail(battle.active(player.foe()), .None);
1986
2066
  }
1987
2067
  }
1988
2068
  if (!try checkHit(battle, player, move, log)) return;
1989
2069
 
1990
- const rslot = randomMoveSlot(&battle.rng, &foe.active.moves, 0);
2070
+ const rslot = Rolls.moveSlot(battle, &foe.active.moves, 0);
1991
2071
  side.active.move(oslot).id = foe.active.move(rslot).id;
1992
2072
 
1993
2073
  try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
@@ -2008,7 +2088,8 @@ pub const Effects = struct {
2008
2088
  assert(!side.active.volatiles.MultiHit);
2009
2089
  side.active.volatiles.MultiHit = true;
2010
2090
 
2011
- 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;
2012
2093
  }
2013
2094
 
2014
2095
  fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2052,14 +2133,8 @@ pub const Effects = struct {
2052
2133
  }
2053
2134
 
2054
2135
  // Body Slam can't paralyze a Normal type Pokémon
2055
- const chance = !foe.active.types.includes(move.type) and if (showdown)
2056
- battle.rng.chance(u8, @as(u8, if (move.effect == .ParalyzeChance1) 26 else 77), 256)
2057
- else
2058
- battle.rng.next() < 1 + (if (move.effect == .ParalyzeChance1)
2059
- Gen12.percent(10)
2060
- else
2061
- Gen12.percent(30));
2062
- if (!chance) return;
2136
+ if (foe.active.types.includes(move.type)) return;
2137
+ if (!Rolls.secondaryChance(battle, move.effect == .ParalyzeChance1)) return;
2063
2138
 
2064
2139
  foe_stored.status = Status.init(.PAR);
2065
2140
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
@@ -2102,16 +2177,7 @@ pub const Effects = struct {
2102
2177
 
2103
2178
  if (move.effect == .Poison) {
2104
2179
  if (!showdown and !try checkHit(battle, player, move, log)) return;
2105
- } else {
2106
- const chance = if (showdown)
2107
- battle.rng.chance(u8, @as(u8, if (move.effect == .PoisonChance1) 52 else 103), 256)
2108
- else
2109
- battle.rng.next() < 1 + (if (move.effect == .PoisonChance1)
2110
- Gen12.percent(20)
2111
- else
2112
- Gen12.percent(40));
2113
- if (!chance) return;
2114
- }
2180
+ } else if (!Rolls.poisonChance(battle, move.effect == .PoisonChance1)) return;
2115
2181
 
2116
2182
  foe_stored.status = Status.init(.PSN);
2117
2183
  if (toxic) {
@@ -2125,10 +2191,8 @@ pub const Effects = struct {
2125
2191
 
2126
2192
  fn rage(battle: anytype, player: Player) !void {
2127
2193
  var volatiles = &battle.side(player).active.volatiles;
2128
- if (volatiles.Rage) return;
2129
- volatiles.Rage = true;
2130
2194
  assert(!volatiles.Bide);
2131
- volatiles.state = 0;
2195
+ volatiles.Rage = true;
2132
2196
  }
2133
2197
 
2134
2198
  fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
@@ -2180,16 +2244,7 @@ pub const Effects = struct {
2180
2244
  }
2181
2245
  foe.active.volatiles.Recharging = false;
2182
2246
 
2183
- const duration = @intCast(u3, if (showdown)
2184
- battle.rng.range(u8, 1, 8)
2185
- else loop: {
2186
- while (true) {
2187
- const r = battle.rng.next() & 7;
2188
- if (r != 0) break :loop r;
2189
- }
2190
- });
2191
-
2192
- foe_stored.status = Status.slp(duration);
2247
+ foe_stored.status = Status.slp(Rolls.sleepDuration(battle));
2193
2248
  try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
2194
2249
  }
2195
2250
 
@@ -2231,8 +2286,9 @@ pub const Effects = struct {
2231
2286
  fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
2232
2287
  if (!showdown or battle.side(player).last_selected_move == .Teleport) return;
2233
2288
 
2234
- // Whirlwind/Roar should not roll to hit but Pokémon Showdown does anyway
2289
+ // Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
2235
2290
  _ = try checkHit(battle, player, move, log);
2291
+ battle.last_damage = 0;
2236
2292
  }
2237
2293
 
2238
2294
  fn thrashing(battle: anytype, player: Player) void {
@@ -2241,11 +2297,7 @@ pub const Effects = struct {
2241
2297
  assert(!volatiles.Bide);
2242
2298
 
2243
2299
  volatiles.Thrashing = true;
2244
- volatiles.state = 0;
2245
- volatiles.attacks = @intCast(u3, if (showdown)
2246
- battle.rng.range(u8, 2, 4)
2247
- else
2248
- (battle.rng.next() & 1) + 2);
2300
+ volatiles.attacks = Rolls.bideThrashDuration(battle);
2249
2301
  }
2250
2302
 
2251
2303
  fn transform(battle: anytype, player: Player, log: anytype) !void {
@@ -2277,7 +2329,7 @@ pub const Effects = struct {
2277
2329
  try log.transform(battle.active(player), foe_ident);
2278
2330
  }
2279
2331
 
2280
- fn binding(battle: anytype, player: Player) void {
2332
+ fn binding(battle: anytype, player: Player, rewrap: bool) void {
2281
2333
  var side = battle.side(player);
2282
2334
  var foe = battle.foe(player);
2283
2335
 
@@ -2285,9 +2337,11 @@ pub const Effects = struct {
2285
2337
  side.active.volatiles.Binding = true;
2286
2338
  // GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
2287
2339
  // (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
2288
- if (!showdown) foe.active.volatiles.Recharging = false;
2340
+ if (!showdown) {
2341
+ foe.active.volatiles.Recharging = false;
2342
+ } else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
2289
2343
 
2290
- side.active.volatiles.attacks = distribution(battle) - 1;
2344
+ side.active.volatiles.attacks = Rolls.distribution(battle) - 1;
2291
2345
  }
2292
2346
 
2293
2347
  fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2314,9 +2368,11 @@ pub const Effects = struct {
2314
2368
  return try log.fail(ident, .None);
2315
2369
  }
2316
2370
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2317
- const stat = unmodifiedStats(battle, player).atk;
2371
+ const stat = unmodifiedStats(battle, side).atk;
2318
2372
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2319
2373
  try log.boost(ident, reason, n);
2374
+ // Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
2375
+ if (showdown and move.effect == .Rage) return;
2320
2376
  },
2321
2377
  .DefenseUp1, .DefenseUp2 => {
2322
2378
  assert(boosts.def >= -6 and boosts.def <= 6);
@@ -2332,7 +2388,7 @@ pub const Effects = struct {
2332
2388
  return try log.fail(ident, .None);
2333
2389
  }
2334
2390
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2335
- const stat = unmodifiedStats(battle, player).def;
2391
+ const stat = unmodifiedStats(battle, side).def;
2336
2392
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2337
2393
  try log.boost(ident, .Defense, n);
2338
2394
  },
@@ -2349,7 +2405,7 @@ pub const Effects = struct {
2349
2405
  return try log.fail(ident, .None);
2350
2406
  }
2351
2407
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2352
- const stat = unmodifiedStats(battle, player).spe;
2408
+ const stat = unmodifiedStats(battle, side).spe;
2353
2409
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2354
2410
  try log.boost(ident, .Speed, 2);
2355
2411
  },
@@ -2369,7 +2425,7 @@ pub const Effects = struct {
2369
2425
  return try log.fail(ident, .None);
2370
2426
  }
2371
2427
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2372
- const stat = unmodifiedStats(battle, player).spc;
2428
+ const stat = unmodifiedStats(battle, side).spc;
2373
2429
  stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2374
2430
  try log.boost(ident, .SpecialAttack, n);
2375
2431
  try log.boost(ident, .SpecialDefense, n);
@@ -2396,11 +2452,7 @@ pub const Effects = struct {
2396
2452
  }
2397
2453
 
2398
2454
  if (move.effect.isStatDownChance()) {
2399
- const chance = if (showdown)
2400
- battle.rng.chance(u8, 85, 256)
2401
- else
2402
- battle.rng.next() < Gen12.percent(33) + 1;
2403
- if (!chance or foe.active.volatiles.Invulnerable) return;
2455
+ if (!Rolls.unboost(battle) or foe.active.volatiles.Invulnerable) return;
2404
2456
  } else if (!showdown and !try checkHit(battle, player, move, log)) {
2405
2457
  return; // checkHit already checks for Invulnerable
2406
2458
  }
@@ -2422,7 +2474,7 @@ pub const Effects = struct {
2422
2474
  return try log.fail(foe_ident, .None);
2423
2475
  }
2424
2476
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2425
- const stat = unmodifiedStats(battle, player.foe()).atk;
2477
+ const stat = unmodifiedStats(battle, foe).atk;
2426
2478
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2427
2479
  try log.boost(foe_ident, .Attack, -1);
2428
2480
  },
@@ -2440,7 +2492,7 @@ pub const Effects = struct {
2440
2492
  return try log.fail(foe_ident, .None);
2441
2493
  }
2442
2494
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2443
- const stat = unmodifiedStats(battle, player.foe()).def;
2495
+ const stat = unmodifiedStats(battle, foe).def;
2444
2496
  stats.def = @max(1, stat * mod[0] / mod[1]);
2445
2497
  try log.boost(foe_ident, .Defense, -@as(i8, n));
2446
2498
  },
@@ -2457,7 +2509,7 @@ pub const Effects = struct {
2457
2509
  return try log.fail(foe_ident, .None);
2458
2510
  }
2459
2511
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2460
- const stat = unmodifiedStats(battle, player.foe()).spe;
2512
+ const stat = unmodifiedStats(battle, foe).spe;
2461
2513
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2462
2514
  try log.boost(foe_ident, .Speed, -1);
2463
2515
  assert(boosts.spe >= -6);
@@ -2477,7 +2529,7 @@ pub const Effects = struct {
2477
2529
  return try log.fail(foe_ident, .None);
2478
2530
  }
2479
2531
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2480
- const stat = unmodifiedStats(battle, player.foe()).spc;
2532
+ const stat = unmodifiedStats(battle, foe).spc;
2481
2533
  stats.spc = @max(1, stat * mod[0] / mod[1]);
2482
2534
  try log.boost(foe_ident, .SpecialAttack, -1);
2483
2535
  try log.boost(foe_ident, .SpecialDefense, -1);
@@ -2496,8 +2548,7 @@ pub const Effects = struct {
2496
2548
  }
2497
2549
  };
2498
2550
 
2499
- fn unmodifiedStats(battle: anytype, who: Player) *Stats(u16) {
2500
- const side = battle.side(who);
2551
+ fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
2501
2552
  if (!side.active.volatiles.Transform) return &side.stored().stats;
2502
2553
  const id = ID.from(side.active.volatiles.transform);
2503
2554
  return &battle.side(id.player).pokemon[id.id - 1].stats;
@@ -2563,75 +2614,140 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
2563
2614
  }
2564
2615
  }
2565
2616
 
2566
- fn disabled(side: *Side, ident: ID, log: anytype) !bool {
2567
- if (side.active.volatiles.disabled_move != 0) {
2568
- // A Pokémon that transforms after being disabled may end up with less move slots
2569
- const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
2570
- if (m != .None and m == side.last_selected_move) {
2571
- side.active.volatiles.Charging = false;
2572
- try log.disabled(ident, side.last_selected_move);
2573
- return true;
2617
+ pub const Rolls = struct {
2618
+ inline fn speedTie(battle: anytype) bool {
2619
+ if (showdown) return battle.rng.range(u8, 0, 2) == 0;
2620
+ return battle.rng.next() < Gen12.percent(50) + 1;
2621
+ }
2622
+
2623
+ inline fn criticalHit(battle: anytype, chance: u16) bool {
2624
+ if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
2625
+ return std.math.rotl(u8, battle.rng.next(), 3) < chance;
2626
+ }
2627
+
2628
+ inline fn damage(battle: anytype) u8 {
2629
+ if (showdown) return battle.rng.range(u8, 217, 256);
2630
+ while (true) {
2631
+ const r = std.math.rotr(u8, battle.rng.next(), 1);
2632
+ if (r >= 217) return r;
2574
2633
  }
2575
2634
  }
2576
- return false;
2577
- }
2578
2635
 
2579
- inline fn buildRage(battle: anytype, who: Player, log: anytype) !void {
2580
- const side = battle.side(who);
2581
- if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
2582
- try Effects.boost(battle, who, Move.get(.Rage), log);
2636
+ inline fn hit(battle: anytype, accuracy: u16) bool {
2637
+ if (showdown) return battle.rng.chance(u8, @intCast(u8, accuracy), 256);
2638
+ return battle.rng.next() < accuracy;
2583
2639
  }
2584
- }
2585
2640
 
2586
- fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
2587
- var volatiles = &active.volatiles;
2588
- assert(volatiles.Thrashing);
2589
- if (volatiles.attacks > 0) return false;
2641
+ inline fn confusionDuration(battle: anytype) u3 {
2642
+ if (showdown) return @intCast(u3, battle.rng.range(u8, 2, 6));
2643
+ return @intCast(u3, (battle.rng.next() & 3) + 2);
2644
+ }
2590
2645
 
2591
- volatiles.Thrashing = false;
2592
- volatiles.Confusion = true;
2593
- volatiles.confusion = @intCast(u3, if (showdown)
2594
- battle.rng.range(u8, 2, 6)
2595
- else
2596
- (battle.rng.next() & 3) + 2);
2646
+ inline fn confused(battle: anytype) bool {
2647
+ if (showdown) return !battle.rng.chance(u8, 128, 256);
2648
+ return battle.rng.next() >= Gen12.percent(50) + 1;
2649
+ }
2597
2650
 
2598
- return true;
2599
- }
2651
+ inline fn paralyzed(battle: anytype) bool {
2652
+ if (showdown) return battle.rng.chance(u8, 63, 256);
2653
+ return battle.rng.next() < Gen12.percent(25);
2654
+ }
2600
2655
 
2601
- const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
2656
+ inline fn confusionChance(battle: anytype) bool {
2657
+ if (showdown) return battle.rng.chance(u8, 25, 256);
2658
+ return battle.rng.next() < Gen12.percent(10);
2659
+ }
2602
2660
 
2603
- fn distribution(battle: anytype) u3 {
2604
- if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
2605
- const r = (battle.rng.next() & 3);
2606
- return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
2607
- }
2661
+ inline fn secondaryChance(battle: anytype, low: bool) bool {
2662
+ if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 26 else 77), 256);
2663
+ return battle.rng.next() < 1 + (if (low) Gen12.percent(10) else Gen12.percent(30));
2664
+ }
2608
2665
 
2609
- fn randomMoveSlot(rand: anytype, moves: []MoveSlot, check_pp: u4) u4 {
2610
- if (showdown) {
2611
- if (check_pp == 0) {
2612
- var i: usize = moves.len;
2613
- while (i > 0) {
2614
- i -= 1;
2615
- if (moves[i].id != .None) return rand.range(u4, 0, @intCast(u4, i + 1)) + 1;
2616
- }
2617
- } else {
2618
- var r = rand.range(u4, 0, @intCast(u4, check_pp)) + 1;
2619
- var i: usize = 0;
2620
- while (i < moves.len and r > 0) : (i += 1) {
2621
- if (moves[i].pp > 0) {
2622
- r -= 1;
2623
- if (r == 0) break;
2666
+ inline fn poisonChance(battle: anytype, low: bool) bool {
2667
+ if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 52 else 103), 256);
2668
+ return battle.rng.next() < 1 + (if (low) Gen12.percent(20) else Gen12.percent(40));
2669
+ }
2670
+
2671
+ inline fn sleepDuration(battle: anytype) u3 {
2672
+ if (showdown) return @intCast(u3, battle.rng.range(u8, 1, 8));
2673
+ while (true) {
2674
+ const r = battle.rng.next() & 7;
2675
+ if (r != 0) return @intCast(u3, r);
2676
+ }
2677
+ }
2678
+
2679
+ inline fn bideThrashDuration(battle: anytype) u3 {
2680
+ if (showdown) return @intCast(u3, battle.rng.range(u4, 2, 4));
2681
+ return @intCast(u3, (battle.rng.next() & 1) + 2);
2682
+ }
2683
+
2684
+ fn moveSlot(battle: anytype, moves: []MoveSlot, check_pp: u4) u4 {
2685
+ if (showdown) {
2686
+ if (check_pp == 0) {
2687
+ var i: usize = moves.len;
2688
+ while (i > 0) {
2689
+ i -= 1;
2690
+ if (moves[i].id != .None) {
2691
+ return battle.rng.range(u4, 0, @intCast(u4, i + 1)) + 1;
2692
+ }
2624
2693
  }
2694
+ } else {
2695
+ var r = battle.rng.range(u4, 0, @intCast(u4, check_pp)) + 1;
2696
+ var i: usize = 0;
2697
+ while (i < moves.len and r > 0) : (i += 1) {
2698
+ if (moves[i].pp > 0) {
2699
+ r -= 1;
2700
+ if (r == 0) break;
2701
+ }
2702
+ }
2703
+ return @intCast(u4, i + 1);
2625
2704
  }
2626
- return @intCast(u4, i + 1);
2705
+ }
2706
+
2707
+ while (true) {
2708
+ const r = @intCast(u4, battle.rng.next() & 3);
2709
+ if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
2627
2710
  }
2628
2711
  }
2629
2712
 
2630
- while (true) {
2631
- const r = @intCast(u4, rand.next() & 3);
2632
- if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
2713
+ inline fn disableDuration(battle: anytype) u4 {
2714
+ if (showdown) return @intCast(u4, battle.rng.range(u8, 1, 9));
2715
+ return @intCast(u4, (battle.rng.next() & 7) + 1);
2633
2716
  }
2634
- }
2717
+
2718
+ inline fn metronome(battle: anytype) Move {
2719
+ if (showdown) {
2720
+ const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
2721
+ const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
2722
+ return @intToEnum(Move, r + mod);
2723
+ }
2724
+ while (true) {
2725
+ const r = battle.rng.next();
2726
+ if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
2727
+ if (r >= @enumToInt(Move.Struggle)) continue;
2728
+ return @intToEnum(Move, r);
2729
+ }
2730
+ }
2731
+
2732
+ inline fn psywave(battle: anytype, max: u8) u8 {
2733
+ if (showdown) return battle.rng.range(u8, 0, max);
2734
+ while (true) {
2735
+ const r = battle.rng.next();
2736
+ if (r < max) return r;
2737
+ }
2738
+ }
2739
+
2740
+ inline fn distribution(battle: anytype) u3 {
2741
+ if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
2742
+ const r = (battle.rng.next() & 3);
2743
+ return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
2744
+ }
2745
+
2746
+ inline fn unboost(battle: anytype) bool {
2747
+ if (showdown) return battle.rng.chance(u8, 85, 256);
2748
+ return battle.rng.next() < Gen12.percent(33) + 1;
2749
+ }
2750
+ };
2635
2751
 
2636
2752
  test "RNG agreement" {
2637
2753
  if (!showdown) return;
@@ -2731,9 +2847,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
2731
2847
  while (slot <= 4) : (slot += 1) {
2732
2848
  const m = active.moves[slot - 1];
2733
2849
  if (m.id == .None) break;
2734
- if (m.id == side.last_selected_move) {
2735
- // Pokémon Showdown displays Struggle if limited to Bide when out of PP
2736
- const s = if (m.pp == 0 and m.id == .Bide) 0 else slot;
2850
+ if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
2851
+ // Pokémon Showdown displays Struggle if limited to Bide but unable to pick
2852
+ const struggle =
2853
+ m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);
2854
+ const s = if (struggle) 0 else slot;
2737
2855
  out[n] = .{ .type = .Move, .data = s };
2738
2856
  n += 1;
2739
2857
  return n;