@pkmn/engine 0.1.0-dev.550c3889 → 0.1.0-dev.5ec64ea5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -56,34 +56,30 @@ const BOOSTS = &[_][2]u8{
56
56
 
57
57
  const MAX_STAT_VALUE = 999;
58
58
 
59
- const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
60
-
61
59
  pub fn update(battle: anytype, c1: Choice, c2: Choice, log: anytype) !Result {
62
60
  assert(c1.type != .Pass or c2.type != .Pass or battle.turn == 0);
63
61
  if (battle.turn == 0) return start(battle, log);
64
62
 
65
- var s1 = false;
66
- var s2 = false;
67
63
  var f1: ?Move = null;
68
64
  var f2: ?Move = null;
69
65
 
70
- if (selectMove(battle, .P1, c1, c2, &s1, &f1)) |r| return r;
71
- if (selectMove(battle, .P2, c2, c1, &s2, &f2)) |r| return r;
72
-
73
- var p1 = battle.side(.P1);
74
- var p2 = battle.side(.P2);
75
-
76
- var r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
77
- var r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
66
+ if (selectMove(battle, .P1, c1, c2, &f1)) |r| return r;
67
+ if (selectMove(battle, .P2, c2, c1, &f2)) |r| return r;
78
68
 
79
69
  if (turnOrder(battle, c1, c2) == .P1) {
80
- if (try doTurn(battle, .P1, c1, r1, s1, f1, .P2, c2, r2, s2, f2, log)) |r| return r;
70
+ if (try doTurn(battle, .P1, c1, f1, .P2, c2, f2, log)) |r| return r;
81
71
  } else {
82
- if (try doTurn(battle, .P2, c2, r2, s2, f2, .P1, c1, r1, s1, f1, log)) |r| return r;
72
+ if (try doTurn(battle, .P2, c2, f2, .P1, c1, f1, log)) |r| return r;
83
73
  }
84
74
 
85
- if (p1.active.volatiles.attacks == 0) p1.active.volatiles.Binding = false;
86
- if (p2.active.volatiles.attacks == 0) p2.active.volatiles.Binding = false;
75
+ var p1 = battle.side(.P1);
76
+ if (p1.active.volatiles.attacks == 0) {
77
+ p1.active.volatiles.Binding = false;
78
+ }
79
+ var p2 = battle.side(.P2);
80
+ if (p2.active.volatiles.attacks == 0) {
81
+ p2.active.volatiles.Binding = false;
82
+ }
87
83
 
88
84
  return endTurn(battle, log);
89
85
  }
@@ -116,7 +112,6 @@ fn selectMove(
116
112
  player: Player,
117
113
  choice: Choice,
118
114
  foe_choice: Choice,
119
- skip_turn: *bool,
120
115
  from: *?Move,
121
116
  ) ?Result {
122
117
  if (choice.type == .Pass) return null;
@@ -132,20 +127,14 @@ fn selectMove(
132
127
  if (volatiles.Recharging) return null;
133
128
  if (volatiles.Rage) {
134
129
  from.* = side.last_used_move;
135
- if (showdown) {
136
- if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
137
- saveMove(battle, player, null);
138
- }
130
+ if (showdown) saveMove(battle, player, null);
139
131
  return null;
140
132
  }
141
133
  // Pokémon Showdown removes Flinch at the end-of-turn in its residual handler
142
134
  if (!showdown) volatiles.Flinch = false;
143
135
  if (volatiles.Thrashing or volatiles.Charging) {
144
136
  from.* = side.last_selected_move;
145
- if (showdown) {
146
- if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
147
- saveMove(battle, player, null);
148
- }
137
+ if (showdown) saveMove(battle, player, null);
149
138
  return null;
150
139
  }
151
140
 
@@ -155,10 +144,7 @@ fn selectMove(
155
144
  // pre-move select
156
145
  if (Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP) or volatiles.Bide) {
157
146
  assert(showdown or choice.data == 0);
158
- if (showdown) {
159
- if (volatiles.Bide and battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
160
- saveMove(battle, player, choice);
161
- }
147
+ if (showdown) saveMove(battle, player, choice);
162
148
  return null;
163
149
  }
164
150
  if (volatiles.Binding) {
@@ -189,7 +175,6 @@ fn selectMove(
189
175
  }
190
176
 
191
177
  if (battle.foe(player).active.volatiles.Binding) {
192
- skip_turn.* = true;
193
178
  if (showdown) {
194
179
  saveMove(battle, player, choice);
195
180
  } else {
@@ -200,7 +185,6 @@ fn selectMove(
200
185
  }
201
186
 
202
187
  // move select
203
- volatiles.state = 0;
204
188
  if (choice.data == 0) {
205
189
  const struggle = ok: {
206
190
  for (side.active.moves, 0..) |move, i| {
@@ -317,7 +301,11 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
317
301
  // Pokémon Showdown's beforeTurnCallback shenanigans
318
302
  if (showdown and m1 == .Counter and m2 == .Counter) battle.rng.advance(1);
319
303
 
320
- const p1 = Rolls.speedTie(battle);
304
+ const p1 = if (showdown)
305
+ battle.rng.range(u8, 0, 2) == 0
306
+ else
307
+ battle.rng.next() < Gen12.percent(50) + 1;
308
+
321
309
  if (!showdown) return if (p1) .P1 else .P2;
322
310
 
323
311
  // Pokémon Showdown's "lockedmove" volatile's onBeforeTurn uses BattleQueue#changeAction,
@@ -327,7 +315,7 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
327
315
  const t1 = battle.side(.P1).active.volatiles.Thrashing;
328
316
  const t2 = battle.side(.P2).active.volatiles.Thrashing;
329
317
  // If *both* sides are thrashing it really should be another speed tie, but we've patched
330
- // that out and enforce host ordering of events, so P1 just goes first regardless of who
318
+ // that out and enforce host ordering of events, so P1 just goes first reqgardless of who
331
319
  // won the original coin flip
332
320
  if (t1 and t2) return .P1;
333
321
  return if (p1) if (t1 and !t2) .P2 else .P1 else if (t2 and !t1) .P1 else .P2;
@@ -340,13 +328,9 @@ fn doTurn(
340
328
  battle: anytype,
341
329
  player: Player,
342
330
  player_choice: Choice,
343
- player_rewrap: bool,
344
- player_skip: bool,
345
331
  player_from: ?Move,
346
332
  foe_player: Player,
347
333
  foe_choice: Choice,
348
- foe_rewrap: bool,
349
- foe_skip: bool,
350
334
  foe_from: ?Move,
351
335
  log: anytype,
352
336
  ) !?Result {
@@ -354,16 +338,7 @@ fn doTurn(
354
338
 
355
339
  var residual = true;
356
340
  var replace = battle.side(player).stored().hp == 0;
357
- if (try executeMove(
358
- battle,
359
- player,
360
- player_choice,
361
- player_rewrap,
362
- player_skip,
363
- player_from,
364
- &residual,
365
- log,
366
- )) |r| return r;
341
+ if (try executeMove(battle, player, player_choice, player_from, &residual, log)) |r| return r;
367
342
  if (!replace) {
368
343
  if (player_choice.type != .Switch) {
369
344
  if (try checkFaint(battle, foe_player, log)) |r| return r;
@@ -374,16 +349,7 @@ fn doTurn(
374
349
 
375
350
  residual = true;
376
351
  replace = battle.side(foe_player).stored().hp == 0;
377
- if (try executeMove(
378
- battle,
379
- foe_player,
380
- foe_choice,
381
- foe_rewrap,
382
- foe_skip,
383
- foe_from,
384
- &residual,
385
- log,
386
- )) |r| return r;
352
+ if (try executeMove(battle, foe_player, foe_choice, foe_from, &residual, log)) |r| return r;
387
353
  if (!replace) {
388
354
  if (foe_choice.type != .Switch) {
389
355
  if (try checkFaint(battle, player, log)) |r| return r;
@@ -406,8 +372,6 @@ fn executeMove(
406
372
  battle: anytype,
407
373
  player: Player,
408
374
  choice: Choice,
409
- rewrap: bool,
410
- skip: bool,
411
375
  from: ?Move,
412
376
  residual: *bool,
413
377
  log: anytype,
@@ -419,10 +383,9 @@ fn executeMove(
419
383
  return null;
420
384
  }
421
385
 
422
- // This is the correct place to check for SKIP_TURN and abort early, however since Pokémon
423
- // Showdown overwrites the SKIP_TURN sentinel with its botched move select we need to add an
424
- // additional skip boolean to accomplish the same thing in the Binding check of BeforeMove
425
386
  if (side.last_selected_move == .SKIP_TURN) {
387
+ // Pokémon Showdown overwrites the SKIP_TURN sentinel with its botched move select,
388
+ // Binding instead gets handled in beforeMove after sleep and freeze
426
389
  assert(!showdown);
427
390
  if (battle.foe(player).active.volatiles.Binding) {
428
391
  try log.cant(battle.active(player), .Bound);
@@ -472,7 +435,7 @@ fn executeMove(
472
435
 
473
436
  var skip_can = false;
474
437
  var skip_pp = false;
475
- switch (try beforeMove(battle, player, skip, from, residual, log)) {
438
+ switch (try beforeMove(battle, player, from, residual, log)) {
476
439
  .done => return null,
477
440
  .skip_can => skip_can = true,
478
441
  .skip_pp => skip_pp = true,
@@ -484,15 +447,14 @@ fn executeMove(
484
447
  return null;
485
448
  }
486
449
 
487
- return doMove(battle, player, mslot, rewrap, auto, residual, log);
450
+ return doMove(battle, player, mslot, auto, residual, log);
488
451
  }
489
452
 
490
- const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
453
+ const BeforeMove = union(enum) { done, skip_can, skip_pp, ok, err };
491
454
 
492
455
  fn beforeMove(
493
456
  battle: anytype,
494
457
  player: Player,
495
- skip: bool,
496
458
  from: ?Move,
497
459
  residual: *bool,
498
460
  log: anytype,
@@ -524,15 +486,13 @@ fn beforeMove(
524
486
  return .done;
525
487
  }
526
488
 
527
- if (skip or foe.active.volatiles.Binding) {
489
+ if (foe.active.volatiles.Binding) {
528
490
  try log.cant(ident, .Bound);
529
491
  return .done;
530
492
  }
531
493
 
532
- if (volatiles.Flinch) {
533
- // Pokémon Showdown doesn't clear Flinch until its imaginary "residual" phase, meaning
534
- // Pokémon can sometimes flinch multiple times from the same original hit
535
- if (!showdown) volatiles.Flinch = false;
494
+ if (!showdown and volatiles.Flinch) {
495
+ volatiles.Flinch = false;
536
496
  try log.cant(ident, .Flinch);
537
497
  return .done;
538
498
  }
@@ -553,6 +513,13 @@ fn beforeMove(
553
513
  // Pokémon Showdown's disable condition has a single onBeforeMove handler
554
514
  if (showdown and try disabled(side, ident, log)) return .done;
555
515
 
516
+ // Pokémon Showdown checks for Flinch *after* instead of before
517
+ if (showdown and volatiles.Flinch) {
518
+ volatiles.Flinch = false;
519
+ try log.cant(ident, .Flinch);
520
+ return .done;
521
+ }
522
+
556
523
  // This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
557
524
  // before the side had a selected a move - we simply need to assume this leads to a desync
558
525
  if (side.last_selected_move == .None) {
@@ -570,7 +537,12 @@ fn beforeMove(
570
537
  } else {
571
538
  try log.activate(ident, .Confusion);
572
539
 
573
- if (Rolls.confused(battle)) {
540
+ const confused = if (showdown)
541
+ !battle.rng.chance(u8, 128, 256)
542
+ else
543
+ battle.rng.next() >= Gen12.percent(50) + 1;
544
+
545
+ if (confused) {
574
546
  assert(!volatiles.MultiHit);
575
547
  if (!volatiles.Rage) volatiles.state = 0;
576
548
  volatiles.Bide = false;
@@ -596,12 +568,8 @@ fn beforeMove(
596
568
  player
597
569
  else
598
570
  player.foe();
599
-
600
- const uncapped = battle.last_damage;
601
571
  // Skipping adjustDamage / randomizeDamage / checkHit
602
572
  _ = try applyDamage(battle, player, target, .Confusion, log);
603
- // Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
604
- if (showdown) battle.last_damage = uncapped;
605
573
 
606
574
  return .done;
607
575
  }
@@ -610,15 +578,22 @@ fn beforeMove(
610
578
 
611
579
  if (!showdown and try disabled(side, ident, log)) return .done;
612
580
 
613
- if (Status.is(stored.status, .PAR) and Rolls.paralyzed(battle)) {
614
- if (!volatiles.Rage) volatiles.state = 0;
615
- volatiles.Bide = false;
616
- volatiles.Thrashing = false;
617
- volatiles.Charging = false;
618
- volatiles.Binding = false;
619
- // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
620
- try log.cant(ident, .Paralysis);
621
- return .done;
581
+ if (Status.is(stored.status, .PAR)) {
582
+ const paralyzed = if (showdown)
583
+ battle.rng.chance(u8, 63, 256)
584
+ else
585
+ battle.rng.next() < Gen12.percent(25);
586
+
587
+ if (paralyzed) {
588
+ if (!volatiles.Rage) volatiles.state = 0;
589
+ volatiles.Bide = false;
590
+ volatiles.Thrashing = false;
591
+ volatiles.Charging = false;
592
+ volatiles.Binding = false;
593
+ // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
594
+ try log.cant(ident, .Paralysis);
595
+ return .done;
596
+ }
622
597
  }
623
598
 
624
599
  if (volatiles.Bide) {
@@ -668,13 +643,8 @@ fn beforeMove(
668
643
  try log.start(battle.active(player), .ConfusionSilent);
669
644
  }
670
645
  try log.move(ident, side.last_selected_move, battle.active(player.foe()), from);
671
- if (showdown) {
672
- // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
673
- // used in useMove and doesn't have the notion of skipping canMove semantics
674
- side.last_used_move = side.last_selected_move;
675
- if (handleThrashing(battle, active)) {
676
- try log.start(battle.active(player), .ConfusionSilent);
677
- }
646
+ if (showdown and handleThrashing(battle, active)) {
647
+ try log.start(battle.active(player), .ConfusionSilent);
678
648
  }
679
649
  return .skip_can;
680
650
  }
@@ -689,11 +659,7 @@ fn beforeMove(
689
659
  side.last_selected_move,
690
660
  );
691
661
  if (showdown or battle.last_damage != 0) {
692
- const sub = showdown and foe.active.volatiles.Substitute;
693
662
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
694
- if (battle.foe(player).stored().hp > 0 and !sub) {
695
- try buildRage(battle, player.foe(), log);
696
- }
697
663
  }
698
664
  return .done;
699
665
  }
@@ -751,7 +717,7 @@ fn canMove(
751
717
  Effects.thrashing(battle, player);
752
718
  } else if (!showdown and move.effect == .Binding) {
753
719
  // Pokémon Showdown handles this after hit/miss checks and damage calculation
754
- Effects.binding(battle, player, false);
720
+ Effects.binding(battle, player);
755
721
  }
756
722
 
757
723
  return true;
@@ -792,7 +758,6 @@ fn doMove(
792
758
  battle: anytype,
793
759
  player: Player,
794
760
  mslot: u4,
795
- rewrap: bool,
796
761
  auto: bool,
797
762
  residual: *bool,
798
763
  log: anytype,
@@ -831,7 +796,7 @@ fn doMove(
831
796
  };
832
797
  assert(!immune or miss or (showdown and move.effect == .Binding));
833
798
 
834
- var late = showdown and move.effect != .Explode;
799
+ var late = showdown;
835
800
  const skip = status or immune;
836
801
  if ((!showdown or (!skip or counter)) and !miss) blk: {
837
802
  if (showdown and move.effect.isMulti()) {
@@ -884,9 +849,9 @@ fn doMove(
884
849
 
885
850
  if (!showdown or !miss) {
886
851
  if (move.effect == .MirrorMove) {
887
- return mirrorMove(battle, player, mslot, rewrap, auto, residual, log);
852
+ return mirrorMove(battle, player, mslot, auto, residual, log);
888
853
  } else if (move.effect == .Metronome) {
889
- return metronome(battle, player, mslot, rewrap, auto, residual, log);
854
+ return metronome(battle, player, mslot, auto, residual, log);
890
855
  } else if (move.effect.onEnd()) {
891
856
  try onEnd(battle, player, move, log);
892
857
  return null;
@@ -901,7 +866,7 @@ fn doMove(
901
866
  move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
902
867
  if (ohko) {
903
868
  try log.immune(foe_ident, .OHKO);
904
- } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
869
+ } else if (immune and !invulnerable) {
905
870
  try log.immune(foe_ident, .None);
906
871
  } else if (mist) {
907
872
  if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
@@ -951,7 +916,7 @@ fn doMove(
951
916
  if (hit == 0 and ohko) try log.ohko();
952
917
  hit += 1;
953
918
  if (foe.stored().hp == 0) break;
954
- if (!late and (!sub or move.effect == .Explode)) try buildRage(battle, player.foe(), log);
919
+ if (!late and !sub) try buildRage(battle, player.foe(), log);
955
920
  // If the substitute breaks during a multi-hit attack, the attack ends
956
921
  if (nullified) break;
957
922
  }
@@ -967,7 +932,7 @@ fn doMove(
967
932
  } else if (showdown) {
968
933
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
969
934
  if (move.effect == .Binding) {
970
- Effects.binding(battle, player, rewrap);
935
+ Effects.binding(battle, player);
971
936
  if (immune) {
972
937
  battle.last_damage = 0;
973
938
  // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
@@ -1022,7 +987,8 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
1022
987
  chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
1023
988
  chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
1024
989
 
1025
- return Rolls.criticalHit(battle, chance);
990
+ if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
991
+ return std.math.rotl(u8, battle.rng.next(), 3) < chance;
1026
992
  }
1027
993
 
1028
994
  fn calcDamage(
@@ -1126,7 +1092,16 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
1126
1092
 
1127
1093
  fn randomizeDamage(battle: anytype) void {
1128
1094
  if (battle.last_damage <= 1) return;
1129
- const random = Rolls.damage(battle);
1095
+
1096
+ const random = if (showdown)
1097
+ battle.rng.range(u8, 217, 256)
1098
+ else loop: {
1099
+ while (true) {
1100
+ const r = std.math.rotr(u8, battle.rng.next(), 1);
1101
+ if (r >= 217) break :loop r;
1102
+ }
1103
+ };
1104
+
1130
1105
  battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
1131
1106
  }
1132
1107
 
@@ -1144,9 +1119,16 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
1144
1119
  // GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
1145
1120
  .Psywave => power: {
1146
1121
  const max = @intCast(u8, @as(u16, side.stored().level) * 3 / 2);
1147
- // GLITCH: Psywave infinite glitch loop
1148
- if (!showdown and max <= 1) return Result.Error;
1149
- break :power Rolls.psywave(battle, max);
1122
+ if (showdown) {
1123
+ break :power battle.rng.range(u8, 0, max);
1124
+ } else {
1125
+ // GLITCH: Psywave infinite glitch loop
1126
+ if (max <= 1) return Result.Error;
1127
+ while (true) {
1128
+ const r = battle.rng.next();
1129
+ if (r < max) break :power r;
1130
+ }
1131
+ }
1150
1132
  },
1151
1133
  else => unreachable,
1152
1134
  };
@@ -1204,9 +1186,7 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
1204
1186
  // Pokémon Showdown calls checkHit before Counter
1205
1187
  if (!showdown and !try checkHit(battle, player, move, log)) return null;
1206
1188
 
1207
- const sub = showdown and foe.active.volatiles.Substitute;
1208
1189
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
1209
- if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
1210
1190
  return null;
1211
1191
  }
1212
1192
 
@@ -1250,7 +1230,6 @@ fn mirrorMove(
1250
1230
  battle: anytype,
1251
1231
  player: Player,
1252
1232
  mslot: u4,
1253
- rewrap: bool,
1254
1233
  auto: bool,
1255
1234
  residual: *bool,
1256
1235
  log: anytype,
@@ -1268,25 +1247,36 @@ fn mirrorMove(
1268
1247
  incrementPP(side, mslot);
1269
1248
 
1270
1249
  if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
1271
- return doMove(battle, player, mslot, rewrap, auto, residual, log);
1250
+ return doMove(battle, player, mslot, auto, residual, log);
1272
1251
  }
1273
1252
 
1274
1253
  fn metronome(
1275
1254
  battle: anytype,
1276
1255
  player: Player,
1277
1256
  mslot: u4,
1278
- rewrap: bool,
1279
1257
  auto: bool,
1280
1258
  residual: *bool,
1281
1259
  log: anytype,
1282
1260
  ) !?Result {
1283
1261
  var side = battle.side(player);
1284
1262
 
1285
- side.last_selected_move = Rolls.metronome(battle);
1263
+ side.last_selected_move = if (showdown) blk: {
1264
+ const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
1265
+ const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
1266
+ break :blk @intToEnum(Move, r + mod);
1267
+ } else loop: {
1268
+ while (true) {
1269
+ const r = battle.rng.next();
1270
+ if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
1271
+ if (r >= @enumToInt(Move.Struggle)) continue;
1272
+ break :loop @intToEnum(Move, r);
1273
+ }
1274
+ };
1275
+
1286
1276
  incrementPP(side, mslot);
1287
1277
 
1288
1278
  if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
1289
- return doMove(battle, player, mslot, rewrap, auto, residual, log);
1279
+ return doMove(battle, player, mslot, auto, residual, log);
1290
1280
  }
1291
1281
 
1292
1282
  fn checkHit(battle: anytype, player: Player, move: Move.Data, log: anytype) !bool {
@@ -1340,24 +1330,23 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
1340
1330
  }
1341
1331
 
1342
1332
  // GLITCH: Thrash / Petal Dance / Rage get their accuracy overwritten on subsequent hits
1343
- const state = side.active.volatiles.state;
1344
- var overwrite = move.effect == .Rage or move.effect == .Thrashing;
1345
- const overwritten = overwrite and state > 0;
1346
- assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
1347
-
1348
- var accuracy = if (overwritten) state else @as(u16, Gen12.percent(move.accuracy));
1333
+ var state = &side.active.volatiles.state;
1334
+ const overwritten = (move.effect == .Thrashing or move.effect == .Rage) and state.* > 0;
1335
+ assert(!overwritten or (0 < state.* and state.* <= 255 and !side.active.volatiles.Bide));
1336
+ var accuracy = if (overwritten) state.* else @as(u16, Gen12.percent(move.accuracy));
1349
1337
  var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
1350
1338
  accuracy = accuracy * boost[0] / boost[1];
1351
1339
  boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
1352
1340
  accuracy = accuracy * boost[0] / boost[1];
1353
1341
  accuracy = @min(255, @max(1, accuracy));
1354
1342
 
1355
- // Pokémon Showdown only overwrites if the volatile is present
1356
- if (showdown) overwrite = side.active.volatiles.Rage or side.active.volatiles.Thrashing;
1357
- if (overwrite) side.active.volatiles.state = accuracy;
1343
+ if (move.effect == .Thrashing or move.effect == .Rage) state.* = accuracy;
1358
1344
 
1359
1345
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1360
- break :miss !Rolls.hit(battle, accuracy);
1346
+ break :miss if (showdown)
1347
+ !battle.rng.chance(u8, @intCast(u8, accuracy), 256)
1348
+ else
1349
+ battle.rng.next() >= accuracy;
1361
1350
  };
1362
1351
 
1363
1352
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1489,6 +1478,9 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1489
1478
  damage *= volatiles.toxic;
1490
1479
  }
1491
1480
 
1481
+ // Pokémon Showdown erroneously updates last damage with uncapped Leech Seed damage
1482
+ if (showdown) battle.last_damage = damage;
1483
+
1492
1484
  const amount = @min(damage, stored.hp);
1493
1485
  stored.hp -= amount;
1494
1486
 
@@ -1580,42 +1572,6 @@ fn checkEBC(battle: anytype) bool {
1580
1572
  return false;
1581
1573
  }
1582
1574
 
1583
- fn disabled(side: *Side, ident: ID, log: anytype) !bool {
1584
- if (side.active.volatiles.disabled_move != 0) {
1585
- // A Pokémon that transforms after being disabled may end up with less move slots
1586
- const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
1587
- // side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
1588
- const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
1589
- m
1590
- else
1591
- side.last_selected_move;
1592
- if (m != .None and m == last) {
1593
- side.active.volatiles.Charging = false;
1594
- try log.disabled(ident, last);
1595
- return true;
1596
- }
1597
- }
1598
- return false;
1599
- }
1600
-
1601
- inline fn buildRage(battle: anytype, who: Player, log: anytype) !void {
1602
- const side = battle.side(who);
1603
- if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
1604
- try Effects.boost(battle, who, Move.get(.Rage), log);
1605
- }
1606
- }
1607
-
1608
- fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
1609
- var volatiles = &active.volatiles;
1610
- assert(volatiles.Thrashing);
1611
- if (volatiles.attacks > 0) return false;
1612
-
1613
- volatiles.Thrashing = false;
1614
- volatiles.Confusion = true;
1615
- volatiles.confusion = Rolls.confusionDuration(battle);
1616
- return true;
1617
- }
1618
-
1619
1575
  inline fn onBegin(
1620
1576
  battle: anytype,
1621
1577
  player: Player,
@@ -1706,7 +1662,10 @@ pub const Effects = struct {
1706
1662
  side.active.volatiles.Bide = true;
1707
1663
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1708
1664
  side.active.volatiles.state = 0;
1709
- side.active.volatiles.attacks = Rolls.bideThrashDuration(battle);
1665
+ side.active.volatiles.attacks = @intCast(u3, if (showdown)
1666
+ battle.rng.range(u4, 2, 4)
1667
+ else
1668
+ (battle.rng.next() & 1) + 2);
1710
1669
 
1711
1670
  try log.start(battle.active(player), .Bide);
1712
1671
  }
@@ -1728,8 +1687,14 @@ pub const Effects = struct {
1728
1687
  return;
1729
1688
  }
1730
1689
 
1731
- if (foe.active.types.includes(move.type)) return;
1732
- if (!Rolls.secondaryChance(battle, move.effect == .BurnChance1)) return;
1690
+ const chance = !foe.active.types.includes(move.type) and if (showdown)
1691
+ battle.rng.chance(u8, @as(u8, if (move.effect == .BurnChance1) 26 else 77), 256)
1692
+ else
1693
+ battle.rng.next() < 1 + (if (move.effect == .BurnChance1)
1694
+ Gen12.percent(10)
1695
+ else
1696
+ Gen12.percent(30));
1697
+ if (!chance) return;
1733
1698
 
1734
1699
  foe_stored.status = Status.init(.BRN);
1735
1700
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
@@ -1753,7 +1718,11 @@ pub const Effects = struct {
1753
1718
  const sub = foe.active.volatiles.Substitute;
1754
1719
 
1755
1720
  if (move.effect == .ConfusionChance) {
1756
- if (!Rolls.confusionChance(battle)) return;
1721
+ const chance = if (showdown)
1722
+ battle.rng.chance(u8, 25, 256)
1723
+ else
1724
+ battle.rng.next() < Gen12.percent(10);
1725
+ if (!chance) return;
1757
1726
  } else {
1758
1727
  if (showdown) {
1759
1728
  if (!try checkHit(battle, player, move, log)) {
@@ -1772,7 +1741,10 @@ pub const Effects = struct {
1772
1741
 
1773
1742
  if (foe.active.volatiles.Confusion) return;
1774
1743
  foe.active.volatiles.Confusion = true;
1775
- foe.active.volatiles.confusion = Rolls.confusionDuration(battle);
1744
+ foe.active.volatiles.confusion = @intCast(u3, if (showdown)
1745
+ battle.rng.range(u8, 2, 6)
1746
+ else
1747
+ (battle.rng.next() & 3) + 2);
1776
1748
 
1777
1749
  try log.start(battle.active(player.foe()), .Confusion);
1778
1750
  }
@@ -1822,8 +1794,11 @@ pub const Effects = struct {
1822
1794
  return Result.Error;
1823
1795
  }
1824
1796
 
1825
- volatiles.disabled_move = @intCast(u3, Rolls.moveSlot(battle, &foe.active.moves, n));
1826
- volatiles.disabled_duration = Rolls.disableDuration(battle);
1797
+ volatiles.disabled_move = @intCast(u3, randomMoveSlot(&battle.rng, &foe.active.moves, n));
1798
+ volatiles.disabled_duration = @intCast(u4, if (showdown)
1799
+ battle.rng.range(u8, 1, 9)
1800
+ else
1801
+ (battle.rng.next() & 7) + 1);
1827
1802
 
1828
1803
  try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
1829
1804
  return null;
@@ -1835,7 +1810,7 @@ pub const Effects = struct {
1835
1810
 
1836
1811
  if (battle.last_damage == 0) {
1837
1812
  assert(showdown);
1838
- if (!battle.foe(player).active.volatiles.Substitute) return;
1813
+ return;
1839
1814
  }
1840
1815
 
1841
1816
  const drain = @max(battle.last_damage / 2, 1);
@@ -1862,7 +1837,15 @@ pub const Effects = struct {
1862
1837
  var volatiles = &battle.foe(player).active.volatiles;
1863
1838
 
1864
1839
  if (volatiles.Substitute) return;
1865
- if (!Rolls.secondaryChance(battle, move.effect == .FlinchChance1)) return;
1840
+
1841
+ const chance = if (showdown)
1842
+ battle.rng.chance(u8, @as(u8, if (move.effect == .FlinchChance1) 26 else 77), 256)
1843
+ else
1844
+ battle.rng.next() < 1 + (if (move.effect == .FlinchChance1)
1845
+ Gen12.percent(10)
1846
+ else
1847
+ Gen12.percent(30));
1848
+ if (!chance) return;
1866
1849
 
1867
1850
  volatiles.Flinch = true;
1868
1851
  volatiles.Recharging = false;
@@ -1888,8 +1871,12 @@ pub const Effects = struct {
1888
1871
  return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
1889
1872
  }
1890
1873
 
1891
- if (foe.active.types.includes(move.type)) return;
1892
- if (!Rolls.secondaryChance(battle, true)) return;
1874
+ const chance = !foe.active.types.includes(move.type) and if (showdown)
1875
+ battle.rng.chance(u8, 26, 256)
1876
+ else
1877
+ battle.rng.next() < 1 + Gen12.percent(10);
1878
+ if (!chance) return;
1879
+
1893
1880
  // Freeze Clause Mod
1894
1881
  if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1895
1882
 
@@ -2034,7 +2021,7 @@ pub const Effects = struct {
2034
2021
  }
2035
2022
  if (!try checkHit(battle, player, move, log)) return;
2036
2023
 
2037
- const rslot = Rolls.moveSlot(battle, &foe.active.moves, 0);
2024
+ const rslot = randomMoveSlot(&battle.rng, &foe.active.moves, 0);
2038
2025
  side.active.move(oslot).id = foe.active.move(rslot).id;
2039
2026
 
2040
2027
  try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
@@ -2055,8 +2042,7 @@ pub const Effects = struct {
2055
2042
  assert(!side.active.volatiles.MultiHit);
2056
2043
  side.active.volatiles.MultiHit = true;
2057
2044
 
2058
- side.active.volatiles.attacks =
2059
- if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
2045
+ side.active.volatiles.attacks = if (move.effect == .MultiHit) distribution(battle) else 2;
2060
2046
  }
2061
2047
 
2062
2048
  fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2100,8 +2086,14 @@ pub const Effects = struct {
2100
2086
  }
2101
2087
 
2102
2088
  // Body Slam can't paralyze a Normal type Pokémon
2103
- if (foe.active.types.includes(move.type)) return;
2104
- if (!Rolls.secondaryChance(battle, move.effect == .ParalyzeChance1)) return;
2089
+ const chance = !foe.active.types.includes(move.type) and if (showdown)
2090
+ battle.rng.chance(u8, @as(u8, if (move.effect == .ParalyzeChance1) 26 else 77), 256)
2091
+ else
2092
+ battle.rng.next() < 1 + (if (move.effect == .ParalyzeChance1)
2093
+ Gen12.percent(10)
2094
+ else
2095
+ Gen12.percent(30));
2096
+ if (!chance) return;
2105
2097
 
2106
2098
  foe_stored.status = Status.init(.PAR);
2107
2099
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
@@ -2144,7 +2136,16 @@ pub const Effects = struct {
2144
2136
 
2145
2137
  if (move.effect == .Poison) {
2146
2138
  if (!showdown and !try checkHit(battle, player, move, log)) return;
2147
- } else if (!Rolls.poisonChance(battle, move.effect == .PoisonChance1)) return;
2139
+ } else {
2140
+ const chance = if (showdown)
2141
+ battle.rng.chance(u8, @as(u8, if (move.effect == .PoisonChance1) 52 else 103), 256)
2142
+ else
2143
+ battle.rng.next() < 1 + (if (move.effect == .PoisonChance1)
2144
+ Gen12.percent(20)
2145
+ else
2146
+ Gen12.percent(40));
2147
+ if (!chance) return;
2148
+ }
2148
2149
 
2149
2150
  foe_stored.status = Status.init(.PSN);
2150
2151
  if (toxic) {
@@ -2158,8 +2159,10 @@ pub const Effects = struct {
2158
2159
 
2159
2160
  fn rage(battle: anytype, player: Player) !void {
2160
2161
  var volatiles = &battle.side(player).active.volatiles;
2161
- assert(!volatiles.Bide);
2162
+ if (volatiles.Rage) return;
2162
2163
  volatiles.Rage = true;
2164
+ assert(!volatiles.Bide);
2165
+ volatiles.state = 0;
2163
2166
  }
2164
2167
 
2165
2168
  fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
@@ -2211,7 +2214,16 @@ pub const Effects = struct {
2211
2214
  }
2212
2215
  foe.active.volatiles.Recharging = false;
2213
2216
 
2214
- foe_stored.status = Status.slp(Rolls.sleepDuration(battle));
2217
+ const duration = @intCast(u3, if (showdown)
2218
+ battle.rng.range(u8, 1, 8)
2219
+ else loop: {
2220
+ while (true) {
2221
+ const r = battle.rng.next() & 7;
2222
+ if (r != 0) break :loop r;
2223
+ }
2224
+ });
2225
+
2226
+ foe_stored.status = Status.slp(duration);
2215
2227
  try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
2216
2228
  }
2217
2229
 
@@ -2264,7 +2276,11 @@ pub const Effects = struct {
2264
2276
  assert(!volatiles.Bide);
2265
2277
 
2266
2278
  volatiles.Thrashing = true;
2267
- volatiles.attacks = Rolls.bideThrashDuration(battle);
2279
+ volatiles.state = 0;
2280
+ volatiles.attacks = @intCast(u3, if (showdown)
2281
+ battle.rng.range(u8, 2, 4)
2282
+ else
2283
+ (battle.rng.next() & 1) + 2);
2268
2284
  }
2269
2285
 
2270
2286
  fn transform(battle: anytype, player: Player, log: anytype) !void {
@@ -2296,7 +2312,7 @@ pub const Effects = struct {
2296
2312
  try log.transform(battle.active(player), foe_ident);
2297
2313
  }
2298
2314
 
2299
- fn binding(battle: anytype, player: Player, rewrap: bool) void {
2315
+ fn binding(battle: anytype, player: Player) void {
2300
2316
  var side = battle.side(player);
2301
2317
  var foe = battle.foe(player);
2302
2318
 
@@ -2304,11 +2320,9 @@ pub const Effects = struct {
2304
2320
  side.active.volatiles.Binding = true;
2305
2321
  // GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
2306
2322
  // (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
2307
- if (!showdown) {
2308
- foe.active.volatiles.Recharging = false;
2309
- } else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
2323
+ if (!showdown) foe.active.volatiles.Recharging = false;
2310
2324
 
2311
- side.active.volatiles.attacks = Rolls.distribution(battle) - 1;
2325
+ side.active.volatiles.attacks = distribution(battle) - 1;
2312
2326
  }
2313
2327
 
2314
2328
  fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2335,7 +2349,7 @@ pub const Effects = struct {
2335
2349
  return try log.fail(ident, .None);
2336
2350
  }
2337
2351
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2338
- const stat = unmodifiedStats(battle, side).atk;
2352
+ const stat = unmodifiedStats(battle, player).atk;
2339
2353
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2340
2354
  try log.boost(ident, reason, n);
2341
2355
  // Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
@@ -2355,7 +2369,7 @@ pub const Effects = struct {
2355
2369
  return try log.fail(ident, .None);
2356
2370
  }
2357
2371
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2358
- const stat = unmodifiedStats(battle, side).def;
2372
+ const stat = unmodifiedStats(battle, player).def;
2359
2373
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2360
2374
  try log.boost(ident, .Defense, n);
2361
2375
  },
@@ -2372,7 +2386,7 @@ pub const Effects = struct {
2372
2386
  return try log.fail(ident, .None);
2373
2387
  }
2374
2388
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2375
- const stat = unmodifiedStats(battle, side).spe;
2389
+ const stat = unmodifiedStats(battle, player).spe;
2376
2390
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2377
2391
  try log.boost(ident, .Speed, 2);
2378
2392
  },
@@ -2392,7 +2406,7 @@ pub const Effects = struct {
2392
2406
  return try log.fail(ident, .None);
2393
2407
  }
2394
2408
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2395
- const stat = unmodifiedStats(battle, side).spc;
2409
+ const stat = unmodifiedStats(battle, player).spc;
2396
2410
  stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2397
2411
  try log.boost(ident, .SpecialAttack, n);
2398
2412
  try log.boost(ident, .SpecialDefense, n);
@@ -2419,7 +2433,11 @@ pub const Effects = struct {
2419
2433
  }
2420
2434
 
2421
2435
  if (move.effect.isStatDownChance()) {
2422
- if (!Rolls.unboost(battle) or foe.active.volatiles.Invulnerable) return;
2436
+ const chance = if (showdown)
2437
+ battle.rng.chance(u8, 85, 256)
2438
+ else
2439
+ battle.rng.next() < Gen12.percent(33) + 1;
2440
+ if (!chance or foe.active.volatiles.Invulnerable) return;
2423
2441
  } else if (!showdown and !try checkHit(battle, player, move, log)) {
2424
2442
  return; // checkHit already checks for Invulnerable
2425
2443
  }
@@ -2441,7 +2459,7 @@ pub const Effects = struct {
2441
2459
  return try log.fail(foe_ident, .None);
2442
2460
  }
2443
2461
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2444
- const stat = unmodifiedStats(battle, foe).atk;
2462
+ const stat = unmodifiedStats(battle, player.foe()).atk;
2445
2463
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2446
2464
  try log.boost(foe_ident, .Attack, -1);
2447
2465
  },
@@ -2459,7 +2477,7 @@ pub const Effects = struct {
2459
2477
  return try log.fail(foe_ident, .None);
2460
2478
  }
2461
2479
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2462
- const stat = unmodifiedStats(battle, foe).def;
2480
+ const stat = unmodifiedStats(battle, player.foe()).def;
2463
2481
  stats.def = @max(1, stat * mod[0] / mod[1]);
2464
2482
  try log.boost(foe_ident, .Defense, -@as(i8, n));
2465
2483
  },
@@ -2476,7 +2494,7 @@ pub const Effects = struct {
2476
2494
  return try log.fail(foe_ident, .None);
2477
2495
  }
2478
2496
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2479
- const stat = unmodifiedStats(battle, foe).spe;
2497
+ const stat = unmodifiedStats(battle, player.foe()).spe;
2480
2498
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2481
2499
  try log.boost(foe_ident, .Speed, -1);
2482
2500
  assert(boosts.spe >= -6);
@@ -2496,7 +2514,7 @@ pub const Effects = struct {
2496
2514
  return try log.fail(foe_ident, .None);
2497
2515
  }
2498
2516
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2499
- const stat = unmodifiedStats(battle, foe).spc;
2517
+ const stat = unmodifiedStats(battle, player.foe()).spc;
2500
2518
  stats.spc = @max(1, stat * mod[0] / mod[1]);
2501
2519
  try log.boost(foe_ident, .SpecialAttack, -1);
2502
2520
  try log.boost(foe_ident, .SpecialDefense, -1);
@@ -2515,7 +2533,8 @@ pub const Effects = struct {
2515
2533
  }
2516
2534
  };
2517
2535
 
2518
- fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
2536
+ fn unmodifiedStats(battle: anytype, who: Player) *Stats(u16) {
2537
+ const side = battle.side(who);
2519
2538
  if (!side.active.volatiles.Transform) return &side.stored().stats;
2520
2539
  const id = ID.from(side.active.volatiles.transform);
2521
2540
  return &battle.side(id.player).pokemon[id.id - 1].stats;
@@ -2581,140 +2600,80 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
2581
2600
  }
2582
2601
  }
2583
2602
 
2584
- pub const Rolls = struct {
2585
- inline fn speedTie(battle: anytype) bool {
2586
- if (showdown) return battle.rng.range(u8, 0, 2) == 0;
2587
- return battle.rng.next() < Gen12.percent(50) + 1;
2588
- }
2589
-
2590
- inline fn criticalHit(battle: anytype, chance: u16) bool {
2591
- if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
2592
- return std.math.rotl(u8, battle.rng.next(), 3) < chance;
2593
- }
2594
-
2595
- inline fn damage(battle: anytype) u8 {
2596
- if (showdown) return battle.rng.range(u8, 217, 256);
2597
- while (true) {
2598
- const r = std.math.rotr(u8, battle.rng.next(), 1);
2599
- if (r >= 217) return r;
2603
+ fn disabled(side: *Side, ident: ID, log: anytype) !bool {
2604
+ if (side.active.volatiles.disabled_move != 0) {
2605
+ // A Pokémon that transforms after being disabled may end up with less move slots
2606
+ const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
2607
+ // side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
2608
+ const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
2609
+ m
2610
+ else
2611
+ side.last_selected_move;
2612
+ if (m != .None and m == last) {
2613
+ side.active.volatiles.Charging = false;
2614
+ try log.disabled(ident, last);
2615
+ return true;
2600
2616
  }
2601
2617
  }
2618
+ return false;
2619
+ }
2602
2620
 
2603
- inline fn hit(battle: anytype, accuracy: u16) bool {
2604
- if (showdown) return battle.rng.chance(u8, @intCast(u8, accuracy), 256);
2605
- return battle.rng.next() < accuracy;
2606
- }
2607
-
2608
- inline fn confusionDuration(battle: anytype) u3 {
2609
- if (showdown) return @intCast(u3, battle.rng.range(u8, 2, 6));
2610
- return @intCast(u3, (battle.rng.next() & 3) + 2);
2611
- }
2612
-
2613
- inline fn confused(battle: anytype) bool {
2614
- if (showdown) return !battle.rng.chance(u8, 128, 256);
2615
- return battle.rng.next() >= Gen12.percent(50) + 1;
2616
- }
2617
-
2618
- inline fn paralyzed(battle: anytype) bool {
2619
- if (showdown) return battle.rng.chance(u8, 63, 256);
2620
- return battle.rng.next() < Gen12.percent(25);
2621
+ inline fn buildRage(battle: anytype, who: Player, log: anytype) !void {
2622
+ const side = battle.side(who);
2623
+ if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
2624
+ try Effects.boost(battle, who, Move.get(.Rage), log);
2621
2625
  }
2626
+ }
2622
2627
 
2623
- inline fn confusionChance(battle: anytype) bool {
2624
- if (showdown) return battle.rng.chance(u8, 25, 256);
2625
- return battle.rng.next() < Gen12.percent(10);
2626
- }
2628
+ fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
2629
+ var volatiles = &active.volatiles;
2630
+ assert(volatiles.Thrashing);
2631
+ if (volatiles.attacks > 0) return false;
2627
2632
 
2628
- inline fn secondaryChance(battle: anytype, low: bool) bool {
2629
- if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 26 else 77), 256);
2630
- return battle.rng.next() < 1 + (if (low) Gen12.percent(10) else Gen12.percent(30));
2631
- }
2633
+ volatiles.Thrashing = false;
2634
+ volatiles.Confusion = true;
2635
+ volatiles.confusion = @intCast(u3, if (showdown)
2636
+ battle.rng.range(u8, 2, 6)
2637
+ else
2638
+ (battle.rng.next() & 3) + 2);
2632
2639
 
2633
- inline fn poisonChance(battle: anytype, low: bool) bool {
2634
- if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 52 else 103), 256);
2635
- return battle.rng.next() < 1 + (if (low) Gen12.percent(20) else Gen12.percent(40));
2636
- }
2640
+ return true;
2641
+ }
2637
2642
 
2638
- inline fn sleepDuration(battle: anytype) u3 {
2639
- if (showdown) return @intCast(u3, battle.rng.range(u8, 1, 8));
2640
- while (true) {
2641
- const r = battle.rng.next() & 7;
2642
- if (r != 0) return @intCast(u3, r);
2643
- }
2644
- }
2643
+ const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
2645
2644
 
2646
- inline fn bideThrashDuration(battle: anytype) u3 {
2647
- if (showdown) return @intCast(u3, battle.rng.range(u4, 2, 4));
2648
- return @intCast(u3, (battle.rng.next() & 1) + 2);
2649
- }
2645
+ fn distribution(battle: anytype) u3 {
2646
+ if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
2647
+ const r = (battle.rng.next() & 3);
2648
+ return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
2649
+ }
2650
2650
 
2651
- fn moveSlot(battle: anytype, moves: []MoveSlot, check_pp: u4) u4 {
2652
- if (showdown) {
2653
- if (check_pp == 0) {
2654
- var i: usize = moves.len;
2655
- while (i > 0) {
2656
- i -= 1;
2657
- if (moves[i].id != .None) {
2658
- return battle.rng.range(u4, 0, @intCast(u4, i + 1)) + 1;
2659
- }
2660
- }
2661
- } else {
2662
- var r = battle.rng.range(u4, 0, @intCast(u4, check_pp)) + 1;
2663
- var i: usize = 0;
2664
- while (i < moves.len and r > 0) : (i += 1) {
2665
- if (moves[i].pp > 0) {
2666
- r -= 1;
2667
- if (r == 0) break;
2668
- }
2651
+ fn randomMoveSlot(rand: anytype, moves: []MoveSlot, check_pp: u4) u4 {
2652
+ if (showdown) {
2653
+ if (check_pp == 0) {
2654
+ var i: usize = moves.len;
2655
+ while (i > 0) {
2656
+ i -= 1;
2657
+ if (moves[i].id != .None) return rand.range(u4, 0, @intCast(u4, i + 1)) + 1;
2658
+ }
2659
+ } else {
2660
+ var r = rand.range(u4, 0, @intCast(u4, check_pp)) + 1;
2661
+ var i: usize = 0;
2662
+ while (i < moves.len and r > 0) : (i += 1) {
2663
+ if (moves[i].pp > 0) {
2664
+ r -= 1;
2665
+ if (r == 0) break;
2669
2666
  }
2670
- return @intCast(u4, i + 1);
2671
2667
  }
2672
- }
2673
-
2674
- while (true) {
2675
- const r = @intCast(u4, battle.rng.next() & 3);
2676
- if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
2668
+ return @intCast(u4, i + 1);
2677
2669
  }
2678
2670
  }
2679
2671
 
2680
- inline fn disableDuration(battle: anytype) u4 {
2681
- if (showdown) return @intCast(u4, battle.rng.range(u8, 1, 9));
2682
- return @intCast(u4, (battle.rng.next() & 7) + 1);
2672
+ while (true) {
2673
+ const r = @intCast(u4, rand.next() & 3);
2674
+ if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
2683
2675
  }
2684
-
2685
- inline fn metronome(battle: anytype) Move {
2686
- if (showdown) {
2687
- const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
2688
- const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
2689
- return @intToEnum(Move, r + mod);
2690
- }
2691
- while (true) {
2692
- const r = battle.rng.next();
2693
- if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
2694
- if (r >= @enumToInt(Move.Struggle)) continue;
2695
- return @intToEnum(Move, r);
2696
- }
2697
- }
2698
-
2699
- inline fn psywave(battle: anytype, max: u8) u8 {
2700
- if (showdown) return battle.rng.range(u8, 0, max);
2701
- while (true) {
2702
- const r = battle.rng.next();
2703
- if (r < max) return r;
2704
- }
2705
- }
2706
-
2707
- inline fn distribution(battle: anytype) u3 {
2708
- if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
2709
- const r = (battle.rng.next() & 3);
2710
- return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
2711
- }
2712
-
2713
- inline fn unboost(battle: anytype) bool {
2714
- if (showdown) return battle.rng.chance(u8, 85, 256);
2715
- return battle.rng.next() < Gen12.percent(33) + 1;
2716
- }
2717
- };
2676
+ }
2718
2677
 
2719
2678
  test "RNG agreement" {
2720
2679
  if (!showdown) return;