@pkmn/engine 0.1.0-dev.46307385 → 0.1.0-dev.4c173f03

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.
@@ -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
 
@@ -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
@@ -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);
@@ -2395,12 +2354,17 @@ pub const Effects = struct {
2395
2354
  }
2396
2355
 
2397
2356
  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
2357
  // 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;
2358
+ if (showdown) {
2359
+ if (battle.side(player).last_selected_move == .Teleport) return;
2360
+ if (try checkHit(battle, player, move, options)) {
2361
+ try options.chance.commit(player, .hit);
2362
+ }
2363
+ battle.last_damage = 0;
2364
+ } else {
2365
+ try options.log.fail(battle.active(player), .None);
2366
+ try options.log.laststill();
2367
+ }
2404
2368
  }
2405
2369
 
2406
2370
  fn thrashing(battle: anytype, player: Player, options: anytype) void {
@@ -2693,9 +2657,9 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2693
2657
 
2694
2658
  options.chance.clearDurations(who, clear);
2695
2659
 
2696
- if (volatiles.disabled_move != 0) {
2697
- volatiles.disabled_move = 0;
2698
- volatiles.disabled_duration = 0;
2660
+ if (volatiles.disable_move != 0) {
2661
+ volatiles.disable_move = 0;
2662
+ volatiles.disable_duration = 0;
2699
2663
  try log.end(ident, .DisableSilent);
2700
2664
  }
2701
2665
  if (volatiles.Confusion) {
@@ -2705,15 +2669,15 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2705
2669
  }
2706
2670
  if (volatiles.Mist) {
2707
2671
  volatiles.Mist = false;
2708
- try log.end(ident, .Mist);
2672
+ try log.end(ident, .MistSilent);
2709
2673
  }
2710
2674
  if (volatiles.FocusEnergy) {
2711
2675
  volatiles.FocusEnergy = false;
2712
- try log.end(ident, .FocusEnergy);
2676
+ try log.end(ident, .FocusEnergySilent);
2713
2677
  }
2714
2678
  if (volatiles.LeechSeed) {
2715
2679
  volatiles.LeechSeed = false;
2716
- try log.end(ident, .LeechSeed);
2680
+ try log.end(ident, .LeechSeedSilent);
2717
2681
  }
2718
2682
  if (!showdown and volatiles.Toxic) {
2719
2683
  volatiles.Toxic = false;
@@ -2721,17 +2685,17 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2721
2685
  }
2722
2686
  if (volatiles.LightScreen) {
2723
2687
  volatiles.LightScreen = false;
2724
- try log.end(ident, .LightScreen);
2688
+ try log.end(ident, .LightScreenSilent);
2725
2689
  }
2726
2690
  if (volatiles.Reflect) {
2727
2691
  volatiles.Reflect = false;
2728
- try log.end(ident, .Reflect);
2692
+ try log.end(ident, .ReflectSilent);
2729
2693
  }
2730
2694
  if (showdown and volatiles.Toxic) {
2731
2695
  volatiles.Toxic = false;
2732
2696
  // Pokémon Showdown erroneously clears the toxic counter
2733
2697
  volatiles.toxic = 0;
2734
- try log.end(ident, .Toxic);
2698
+ try log.end(ident, .ToxicSilent);
2735
2699
  }
2736
2700
  }
2737
2701
 
@@ -2760,7 +2724,7 @@ pub const Rolls = struct {
2760
2724
  return crit;
2761
2725
  }
2762
2726
 
2763
- inline fn damage(battle: anytype, player: anytype, options: anytype) !u8 {
2727
+ inline fn damage(battle: anytype, player: Player, options: anytype) !u8 {
2764
2728
  const roll = if (options.calc.overridden(player, .damage)) |val|
2765
2729
  val
2766
2730
  else roll: {
@@ -2867,11 +2831,9 @@ pub const Rolls = struct {
2867
2831
  inline fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2868
2832
  const move: Move = if (options.calc.overridden(player, .metronome)) |val|
2869
2833
  val
2870
- else if (showdown) move: {
2871
- const r = battle.rng.range(u8, 0, @intFromEnum(Move.Struggle) - 2);
2872
- const mod = @as(u2, (if (r < @intFromEnum(Move.Metronome) - 1) 1 else 2));
2873
- break :move @enumFromInt(r + mod);
2874
- } else move: {
2834
+ else if (showdown)
2835
+ Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
2836
+ else move: {
2875
2837
  while (true) {
2876
2838
  const r = battle.rng.next();
2877
2839
  if (r == 0 or r == @intFromEnum(Move.Metronome)) continue;
@@ -2905,7 +2867,7 @@ pub const Rolls = struct {
2905
2867
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2906
2868
  @intCast(val)
2907
2869
  else if (showdown)
2908
- @intCast(battle.rng.range(u8, 1, 8))
2870
+ battle.rng.range(u3, 1, 8)
2909
2871
  else duration: {
2910
2872
  while (true) {
2911
2873
  const r = battle.rng.next() & 7;
@@ -2922,7 +2884,7 @@ pub const Rolls = struct {
2922
2884
  const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
2923
2885
  val
2924
2886
  else if (showdown)
2925
- @intCast(battle.rng.range(u8, 1, 9))
2887
+ battle.rng.range(u4, 1, 9)
2926
2888
  else
2927
2889
  @intCast((battle.rng.next() & 7) + 1);
2928
2890
 
@@ -2935,7 +2897,7 @@ pub const Rolls = struct {
2935
2897
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2936
2898
  @intCast(val)
2937
2899
  else if (showdown)
2938
- @intCast(battle.rng.range(u8, 2, 6))
2900
+ battle.rng.range(u3, 2, 6)
2939
2901
  else
2940
2902
  @intCast((battle.rng.next() & 3) + 2);
2941
2903
 
@@ -2948,7 +2910,7 @@ pub const Rolls = struct {
2948
2910
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2949
2911
  @intCast(val)
2950
2912
  else if (showdown)
2951
- @intCast(battle.rng.range(u4, 2, 4))
2913
+ battle.rng.range(u3, 2, 4)
2952
2914
  else
2953
2915
  @intCast((battle.rng.next() & 1) + 2);
2954
2916
 
@@ -2957,6 +2919,8 @@ pub const Rolls = struct {
2957
2919
  return duration;
2958
2920
  }
2959
2921
 
2922
+ const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
2923
+
2960
2924
  inline fn distribution(
2961
2925
  battle: anytype,
2962
2926
  comptime effect: Move.Effect,
@@ -2967,7 +2931,7 @@ pub const Rolls = struct {
2967
2931
  const n: u3 = if (options.calc.overridden(player, roll)) |val|
2968
2932
  @intCast(val)
2969
2933
  else if (showdown)
2970
- DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)]
2934
+ DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
2971
2935
  else n: {
2972
2936
  const r = (battle.rng.next() & 3);
2973
2937
  break :n @intCast((if (r < 2) r else battle.rng.next() & 3) + 2);
@@ -3014,10 +2978,9 @@ pub const Rolls = struct {
3014
2978
  while (i < moves.len and r > 0) : (i += 1) {
3015
2979
  if (moves[i].pp > 0) {
3016
2980
  r -= 1;
3017
- if (r == 0) break;
2981
+ if (r == 0) break :slot @intCast(i + 1);
3018
2982
  }
3019
2983
  }
3020
- break :slot @intCast(i + 1);
3021
2984
  }
3022
2985
  }
3023
2986
 
@@ -3039,11 +3002,11 @@ test "RNG agreement" {
3039
3002
  expected[i] = @intCast(i * 0x1000000);
3040
3003
  }
3041
3004
 
3042
- var spe = rng.FixedRNG(1, expected.len){ .rolls = expected };
3043
- var cfz = rng.FixedRNG(1, expected.len){ .rolls = expected };
3044
- var par = rng.FixedRNG(1, expected.len){ .rolls = expected };
3045
- var brn = rng.FixedRNG(1, expected.len){ .rolls = expected };
3046
- var eff = rng.FixedRNG(1, expected.len){ .rolls = expected };
3005
+ var spe: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3006
+ var cfz: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3007
+ var par: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3008
+ var brn: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3009
+ var eff: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3047
3010
 
3048
3011
  for (0..expected.len) |i| {
3049
3012
  try expectEqual(spe.range(u8, 0, 2) == 0, i < Gen12.percent(50) + 1);
@@ -3133,7 +3096,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3133
3096
  if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
3134
3097
  // Pokémon Showdown displays Struggle if limited to Bide but unable to pick
3135
3098
  const struggle =
3136
- m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);
3099
+ m.id == .Bide and (m.pp == 0 or active.volatiles.disable_move == slot);
3137
3100
  const s = if (struggle) 0 else slot;
3138
3101
  out[n] = .{ .type = .Move, .data = s };
3139
3102
  n += 1;
@@ -3148,7 +3111,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3148
3111
  const m = active.moves[slot - 1];
3149
3112
  if (m.id == .None) break;
3150
3113
  if (m.pp == 0) continue;
3151
- if (active.volatiles.disabled_move == slot) continue;
3114
+ if (active.volatiles.disable_move == slot) continue;
3152
3115
  out[n] = .{ .type = .Move, .data = slot };
3153
3116
  n += 1;
3154
3117
  }