@pkmn/engine 0.1.0-dev.21a51fe4 → 0.1.0-dev.21e728d9

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.
Files changed (40) hide show
  1. package/README.md +11 -3
  2. package/build/pkg/data/ids.json +12 -12
  3. package/build/pkg/data/layout.json +2 -2
  4. package/build/pkg/data/protocol.json +69 -12
  5. package/build/pkg/data.d.ts +2 -2
  6. package/build/pkg/gen1.js +2 -2
  7. package/build/pkg/gen1.js.map +1 -1
  8. package/build/pkg/protocol.js +56 -37
  9. package/build/pkg/protocol.js.map +1 -1
  10. package/build/tools/debug.js +3 -1
  11. package/build/tools/debug.js.map +1 -1
  12. package/build.zig +72 -57
  13. package/package.json +27 -15
  14. package/src/bin/install-pkmn-engine +1 -1
  15. package/src/lib/binding/node.zig +1 -1
  16. package/src/lib/common/data.zig +8 -20
  17. package/src/lib/common/optional.zig +8 -1
  18. package/src/lib/common/protocol.zig +578 -80
  19. package/src/lib/common/rational.zig +5 -5
  20. package/src/lib/common/rng.zig +15 -6
  21. package/src/lib/gen1/README.md +114 -32
  22. package/src/lib/gen1/calc.zig +4 -5
  23. package/src/lib/gen1/chance.zig +58 -42
  24. package/src/lib/gen1/data/moves.zig +192 -180
  25. package/src/lib/gen1/data/species.zig +2 -2
  26. package/src/lib/gen1/data/types.zig +7 -7
  27. package/src/lib/gen1/data.zig +31 -43
  28. package/src/lib/gen1/helpers.zig +254 -116
  29. package/src/lib/gen1/mechanics.zig +142 -176
  30. package/src/lib/gen2/README.md +93 -72
  31. package/src/lib/gen2/calc.zig +143 -0
  32. package/src/lib/gen2/chance.zig +1314 -0
  33. package/src/lib/gen2/data/items.zig +12 -18
  34. package/src/lib/gen2/data/moves.zig +663 -569
  35. package/src/lib/gen2/data/species.zig +1 -1
  36. package/src/lib/gen2/data/types.zig +134 -31
  37. package/src/lib/gen2/data.zig +100 -73
  38. package/src/lib/gen2/generated.zig +808 -0
  39. package/src/lib/gen2/helpers.zig +8 -12
  40. package/src/lib/gen2/mechanics.zig +3486 -9
@@ -7,21 +7,19 @@ const DEBUG = @import("../common/debug.zig").print;
7
7
  const protocol = @import("../common/protocol.zig");
8
8
  const rng = @import("../common/rng.zig");
9
9
 
10
- const Action = @import("chance.zig").Action;
11
-
12
10
  const data = @import("data.zig");
13
11
 
14
12
  const assert = std.debug.assert;
15
13
 
16
14
  const expectEqual = std.testing.expectEqual;
17
15
 
16
+ const showdown = pkmn.options.showdown;
17
+
18
18
  const Choice = common.Choice;
19
19
  const ID = common.ID;
20
20
  const Player = common.Player;
21
21
  const Result = common.Result;
22
22
 
23
- const showdown = pkmn.options.showdown;
24
-
25
23
  const Boost = protocol.Boost;
26
24
  const Damage = protocol.Damage;
27
25
  const Heal = protocol.Heal;
@@ -59,23 +57,15 @@ const BOOSTS = &[_][2]u8{
59
57
 
60
58
  const MAX_STAT_VALUE = 999;
61
59
 
62
- const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
63
-
64
60
  pub fn update(battle: anytype, c1: Choice, c2: Choice, options: anytype) !Result {
65
- // FIXME
66
- // std.debug.print("\n", .{});
67
- // DEBUG(battle.turn);
68
- // std.debug.print("// ", .{});
69
61
  assert(c1.type != .Pass or c2.type != .Pass or battle.turn == 0);
70
62
  if (battle.turn == 0) return start(battle, options);
71
63
 
72
64
  var s1 = false;
73
65
  var s2 = false;
74
- var f1: ?Move = null;
75
- var f2: ?Move = null;
76
66
 
77
- if (selectMove(battle, .P1, c1, c2, &s1, &f1)) |r| return r;
78
- if (selectMove(battle, .P2, c2, c1, &s2, &f2)) |r| return r;
67
+ if (selectMove(battle, .P1, c1, c2, &s1)) |r| return r;
68
+ if (selectMove(battle, .P2, c2, c1, &s2)) |r| return r;
79
69
 
80
70
  var p1 = battle.side(.P1);
81
71
  var p2 = battle.side(.P2);
@@ -84,9 +74,9 @@ pub fn update(battle: anytype, c1: Choice, c2: Choice, options: anytype) !Result
84
74
  var r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
85
75
 
86
76
  if (try turnOrder(battle, c1, c2, options) == .P1) {
87
- if (try doTurn(battle, .P1, c1, r1, s1, f1, .P2, c2, r2, s2, f2, options)) |r| return r;
77
+ if (try doTurn(battle, .P1, c1, r1, s1, .P2, c2, r2, s2, options)) |r| return r;
88
78
  } else {
89
- if (try doTurn(battle, .P2, c2, r2, s2, f2, .P1, c1, r1, s1, f1, options)) |r| return r;
79
+ if (try doTurn(battle, .P2, c2, r2, s2, .P1, c1, r1, s1, options)) |r| return r;
90
80
  }
91
81
 
92
82
  if (p1.active.volatiles.attacks == 0) p1.active.volatiles.Binding = false;
@@ -124,7 +114,6 @@ fn selectMove(
124
114
  choice: Choice,
125
115
  foe_choice: Choice,
126
116
  skip_turn: *bool,
127
- from: *?Move,
128
117
  ) ?Result {
129
118
  if (choice.type == .Pass) return null;
130
119
 
@@ -141,7 +130,6 @@ fn selectMove(
141
130
  return null;
142
131
  }
143
132
  if (volatiles.Rage) {
144
- from.* = side.last_used_move;
145
133
  if (showdown) {
146
134
  if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
147
135
  saveMove(battle, player, null);
@@ -151,7 +139,6 @@ fn selectMove(
151
139
  // Pokémon Showdown removes Flinch at the end-of-turn in its residual handler
152
140
  if (!showdown) volatiles.Flinch = false;
153
141
  if (volatiles.Thrashing or volatiles.Charging) {
154
- from.* = side.last_selected_move;
155
142
  if (showdown) {
156
143
  if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
157
144
  saveMove(battle, player, null);
@@ -172,9 +159,7 @@ fn selectMove(
172
159
  return null;
173
160
  }
174
161
  if (volatiles.Binding) {
175
- from.* = side.last_used_move;
176
162
  if (showdown) {
177
- if (foe_choice.type == .Switch) from.* = null;
178
163
  // Pokémon Showdown overwrites Mirror Move with whatever was selected - really this
179
164
  // should set side.last_selected_move = last.id to reuse Mirror Move and fail in order
180
165
  // to satisfy the conditions of the Desync Clause Mod. However, because Binding is still
@@ -210,16 +195,13 @@ fn selectMove(
210
195
  if (choice.data == 0) {
211
196
  const struggle = ok: {
212
197
  for (side.active.moves, 0..) |move, i| {
213
- if (move.pp > 0 and volatiles.disabled_move != i + 1) break :ok false;
198
+ if (move.pp > 0 and volatiles.disable_move != i + 1) break :ok false;
214
199
  }
215
200
  break :ok true;
216
201
  };
217
-
218
202
  assert(struggle);
219
- saveMove(battle, player, choice);
220
- } else {
221
- saveMove(battle, player, choice);
222
203
  }
204
+ saveMove(battle, player, choice);
223
205
 
224
206
  return null;
225
207
  }
@@ -232,7 +214,7 @@ fn saveMove(battle: anytype, player: Player, choice: ?Choice) void {
232
214
  if (c.data == 0) {
233
215
  side.last_selected_move = .Struggle;
234
216
  } else {
235
- assert(showdown or side.active.volatiles.disabled_move != c.data);
217
+ assert(showdown or side.active.volatiles.disable_move != c.data);
236
218
  const move = side.active.move(c.data);
237
219
  // You cannot *select* a move with 0 PP (except on Pokémon Showdown where that is
238
220
  // sometimes required...), but a 0 PP move can be used automatically
@@ -343,12 +325,10 @@ fn doTurn(
343
325
  player_choice: Choice,
344
326
  player_rewrap: bool,
345
327
  player_skip: bool,
346
- player_from: ?Move,
347
328
  foe_player: Player,
348
329
  foe_choice: Choice,
349
330
  foe_rewrap: bool,
350
331
  foe_skip: bool,
351
- foe_from: ?Move,
352
332
  options: anytype,
353
333
  ) !?Result {
354
334
  assert(player_choice.type != .Pass);
@@ -361,7 +341,6 @@ fn doTurn(
361
341
  player_choice,
362
342
  player_rewrap,
363
343
  player_skip,
364
- player_from,
365
344
  &residual,
366
345
  options,
367
346
  )) |r| return r;
@@ -384,7 +363,6 @@ fn doTurn(
384
363
  foe_choice,
385
364
  foe_rewrap,
386
365
  foe_skip,
387
- foe_from,
388
366
  &residual,
389
367
  options,
390
368
  )) |r| return r;
@@ -412,7 +390,6 @@ fn executeMove(
412
390
  choice: Choice,
413
391
  rewrap: bool,
414
392
  skip: bool,
415
- from: ?Move,
416
393
  residual: *bool,
417
394
  options: anytype,
418
395
  ) !?Result {
@@ -447,8 +424,10 @@ fn executeMove(
447
424
  mslot = @intCast(battle.lastMove(player).index);
448
425
  const stored = side.stored();
449
426
  // GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
450
- auto = side.last_selected_move == .HyperBeam or side.active.volatiles.Bide or
451
- from != null or Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
427
+ auto = isForced(&side.active) or
428
+ side.active.volatiles.Binding or side.active.volatiles.Bide or
429
+ side.last_selected_move == .HyperBeam or
430
+ Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
452
431
  // If it wasn't Hyper Beam or the continuation of a move effect then we must have just
453
432
  // thawed, in which case we will desync unless the last_selected_move happened to be at
454
433
  // index 1 and the current Pokémon has the same move in its first slot.
@@ -470,7 +449,7 @@ fn executeMove(
470
449
 
471
450
  var skip_can = false;
472
451
  var skip_pp = false;
473
- switch (try beforeMove(battle, player, skip, from, residual, options)) {
452
+ switch (try beforeMove(battle, player, skip, residual, options)) {
474
453
  .done => return null,
475
454
  .skip_can => skip_can = true,
476
455
  .skip_pp => skip_pp = true,
@@ -478,9 +457,9 @@ fn executeMove(
478
457
  .err => return @as(?Result, Result.Error),
479
458
  }
480
459
 
481
- if (!skip_can and !try canMove(battle, player, mslot, auto, skip_pp, from, residual, options)) {
482
- return null;
483
- }
460
+ const can = skip_can or
461
+ try canMove(battle, player, mslot, auto, skip_pp, .None, residual, options);
462
+ if (!can) return null;
484
463
 
485
464
  return doMove(battle, player, mslot, rewrap, auto, residual, options);
486
465
  }
@@ -491,7 +470,6 @@ fn beforeMove(
491
470
  battle: anytype,
492
471
  player: Player,
493
472
  skip: bool,
494
- from: ?Move,
495
473
  residual: *bool,
496
474
  options: anytype,
497
475
  ) !BeforeMove {
@@ -552,16 +530,16 @@ fn beforeMove(
552
530
  return .done;
553
531
  }
554
532
 
555
- if (volatiles.disabled_duration > 0) {
533
+ if (volatiles.disable_duration > 0) {
556
534
  if (options.calc.modify(player, .disable)) |extend| {
557
- if (!extend) volatiles.disabled_duration = 0;
535
+ if (!extend) volatiles.disable_duration = 0;
558
536
  } else {
559
- volatiles.disabled_duration -= 1;
537
+ volatiles.disable_duration -= 1;
560
538
  }
561
- try options.chance.disable(player, volatiles.disabled_duration);
539
+ try options.chance.disable(player, volatiles.disable_duration);
562
540
 
563
- if (volatiles.disabled_duration == 0) {
564
- volatiles.disabled_move = 0;
541
+ if (volatiles.disable_duration == 0) {
542
+ volatiles.disable_move = 0;
565
543
  try log.end(ident, .Disable);
566
544
  }
567
545
  }
@@ -698,7 +676,6 @@ fn beforeMove(
698
676
 
699
677
  if (volatiles.Thrashing) {
700
678
  assert(volatiles.attacks > 0);
701
- assert(from != null);
702
679
 
703
680
  if (options.calc.modify(player, .attacking)) |extend| {
704
681
  if (!extend) volatiles.attacks = 0;
@@ -710,7 +687,7 @@ fn beforeMove(
710
687
  if (!showdown and handleThrashing(battle, active, player, options)) {
711
688
  try log.start(battle.active(player), .ConfusionSilent);
712
689
  }
713
- try log.move(ident, side.last_selected_move, battle.active(player.foe()), from);
690
+ try log.move(ident, side.last_selected_move, battle.active(player.foe()));
714
691
  if (showdown) {
715
692
  // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
716
693
  // used in useMove and doesn't have the notion of skipping canMove semantics
@@ -732,12 +709,7 @@ fn beforeMove(
732
709
  }
733
710
  try options.chance.binding(player, volatiles.attacks);
734
711
 
735
- try log.move(
736
- battle.active(player),
737
- side.last_selected_move,
738
- battle.active(player.foe()),
739
- side.last_selected_move,
740
- );
712
+ try log.move(battle.active(player), side.last_selected_move, battle.active(player.foe()));
741
713
  if (showdown or battle.last_damage != 0) {
742
714
  const sub = showdown and foe.active.volatiles.Substitute;
743
715
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -757,7 +729,7 @@ fn canMove(
757
729
  mslot: u4,
758
730
  auto: bool,
759
731
  skip_pp: bool,
760
- from: ?Move,
732
+ from: Move,
761
733
  residual: *bool,
762
734
  options: anytype,
763
735
  ) !bool {
@@ -765,33 +737,22 @@ fn canMove(
765
737
  const player_ident = battle.active(player);
766
738
  const move = Move.get(side.last_selected_move);
767
739
 
768
- const foe_last = battle.foe(player).last_used_move;
769
- const special = from != null and (from.? == .Metronome and
770
- (from.? == .MirrorMove and !(foe_last == .None or foe_last == .MirrorMove)));
771
-
772
740
  var skip = skip_pp;
773
741
  if (side.active.volatiles.Charging) {
774
742
  side.active.volatiles.Charging = false;
775
743
  side.active.volatiles.Invulnerable = false;
776
744
  } else if (move.effect == .Charge) {
777
- try options.log.move(player_ident, side.last_selected_move, .{}, from);
745
+ try options.log.moveFrom(player_ident, side.last_selected_move, .{}, from);
778
746
  setCounterable(battle, player, side, move);
779
747
  try Effects.charge(battle, player, options);
780
748
  return false;
781
749
  }
782
750
 
783
- if (!showdown or !special) side.last_used_move = side.last_selected_move;
751
+ side.last_used_move = side.last_selected_move;
784
752
  if (!skip) decrementPP(side, mslot, auto);
785
753
 
786
754
  const target = if (move.target == .Self) player else player.foe();
787
- // Pokémon Showdown's protocol for Rage and Binding moves should come with a [from], though we
788
- // don't have space to be able to track it in all circumstances and thus usually try to infer it
789
- // from side.last_used_move. However, this gets reset by the opponent switching or fainting,
790
- // meaning sometimes we lose this information. from is still set because it not being null is
791
- // relevant for control flow, but we don't have anything useful to put in the log. This is
792
- // regrettable, but this information is more "nice to have" than required
793
- const f = if (from != null and from.? == .None) null else from;
794
- try options.log.move(player_ident, side.last_selected_move, battle.active(target), f);
755
+ try options.log.moveFrom(player_ident, side.last_selected_move, battle.active(target), from);
795
756
  setCounterable(battle, player, side, move);
796
757
 
797
758
  if (move.effect.onBegin()) {
@@ -829,12 +790,14 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
829
790
  assert(!volatiles.Rage and !volatiles.Thrashing and !volatiles.MultiHit);
830
791
  if (volatiles.Bide) return;
831
792
 
832
- assert(active.move(mslot).pp > 0 or auto);
833
- active.move(mslot).pp = @as(u6, @intCast(active.move(mslot).pp)) -% 1;
793
+ var move_slot = active.move(mslot);
794
+ assert(move_slot.pp > 0 or auto);
795
+ move_slot.pp = @as(u6, @intCast(move_slot.pp)) -% 1;
834
796
  if (volatiles.Transform) return;
835
797
 
836
- assert(side.stored().move(mslot).pp > 0 or auto);
837
- side.stored().move(mslot).pp = @as(u6, @intCast(side.stored().move(mslot).pp)) -% 1;
798
+ move_slot = side.stored().move(mslot);
799
+ assert(move_slot.pp > 0 or auto);
800
+ move_slot.pp = @as(u6, @intCast(move_slot.pp)) -% 1;
838
801
  assert(active.move(mslot).pp == side.stored().move(mslot).pp);
839
802
  }
840
803
 
@@ -1332,7 +1295,7 @@ fn mirrorMove(
1332
1295
  auto: bool,
1333
1296
  residual: *bool,
1334
1297
  options: anytype,
1335
- ) !?Result {
1298
+ ) anyerror!?Result {
1336
1299
  var side = battle.side(player);
1337
1300
  const foe = battle.foe(player);
1338
1301
 
@@ -1359,7 +1322,7 @@ fn metronome(
1359
1322
  auto: bool,
1360
1323
  residual: *bool,
1361
1324
  options: anytype,
1362
- ) !?Result {
1325
+ ) anyerror!?Result {
1363
1326
  var side = battle.side(player);
1364
1327
 
1365
1328
  side.last_selected_move = try Rolls.metronome(battle, player, options);
@@ -1438,7 +1401,7 @@ fn moveHit(
1438
1401
  const overwritten = overwrite and state > 0;
1439
1402
  assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
1440
1403
 
1441
- var accuracy = if (overwritten) state else @as(u16, Gen12.percent(move.accuracy));
1404
+ var accuracy = if (overwritten) state else @as(u16, move.accuracy);
1442
1405
  var boost = BOOSTS[@as(u4, @intCast(@as(i8, side.active.boosts.accuracy) + 6))];
1443
1406
  accuracy = accuracy * boost[0] / boost[1];
1444
1407
  boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
@@ -1477,9 +1440,8 @@ fn checkFaint(
1477
1440
  const foe_fainted = foe.stored().hp == 0;
1478
1441
 
1479
1442
  const player_out = findFirstAlive(side) == 0;
1480
- const foe_out = findFirstAlive(foe) == 0;
1481
- const tie = player_out and foe_out;
1482
- const more = tie or player_out or foe_out;
1443
+ const foe_out = foe_fainted and findFirstAlive(foe) == 0;
1444
+ const more = player_out or foe_out;
1483
1445
 
1484
1446
  if (try faint(battle, player, !(more or foe_fainted), options)) |r| return r;
1485
1447
  if (foe_fainted) if (try faint(battle, player.foe(), !more, options)) |r| return r;
@@ -1487,7 +1449,7 @@ fn checkFaint(
1487
1449
  assert(!side.active.volatiles.MultiHit);
1488
1450
  assert(!foe.active.volatiles.MultiHit);
1489
1451
 
1490
- if (tie) {
1452
+ if (player_out and foe_out) {
1491
1453
  try options.log.tie();
1492
1454
  return Result.Tie;
1493
1455
  } else if (player_out) {
@@ -1499,8 +1461,8 @@ fn checkFaint(
1499
1461
  }
1500
1462
 
1501
1463
  const foe_choice: Choice.Type = if (foe_fainted) .Switch else .Pass;
1502
- if (player == .P1) return Result{ .p1 = .Switch, .p2 = foe_choice };
1503
- return Result{ .p1 = foe_choice, .p2 = .Switch };
1464
+ if (player == .P1) return .{ .p1 = .Switch, .p2 = foe_choice };
1465
+ return .{ .p1 = foe_choice, .p2 = .Switch };
1504
1466
  }
1505
1467
 
1506
1468
  fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
@@ -1548,21 +1510,19 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1548
1510
  var volatiles = &side.active.volatiles;
1549
1511
 
1550
1512
  const brn = Status.is(stored.status, .BRN);
1551
- if (brn or Status.is(stored.status, .PSN)) blk: {
1513
+ if (brn or Status.is(stored.status, .PSN)) {
1552
1514
  var damage = @max(stored.stats.hp / 16, 1);
1553
-
1554
1515
  if (volatiles.Toxic) {
1555
1516
  volatiles.toxic += 1;
1556
1517
  damage *= volatiles.toxic;
1557
1518
  }
1519
+ damage = @min(damage, stored.hp);
1558
1520
 
1559
- const amount = @min(damage, stored.hp);
1560
- if (showdown and amount == 0) break :blk;
1561
-
1562
- stored.hp -= amount;
1563
-
1564
- // Pokémon Showdown uses damageOf here but its not relevant in Generation I
1565
- try options.log.damage(ident, stored, if (brn) Damage.Burn else Damage.Poison);
1521
+ if (!showdown or damage > 0) {
1522
+ stored.hp -= damage;
1523
+ // Pokémon Showdown uses damageOf here but its not relevant in Generation I
1524
+ try options.log.damage(ident, stored, if (brn) Damage.Burn else Damage.Poison);
1525
+ }
1566
1526
  }
1567
1527
 
1568
1528
  if (volatiles.LeechSeed) {
@@ -1675,9 +1635,9 @@ fn checkEBC(battle: anytype) bool {
1675
1635
  }
1676
1636
 
1677
1637
  fn disabled(side: *Side, ident: ID, options: anytype) !bool {
1678
- if (side.active.volatiles.disabled_move != 0) {
1638
+ if (side.active.volatiles.disable_move != 0) {
1679
1639
  // A Pokémon that transforms after being disabled may end up with less move slots
1680
- const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
1640
+ const m = side.active.moves[side.active.volatiles.disable_move - 1].id;
1681
1641
  // side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
1682
1642
  const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
1683
1643
  m
@@ -1692,7 +1652,7 @@ fn disabled(side: *Side, ident: ID, options: anytype) !bool {
1692
1652
  return false;
1693
1653
  }
1694
1654
 
1695
- inline fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1655
+ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1696
1656
  const side = battle.side(who);
1697
1657
  if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
1698
1658
  try Effects.boost(battle, who, Move.get(.Rage), options);
@@ -1710,7 +1670,7 @@ fn handleThrashing(battle: anytype, active: *ActivePokemon, player: Player, opti
1710
1670
  return true;
1711
1671
  }
1712
1672
 
1713
- inline fn onBegin(
1673
+ fn onBegin(
1714
1674
  battle: anytype,
1715
1675
  player: Player,
1716
1676
  move: Move.Data,
@@ -1740,7 +1700,7 @@ inline fn onBegin(
1740
1700
  };
1741
1701
  }
1742
1702
 
1743
- inline fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1703
+ fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1744
1704
  assert(move.effect.onEnd());
1745
1705
  return switch (move.effect) {
1746
1706
  .Bide => Effects.bide(battle, player, options),
@@ -1756,7 +1716,7 @@ inline fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anyty
1756
1716
  };
1757
1717
  }
1758
1718
 
1759
- inline fn alwaysHappens(
1719
+ fn alwaysHappens(
1760
1720
  battle: anytype,
1761
1721
  player: Player,
1762
1722
  move: Move.Data,
@@ -1775,7 +1735,7 @@ inline fn alwaysHappens(
1775
1735
  };
1776
1736
  }
1777
1737
 
1778
- inline fn moveEffect(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1738
+ fn moveEffect(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1779
1739
  return switch (move.effect) {
1780
1740
  .BurnChance1, .BurnChance2 => Effects.burnChance(battle, player, move, options),
1781
1741
  .ConfusionChance => Effects.confusion(battle, player, move, options),
@@ -1905,7 +1865,7 @@ pub const Effects = struct {
1905
1865
  try options.chance.commit(player, .hit);
1906
1866
  }
1907
1867
 
1908
- if (volatiles.disabled_move != 0) {
1868
+ if (volatiles.disable_move != 0) {
1909
1869
  try options.log.fail(foe_ident, .None);
1910
1870
  return null;
1911
1871
  }
@@ -1930,18 +1890,17 @@ pub const Effects = struct {
1930
1890
  return Result.Error;
1931
1891
  }
1932
1892
 
1933
- volatiles.disabled_move =
1893
+ volatiles.disable_move =
1934
1894
  @intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
1935
- volatiles.disabled_duration = Rolls.disableDuration(battle, player, options);
1895
+ volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
1936
1896
 
1937
- const id = foe.active.move(volatiles.disabled_move).id;
1897
+ const id = foe.active.move(volatiles.disable_move).id;
1938
1898
  try options.log.startEffect(foe_ident, .Disable, id);
1939
1899
  return null;
1940
1900
  }
1941
1901
 
1942
1902
  fn drainHP(battle: anytype, player: Player, options: anytype) !void {
1943
- var side = battle.side(player);
1944
- var stored = side.stored();
1903
+ var stored = battle.side(player).stored();
1945
1904
 
1946
1905
  if (battle.last_damage == 0) {
1947
1906
  assert(showdown);
@@ -2066,7 +2025,7 @@ pub const Effects = struct {
2066
2025
  // GLITCH: HP recovery move failure glitches
2067
2026
  const delta = stored.stats.hp - stored.hp;
2068
2027
  if (delta == 0 or (delta & 255 == 255 and stored.hp % 256 != 0)) {
2069
- return try options.log.fail(ident, .None);
2028
+ return options.log.fail(ident, .None);
2070
2029
  }
2071
2030
 
2072
2031
  const rest = side.last_selected_move == .Rest;
@@ -2148,7 +2107,7 @@ pub const Effects = struct {
2148
2107
  // Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
2149
2108
  if (!try checkHit(battle, player, move, options)) return;
2150
2109
  try options.chance.commit(player, .hit);
2151
- return try options.log.fail(battle.active(player.foe()), .None);
2110
+ return options.log.fail(battle.active(player.foe()), .None);
2152
2111
  }
2153
2112
  }
2154
2113
  if (!try checkHit(battle, player, move, options)) return;
@@ -2366,8 +2325,7 @@ pub const Effects = struct {
2366
2325
  fn substitute(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
2367
2326
  var side = battle.side(player);
2368
2327
  if (side.active.volatiles.Substitute) {
2369
- try options.log.fail(battle.active(player), .Substitute);
2370
- return;
2328
+ return options.log.fail(battle.active(player), .Substitute);
2371
2329
  }
2372
2330
 
2373
2331
  assert(side.stored().stats.hp <= 1023);
@@ -2378,10 +2336,7 @@ pub const Effects = struct {
2378
2336
  // the target's HP is exactly divisible by 4 (here we're using an inlined divCeil routine to
2379
2337
  // avoid having to convert to floating point)
2380
2338
  const required_hp = if (showdown) @divFloor(side.stored().stats.hp - 1, 4) + 1 else hp;
2381
- if (side.stored().hp < required_hp) {
2382
- try options.log.fail(battle.active(player), .Weak);
2383
- return;
2384
- }
2339
+ if (side.stored().hp < required_hp) return options.log.fail(battle.active(player), .Weak);
2385
2340
 
2386
2341
  // GLITCH: can leave the user with 0 HP (faints later) because didn't check '<=' above
2387
2342
  side.stored().hp -= hp;
@@ -2395,12 +2350,17 @@ pub const Effects = struct {
2395
2350
  }
2396
2351
 
2397
2352
  fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
2398
- if (!showdown or battle.side(player).last_selected_move == .Teleport) return;
2399
-
2400
2353
  // Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
2401
- if (try checkHit(battle, player, move, options)) try options.chance.commit(player, .hit);
2402
-
2403
- battle.last_damage = 0;
2354
+ if (showdown) {
2355
+ if (battle.side(player).last_selected_move == .Teleport) return;
2356
+ if (try checkHit(battle, player, move, options)) {
2357
+ try options.chance.commit(player, .hit);
2358
+ }
2359
+ battle.last_damage = 0;
2360
+ } else {
2361
+ try options.log.fail(battle.active(player), .None);
2362
+ try options.log.laststill();
2363
+ }
2404
2364
  }
2405
2365
 
2406
2366
  fn thrashing(battle: anytype, player: Player, options: anytype) void {
@@ -2468,7 +2428,7 @@ pub const Effects = struct {
2468
2428
  switch (move.effect) {
2469
2429
  .AttackUp1, .AttackUp2, .Rage => {
2470
2430
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2471
- if (boosts.atk == 6) return try log.fail(ident, .None);
2431
+ if (boosts.atk == 6) return log.fail(ident, .None);
2472
2432
  const n: u2 = if (move.effect == .AttackUp2) 2 else 1;
2473
2433
  boosts.atk = @as(i4, @intCast(@min(6, @as(i8, boosts.atk) + n)));
2474
2434
  const reason = if (move.effect == .Rage) Boost.Rage else Boost.Attack;
@@ -2479,7 +2439,7 @@ pub const Effects = struct {
2479
2439
  try log.boost(ident, Boost.Attack, -1);
2480
2440
  if (move.effect == .Rage) return;
2481
2441
  }
2482
- return try log.fail(ident, .None);
2442
+ return log.fail(ident, .None);
2483
2443
  }
2484
2444
  var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2485
2445
  const stat = unmodifiedStats(battle, side).atk;
@@ -2490,7 +2450,7 @@ pub const Effects = struct {
2490
2450
  },
2491
2451
  .DefenseUp1, .DefenseUp2 => {
2492
2452
  assert(boosts.def >= -6 and boosts.def <= 6);
2493
- if (boosts.def == 6) return try log.fail(ident, .None);
2453
+ if (boosts.def == 6) return log.fail(ident, .None);
2494
2454
  const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
2495
2455
  boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
2496
2456
  if (stats.def == MAX_STAT_VALUE) {
@@ -2499,7 +2459,7 @@ pub const Effects = struct {
2499
2459
  try log.boost(ident, .Defense, n);
2500
2460
  try log.boost(ident, .Defense, -1);
2501
2461
  }
2502
- return try log.fail(ident, .None);
2462
+ return log.fail(ident, .None);
2503
2463
  }
2504
2464
  var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2505
2465
  const stat = unmodifiedStats(battle, side).def;
@@ -2508,7 +2468,7 @@ pub const Effects = struct {
2508
2468
  },
2509
2469
  .SpeedUp2 => {
2510
2470
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2511
- if (boosts.spe == 6) return try log.fail(ident, .None);
2471
+ if (boosts.spe == 6) return log.fail(ident, .None);
2512
2472
  boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
2513
2473
  if (stats.spe == MAX_STAT_VALUE) {
2514
2474
  boosts.spe -= 1;
@@ -2516,7 +2476,7 @@ pub const Effects = struct {
2516
2476
  try log.boost(ident, .Speed, 2);
2517
2477
  try log.boost(ident, .Speed, -1);
2518
2478
  }
2519
- return try log.fail(ident, .None);
2479
+ return log.fail(ident, .None);
2520
2480
  }
2521
2481
  var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2522
2482
  const stat = unmodifiedStats(battle, side).spe;
@@ -2525,7 +2485,7 @@ pub const Effects = struct {
2525
2485
  },
2526
2486
  .SpecialUp1, .SpecialUp2 => {
2527
2487
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2528
- if (boosts.spc == 6) return try log.fail(ident, .None);
2488
+ if (boosts.spc == 6) return log.fail(ident, .None);
2529
2489
  const n: u2 = if (move.effect == .SpecialUp2) 2 else 1;
2530
2490
  boosts.spc = @intCast(@min(6, @as(i8, boosts.spc) + n));
2531
2491
  if (stats.spc == MAX_STAT_VALUE) {
@@ -2536,7 +2496,7 @@ pub const Effects = struct {
2536
2496
  try log.boost(ident, .SpecialDefense, n);
2537
2497
  try log.boost(ident, .SpecialDefense, -1);
2538
2498
  }
2539
- return try log.fail(ident, .None);
2499
+ return log.fail(ident, .None);
2540
2500
  }
2541
2501
  var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2542
2502
  const stat = unmodifiedStats(battle, side).spc;
@@ -2546,7 +2506,7 @@ pub const Effects = struct {
2546
2506
  },
2547
2507
  .EvasionUp1 => {
2548
2508
  assert(boosts.evasion >= -6 and boosts.evasion <= 6);
2549
- if (boosts.evasion == 6) return try log.fail(ident, .None);
2509
+ if (boosts.evasion == 6) return log.fail(ident, .None);
2550
2510
  boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
2551
2511
  try log.boost(ident, .Evasion, 1);
2552
2512
  },
@@ -2589,7 +2549,7 @@ pub const Effects = struct {
2589
2549
  try log.boost(foe_ident, .Attack, -1);
2590
2550
  try log.boost(foe_ident, .Attack, 1);
2591
2551
  }
2592
- return try log.fail(foe_ident, .None);
2552
+ return log.fail(foe_ident, .None);
2593
2553
  }
2594
2554
  var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2595
2555
  const stat = unmodifiedStats(battle, foe).atk;
@@ -2607,7 +2567,7 @@ pub const Effects = struct {
2607
2567
  try log.boost(foe_ident, .Defense, -@as(i8, n));
2608
2568
  try log.boost(foe_ident, .Defense, 1);
2609
2569
  }
2610
- return try log.fail(foe_ident, .None);
2570
+ return log.fail(foe_ident, .None);
2611
2571
  }
2612
2572
  var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2613
2573
  const stat = unmodifiedStats(battle, foe).def;
@@ -2624,7 +2584,7 @@ pub const Effects = struct {
2624
2584
  try log.boost(foe_ident, .Speed, -1);
2625
2585
  try log.boost(foe_ident, .Speed, 1);
2626
2586
  }
2627
- return try log.fail(foe_ident, .None);
2587
+ return log.fail(foe_ident, .None);
2628
2588
  }
2629
2589
  var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2630
2590
  const stat = unmodifiedStats(battle, foe).spe;
@@ -2644,7 +2604,7 @@ pub const Effects = struct {
2644
2604
  try log.boost(foe_ident, .SpecialDefense, -1);
2645
2605
  try log.boost(foe_ident, .SpecialDefense, 1);
2646
2606
  }
2647
- return try log.fail(foe_ident, .None);
2607
+ return log.fail(foe_ident, .None);
2648
2608
  }
2649
2609
  var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2650
2610
  const stat = unmodifiedStats(battle, foe).spc;
@@ -2680,7 +2640,7 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
2680
2640
  }
2681
2641
  }
2682
2642
 
2683
- inline fn isForced(active: ActivePokemon) bool {
2643
+ fn isForced(active: anytype) bool {
2684
2644
  return active.volatiles.Recharging or active.volatiles.Rage or
2685
2645
  active.volatiles.Thrashing or active.volatiles.Charging;
2686
2646
  }
@@ -2693,9 +2653,9 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2693
2653
 
2694
2654
  options.chance.clearDurations(who, clear);
2695
2655
 
2696
- if (volatiles.disabled_move != 0) {
2697
- volatiles.disabled_move = 0;
2698
- volatiles.disabled_duration = 0;
2656
+ if (volatiles.disable_move != 0) {
2657
+ volatiles.disable_move = 0;
2658
+ volatiles.disable_duration = 0;
2699
2659
  try log.end(ident, .DisableSilent);
2700
2660
  }
2701
2661
  if (volatiles.Confusion) {
@@ -2705,15 +2665,15 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2705
2665
  }
2706
2666
  if (volatiles.Mist) {
2707
2667
  volatiles.Mist = false;
2708
- try log.end(ident, .Mist);
2668
+ try log.end(ident, .MistSilent);
2709
2669
  }
2710
2670
  if (volatiles.FocusEnergy) {
2711
2671
  volatiles.FocusEnergy = false;
2712
- try log.end(ident, .FocusEnergy);
2672
+ try log.end(ident, .FocusEnergySilent);
2713
2673
  }
2714
2674
  if (volatiles.LeechSeed) {
2715
2675
  volatiles.LeechSeed = false;
2716
- try log.end(ident, .LeechSeed);
2676
+ try log.end(ident, .LeechSeedSilent);
2717
2677
  }
2718
2678
  if (!showdown and volatiles.Toxic) {
2719
2679
  volatiles.Toxic = false;
@@ -2721,22 +2681,22 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2721
2681
  }
2722
2682
  if (volatiles.LightScreen) {
2723
2683
  volatiles.LightScreen = false;
2724
- try log.end(ident, .LightScreen);
2684
+ try log.end(ident, .LightScreenSilent);
2725
2685
  }
2726
2686
  if (volatiles.Reflect) {
2727
2687
  volatiles.Reflect = false;
2728
- try log.end(ident, .Reflect);
2688
+ try log.end(ident, .ReflectSilent);
2729
2689
  }
2730
2690
  if (showdown and volatiles.Toxic) {
2731
2691
  volatiles.Toxic = false;
2732
2692
  // Pokémon Showdown erroneously clears the toxic counter
2733
2693
  volatiles.toxic = 0;
2734
- try log.end(ident, .Toxic);
2694
+ try log.end(ident, .ToxicSilent);
2735
2695
  }
2736
2696
  }
2737
2697
 
2738
2698
  pub const Rolls = struct {
2739
- inline fn speedTie(battle: anytype, options: anytype) !bool {
2699
+ fn speedTie(battle: anytype, options: anytype) !bool {
2740
2700
  const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
2741
2701
  player == .P1
2742
2702
  else if (showdown)
@@ -2748,7 +2708,7 @@ pub const Rolls = struct {
2748
2708
  return p1;
2749
2709
  }
2750
2710
 
2751
- inline fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2711
+ fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2752
2712
  const crit = if (options.calc.overridden(player, .critical_hit)) |val|
2753
2713
  val == .true
2754
2714
  else if (showdown)
@@ -2760,7 +2720,7 @@ pub const Rolls = struct {
2760
2720
  return crit;
2761
2721
  }
2762
2722
 
2763
- inline fn damage(battle: anytype, player: anytype, options: anytype) !u8 {
2723
+ fn damage(battle: anytype, player: Player, options: anytype) !u8 {
2764
2724
  const roll = if (options.calc.overridden(player, .damage)) |val|
2765
2725
  val
2766
2726
  else roll: {
@@ -2771,11 +2731,12 @@ pub const Rolls = struct {
2771
2731
  }
2772
2732
  };
2773
2733
 
2734
+ assert(roll >= 217 and roll <= 255);
2774
2735
  try options.chance.damage(player, roll);
2775
2736
  return roll;
2776
2737
  }
2777
2738
 
2778
- inline fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) bool {
2739
+ fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) bool {
2779
2740
  const ok = if (options.calc.overridden(player, .hit)) |val|
2780
2741
  val == .true
2781
2742
  else if (showdown)
@@ -2787,7 +2748,7 @@ pub const Rolls = struct {
2787
2748
  return ok;
2788
2749
  }
2789
2750
 
2790
- inline fn confused(battle: anytype, player: Player, options: anytype) !bool {
2751
+ fn confused(battle: anytype, player: Player, options: anytype) !bool {
2791
2752
  const cfz = if (options.calc.overridden(player, .confused)) |val|
2792
2753
  val == .true
2793
2754
  else if (showdown)
@@ -2799,7 +2760,7 @@ pub const Rolls = struct {
2799
2760
  return cfz;
2800
2761
  }
2801
2762
 
2802
- inline fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2763
+ fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2803
2764
  const par = if (options.calc.overridden(player, .paralyzed)) |val|
2804
2765
  val == .true
2805
2766
  else if (showdown)
@@ -2811,7 +2772,7 @@ pub const Rolls = struct {
2811
2772
  return par;
2812
2773
  }
2813
2774
 
2814
- inline fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2775
+ fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2815
2776
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2816
2777
  val == .true
2817
2778
  else if (showdown)
@@ -2823,7 +2784,7 @@ pub const Rolls = struct {
2823
2784
  return proc;
2824
2785
  }
2825
2786
 
2826
- inline fn secondaryChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2787
+ fn secondaryChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2827
2788
  const rate: u8 = if (low) 26 else 77;
2828
2789
 
2829
2790
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
@@ -2837,7 +2798,7 @@ pub const Rolls = struct {
2837
2798
  return proc;
2838
2799
  }
2839
2800
 
2840
- inline fn poisonChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2801
+ fn poisonChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2841
2802
  const rate: u8 = if (low) 52 else 103;
2842
2803
 
2843
2804
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
@@ -2851,7 +2812,7 @@ pub const Rolls = struct {
2851
2812
  return proc;
2852
2813
  }
2853
2814
 
2854
- inline fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2815
+ fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2855
2816
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2856
2817
  val == .true
2857
2818
  else if (showdown)
@@ -2863,14 +2824,12 @@ pub const Rolls = struct {
2863
2824
  return proc;
2864
2825
  }
2865
2826
 
2866
- inline fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2827
+ fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2867
2828
  const move: Move = if (options.calc.overridden(player, .metronome)) |val|
2868
2829
  val
2869
- else if (showdown) move: {
2870
- const r = battle.rng.range(u8, 0, @intFromEnum(Move.Struggle) - 2);
2871
- const mod = @as(u2, (if (r < @intFromEnum(Move.Metronome) - 1) 1 else 2));
2872
- break :move @enumFromInt(r + mod);
2873
- } else move: {
2830
+ else if (showdown)
2831
+ Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
2832
+ else move: {
2874
2833
  while (true) {
2875
2834
  const r = battle.rng.next();
2876
2835
  if (r == 0 or r == @intFromEnum(Move.Metronome)) continue;
@@ -2883,7 +2842,7 @@ pub const Rolls = struct {
2883
2842
  return move;
2884
2843
  }
2885
2844
 
2886
- inline fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2845
+ fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2887
2846
  const power = if (options.calc.overridden(player, .psywave)) |val|
2888
2847
  val - 1
2889
2848
  else if (showdown)
@@ -2895,15 +2854,16 @@ pub const Rolls = struct {
2895
2854
  }
2896
2855
  };
2897
2856
 
2857
+ assert(power < max);
2898
2858
  try options.chance.psywave(player, power, max);
2899
2859
  return power;
2900
2860
  }
2901
2861
 
2902
- inline fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2862
+ fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2903
2863
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2904
2864
  @intCast(val)
2905
2865
  else if (showdown)
2906
- @intCast(battle.rng.range(u8, 1, 8))
2866
+ battle.rng.range(u3, 1, 8)
2907
2867
  else duration: {
2908
2868
  while (true) {
2909
2869
  const r = battle.rng.next() & 7;
@@ -2911,47 +2871,53 @@ pub const Rolls = struct {
2911
2871
  }
2912
2872
  };
2913
2873
 
2874
+ assert(duration >= 1 and duration <= 7);
2914
2875
  options.chance.duration(.sleep, player, player.foe(), duration);
2915
2876
  return duration;
2916
2877
  }
2917
2878
 
2918
- inline fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2879
+ fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2919
2880
  const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
2920
2881
  val
2921
2882
  else if (showdown)
2922
- @intCast(battle.rng.range(u8, 1, 9))
2883
+ battle.rng.range(u4, 1, 9)
2923
2884
  else
2924
2885
  @intCast((battle.rng.next() & 7) + 1);
2925
2886
 
2887
+ assert(duration >= 1 and duration <= 8);
2926
2888
  options.chance.duration(.disable, player, player.foe(), duration);
2927
2889
  return duration;
2928
2890
  }
2929
2891
 
2930
- inline fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
2892
+ fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
2931
2893
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2932
2894
  @intCast(val)
2933
2895
  else if (showdown)
2934
- @intCast(battle.rng.range(u8, 2, 6))
2896
+ battle.rng.range(u3, 2, 6)
2935
2897
  else
2936
2898
  @intCast((battle.rng.next() & 3) + 2);
2937
2899
 
2900
+ assert(duration >= 2 and duration <= 5);
2938
2901
  options.chance.duration(.confusion, player, if (self) player else player.foe(), duration);
2939
2902
  return duration;
2940
2903
  }
2941
2904
 
2942
- inline fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2905
+ fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2943
2906
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2944
2907
  @intCast(val)
2945
2908
  else if (showdown)
2946
- @intCast(battle.rng.range(u4, 2, 4))
2909
+ battle.rng.range(u3, 2, 4)
2947
2910
  else
2948
2911
  @intCast((battle.rng.next() & 1) + 2);
2949
2912
 
2913
+ assert(duration >= 2 and duration <= 3);
2950
2914
  options.chance.duration(.attacking, player, player, duration);
2951
2915
  return duration;
2952
2916
  }
2953
2917
 
2954
- inline fn distribution(
2918
+ const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
2919
+
2920
+ fn distribution(
2955
2921
  battle: anytype,
2956
2922
  comptime effect: Move.Effect,
2957
2923
  player: Player,
@@ -2961,12 +2927,13 @@ pub const Rolls = struct {
2961
2927
  const n: u3 = if (options.calc.overridden(player, roll)) |val|
2962
2928
  @intCast(val)
2963
2929
  else if (showdown)
2964
- DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)]
2930
+ DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
2965
2931
  else n: {
2966
2932
  const r = (battle.rng.next() & 3);
2967
2933
  break :n @intCast((if (r < 2) r else battle.rng.next() & 3) + 2);
2968
2934
  };
2969
2935
 
2936
+ assert(n >= 2 and n <= 5);
2970
2937
  if (effect == .Binding) {
2971
2938
  options.chance.duration(.binding, player, player, n);
2972
2939
  } else {
@@ -3007,10 +2974,9 @@ pub const Rolls = struct {
3007
2974
  while (i < moves.len and r > 0) : (i += 1) {
3008
2975
  if (moves[i].pp > 0) {
3009
2976
  r -= 1;
3010
- if (r == 0) break;
2977
+ if (r == 0) break :slot @intCast(i + 1);
3011
2978
  }
3012
2979
  }
3013
- break :slot @intCast(i + 1);
3014
2980
  }
3015
2981
  }
3016
2982
 
@@ -3032,11 +2998,11 @@ test "RNG agreement" {
3032
2998
  expected[i] = @intCast(i * 0x1000000);
3033
2999
  }
3034
3000
 
3035
- var spe = rng.FixedRNG(1, expected.len){ .rolls = expected };
3036
- var cfz = rng.FixedRNG(1, expected.len){ .rolls = expected };
3037
- var par = rng.FixedRNG(1, expected.len){ .rolls = expected };
3038
- var brn = rng.FixedRNG(1, expected.len){ .rolls = expected };
3039
- var eff = rng.FixedRNG(1, expected.len){ .rolls = expected };
3001
+ var spe: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3002
+ var cfz: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3003
+ var par: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3004
+ var brn: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3005
+ var eff: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3040
3006
 
3041
3007
  for (0..expected.len) |i| {
3042
3008
  try expectEqual(spe.range(u8, 0, 2) == 0, i < Gen12.percent(50) + 1);
@@ -3072,7 +3038,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3072
3038
  const side = battle.side(player);
3073
3039
  const foe = battle.foe(player);
3074
3040
 
3075
- var active = side.active;
3041
+ var active = &side.active;
3076
3042
  const stored = side.stored();
3077
3043
 
3078
3044
  // While players are not given any input options on the cartridge in these cases,
@@ -3126,7 +3092,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3126
3092
  if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
3127
3093
  // Pokémon Showdown displays Struggle if limited to Bide but unable to pick
3128
3094
  const struggle =
3129
- m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);
3095
+ m.id == .Bide and (m.pp == 0 or active.volatiles.disable_move == slot);
3130
3096
  const s = if (struggle) 0 else slot;
3131
3097
  out[n] = .{ .type = .Move, .data = s };
3132
3098
  n += 1;
@@ -3141,7 +3107,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3141
3107
  const m = active.moves[slot - 1];
3142
3108
  if (m.id == .None) break;
3143
3109
  if (m.pp == 0) continue;
3144
- if (active.volatiles.disabled_move == slot) continue;
3110
+ if (active.volatiles.disable_move == slot) continue;
3145
3111
  out[n] = .{ .type = .Move, .data = slot };
3146
3112
  n += 1;
3147
3113
  }