@pkmn/engine 0.1.0-dev.83de29ee → 0.1.0-dev.8b83af6b

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.
@@ -126,7 +126,7 @@ fn selectMove(
126
126
  const stored = side.stored();
127
127
 
128
128
  assert(!isForced(side.active) or
129
- (choice.type == .Move and choice.data == @boolToInt(showdown)));
129
+ (choice.type == .Move and choice.data == @intFromBool(showdown)));
130
130
 
131
131
  // pre-battle menu
132
132
  if (volatiles.Recharging) {
@@ -183,7 +183,7 @@ fn selectMove(
183
183
  battle.last_moves.p1_index
184
184
  else
185
185
  battle.last_moves.p2_index;
186
- const last = side.active.move(@intCast(u4, slot));
186
+ const last = side.active.move(slot);
187
187
  if (last.id == .Metronome) side.last_selected_move = last.id;
188
188
  if (last.id == .MirrorMove) return Result.Error;
189
189
  }
@@ -237,9 +237,9 @@ fn saveMove(battle: anytype, player: Player, choice: ?Choice) void {
237
237
 
238
238
  side.last_selected_move = move.id;
239
239
  if (player == .P1) {
240
- battle.last_moves.p1_index = @intCast(u3, c.data);
240
+ battle.last_moves.p1_index = @intCast(c.data);
241
241
  } else {
242
- battle.last_moves.p2_index = @intCast(u3, c.data);
242
+ battle.last_moves.p2_index = @intCast(c.data);
243
243
  }
244
244
  }
245
245
  }
@@ -318,7 +318,9 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
318
318
  const spe2 = battle.side(.P2).active.stats.spe;
319
319
  if (spe1 == spe2) {
320
320
  // Pokémon Showdown's beforeTurnCallback shenanigans
321
- if (showdown and m1 == .Counter and m2 == .Counter) battle.rng.advance(1);
321
+ if (showdown and !double_switch and m1 == .Counter and m2 == .Counter) {
322
+ battle.rng.advance(1);
323
+ }
322
324
 
323
325
  const p1 = Rolls.speedTie(battle);
324
326
  if (!showdown) return if (p1) .P1 else .P2;
@@ -418,7 +420,7 @@ fn executeMove(
418
420
  var side = battle.side(player);
419
421
 
420
422
  if (choice.type == .Switch) {
421
- try switchIn(battle, player, @intCast(u4, choice.data), false, log);
423
+ try switchIn(battle, player, choice.data, false, log);
422
424
  return null;
423
425
  }
424
426
 
@@ -434,7 +436,7 @@ fn executeMove(
434
436
  }
435
437
 
436
438
  assert(choice.type == .Move);
437
- var mslot = @intCast(u4, choice.data);
439
+ var mslot: u4 = @intCast(choice.data);
438
440
  // Sadly, we can't even check `Move.get(side.last_selected_move).effect == .Binding` here
439
441
  // because Pokémon Showdown's Mirror Move implementation clobbers side.last_selected_move
440
442
  var auto = showdown and side.last_selected_move != .None;
@@ -443,7 +445,7 @@ fn executeMove(
443
445
  if (mslot == 0 and side.last_selected_move != .None and side.last_selected_move != .Struggle) {
444
446
  // choice.data == 0 only happens with Struggle on Pokémon Showdown
445
447
  assert(!showdown);
446
- mslot = @intCast(u4, if (player == .P1)
448
+ mslot = @intCast(if (player == .P1)
447
449
  battle.last_moves.p1_index
448
450
  else
449
451
  battle.last_moves.p2_index);
@@ -467,7 +469,7 @@ fn executeMove(
467
469
  } else if (showdown and side.active.volatiles.Charging) {
468
470
  // Incorrect mslot with Pokémon Showdown choice semantics so we need to recover from index
469
471
  assert(mslot == 1);
470
- mslot = @intCast(u4, if (player == .P1)
472
+ mslot = @intCast(if (player == .P1)
471
473
  battle.last_moves.p1_index
472
474
  else
473
475
  battle.last_moves.p2_index);
@@ -627,7 +629,14 @@ fn beforeMove(
627
629
  if (volatiles.Bide) {
628
630
  assert(!volatiles.Thrashing and !volatiles.Rage);
629
631
 
630
- volatiles.state +%= battle.last_damage;
632
+ if (showdown) {
633
+ // Pokémon Showdown doesn't implement Bide potentially overflowing in the event of
634
+ // OHKO-move damage, but we can fake this incorrect behavior by simply saturating the
635
+ // addition because 65535 is sufficient to faint any Pokémon anyway
636
+ volatiles.state +|= battle.last_damage;
637
+ } else {
638
+ volatiles.state +%= battle.last_damage;
639
+ }
631
640
 
632
641
  assert(volatiles.attacks > 0);
633
642
 
@@ -728,6 +737,7 @@ fn canMove(
728
737
  side.active.volatiles.Invulnerable = false;
729
738
  } else if (move.effect == .Charge) {
730
739
  try log.move(player_ident, side.last_selected_move, .{}, from);
740
+ setCounterable(battle, player, side, move);
731
741
  try Effects.charge(battle, player, log);
732
742
  return false;
733
743
  }
@@ -744,19 +754,7 @@ fn canMove(
744
754
  // regrettable, but this information is more "nice to have" than required
745
755
  const f = if (from != null and from.? == .None) null else from;
746
756
  try log.move(player_ident, side.last_selected_move, battle.active(target), f);
747
-
748
- // The Counter desync is caused by the cartridge not calling GetCurrentMove until now, meaning
749
- // in cases where an early return happens the data for a players last selected move does not get
750
- // reloaded and HandleCounterMove actually bases its success/failure off of stale information.
751
- // This boolean state we track here doesn't exist on the cartridge because it instead manifests
752
- // as the desync results from actually having two separate battle states that subtly disagree.
753
- const counterable = side.last_selected_move != .Counter and move.bp > 0 and
754
- (move.type == .Normal or move.type == .Fighting);
755
- if (player == .P1) {
756
- battle.last_moves.p1_counterable = @boolToInt(counterable);
757
- } else {
758
- battle.last_moves.p2_counterable = @boolToInt(counterable);
759
- }
757
+ setCounterable(battle, player, side, move);
760
758
 
761
759
  if (move.effect.onBegin()) {
762
760
  try onBegin(battle, player, move, mslot, residual, log);
@@ -773,6 +771,21 @@ fn canMove(
773
771
  return true;
774
772
  }
775
773
 
774
+ fn setCounterable(battle: anytype, player: Player, side: *Side, move: Move.Data) void {
775
+ // The Counter desync is caused by the cartridge not calling GetCurrentMove until now, meaning
776
+ // in cases where an early return happens the data for a players last selected move does not get
777
+ // reloaded and HandleCounterMove actually bases its success/failure off of stale information.
778
+ // This boolean state we track here doesn't exist on the cartridge because it instead manifests
779
+ // as the desync results from actually having two separate battle states that subtly disagree
780
+ const counterable = side.last_selected_move != .Counter and move.bp > 0 and
781
+ (move.type == .Normal or move.type == .Fighting);
782
+ if (player == .P1) {
783
+ battle.last_moves.p1_counterable = @intFromBool(counterable);
784
+ } else {
785
+ battle.last_moves.p2_counterable = @intFromBool(counterable);
786
+ }
787
+ }
788
+
776
789
  fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
777
790
  if (side.last_selected_move == .Struggle) return;
778
791
 
@@ -783,11 +796,11 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
783
796
  if (volatiles.Bide) return;
784
797
 
785
798
  assert(active.move(mslot).pp > 0 or auto);
786
- active.move(mslot).pp = @intCast(u6, active.move(mslot).pp) -% 1;
799
+ active.move(mslot).pp = @as(u6, @intCast(active.move(mslot).pp)) -% 1;
787
800
  if (volatiles.Transform) return;
788
801
 
789
802
  assert(side.stored().move(mslot).pp > 0 or auto);
790
- side.stored().move(mslot).pp = @intCast(u6, side.stored().move(mslot).pp) -% 1;
803
+ side.stored().move(mslot).pp = @as(u6, @intCast(side.stored().move(mslot).pp)) -% 1;
791
804
  assert(active.move(mslot).pp == side.stored().move(mslot).pp);
792
805
  }
793
806
 
@@ -795,12 +808,12 @@ fn incrementPP(side: *Side, mslot: u4) void {
795
808
  var active = &side.active;
796
809
  const volatiles = &active.volatiles;
797
810
 
798
- active.move(mslot).pp = @intCast(u6, active.move(mslot).pp) +% 1;
811
+ active.move(mslot).pp = @as(u6, @intCast(active.move(mslot).pp)) +% 1;
799
812
  // GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
800
813
  if (showdown and volatiles.Transform) return;
801
814
 
802
815
  assert(mslot > 0 and mslot <= 4);
803
- side.stored().moves[mslot - 1].pp = @intCast(u6, side.stored().moves[mslot - 1].pp) +% 1;
816
+ side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% 1;
804
817
  }
805
818
 
806
819
  // Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
@@ -836,8 +849,8 @@ fn doMove(
836
849
  // calling moveHit early covers most of that but we also need to check type immunity first
837
850
  var miss = showdown and miss: {
838
851
  immune = move.target != .Self and !status and !counter and
839
- (@enumToInt(move.type.effectiveness(foe.active.types.type1)) == 0 or
840
- @enumToInt(move.type.effectiveness(foe.active.types.type2)) == 0);
852
+ (@intFromEnum(move.type.effectiveness(foe.active.types.type1)) == 0 or
853
+ @intFromEnum(move.type.effectiveness(foe.active.types.type2)) == 0);
841
854
  if (immune and move.effect != .Binding) break :miss true;
842
855
  if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) {
843
856
  battle.last_damage = 0;
@@ -928,7 +941,7 @@ fn doMove(
928
941
  }
929
942
  if (move.effect == .JumpKick) {
930
943
  // Recoil is supposed to be damage/8 but damage will always be 0 here
931
- assert(battle.last_damage == 0);
944
+ assert(showdown or battle.last_damage == 0);
932
945
  battle.last_damage = 1;
933
946
  _ = try applyDamage(battle, player, player.foe(), .None, log);
934
947
  if (showdown and side.stored().hp == 0) residual.* = false;
@@ -1110,7 +1123,7 @@ fn calcDamage(
1110
1123
  d = @min(997, d);
1111
1124
  d += 2;
1112
1125
 
1113
- battle.last_damage = @intCast(u16, d);
1126
+ battle.last_damage = @intCast(d);
1114
1127
 
1115
1128
  return true;
1116
1129
  }
@@ -1124,9 +1137,9 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
1124
1137
  var d = battle.last_damage;
1125
1138
  if (side.active.types.includes(move.type)) d +%= d / 2;
1126
1139
 
1127
- const neutral = @enumToInt(Effectiveness.Neutral);
1128
- const eff1: u16 = @enumToInt(move.type.effectiveness(types.type1));
1129
- const eff2: u16 = @enumToInt(move.type.effectiveness(types.type2));
1140
+ const neutral = @intFromEnum(Effectiveness.Neutral);
1141
+ const eff1: u16 = @intFromEnum(move.type.effectiveness(types.type1));
1142
+ const eff2: u16 = @intFromEnum(move.type.effectiveness(types.type2));
1130
1143
 
1131
1144
  // Type effectiveness matchup precedence only matters with (NVE, SE)
1132
1145
  if (!showdown and (eff1 + eff2) == Effectiveness.mismatch and
@@ -1149,7 +1162,7 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
1149
1162
  fn randomizeDamage(battle: anytype) void {
1150
1163
  if (battle.last_damage <= 1) return;
1151
1164
  const random = Rolls.damage(battle);
1152
- battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
1165
+ battle.last_damage = @intCast(@as(u32, battle.last_damage) *% random / 255);
1153
1166
  }
1154
1167
 
1155
1168
  fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
@@ -1165,7 +1178,7 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
1165
1178
  .DragonRage => 40,
1166
1179
  // GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
1167
1180
  .Psywave => power: {
1168
- const max = @intCast(u8, @as(u16, side.stored().level) * 3 / 2);
1181
+ const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
1169
1182
  // GLITCH: Psywave infinite glitch loop
1170
1183
  if (!showdown and max <= 1) return Result.Error;
1171
1184
  break :power Rolls.psywave(battle, max);
@@ -1219,7 +1232,7 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
1219
1232
  return null;
1220
1233
  }
1221
1234
 
1222
- battle.last_damage = if (battle.last_damage > 0x7FFF) 0xFFFF else battle.last_damage * 2;
1235
+ battle.last_damage *|= 2;
1223
1236
 
1224
1237
  // Pokémon Showdown calls checkHit before Counter
1225
1238
  if (!showdown and !try checkHit(battle, player, move, log)) return null;
@@ -1254,7 +1267,7 @@ fn applyDamage(
1254
1267
  return true;
1255
1268
  } else {
1256
1269
  // Safe to truncate since less than subbed.volatiles.substitute which is a u8
1257
- subbed.active.volatiles.substitute -= @intCast(u8, battle.last_damage);
1270
+ subbed.active.volatiles.substitute -= @intCast(battle.last_damage);
1258
1271
  try log.activate(battle.active(sub_player), .Substitute);
1259
1272
  return false;
1260
1273
  }
@@ -1366,9 +1379,9 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
1366
1379
  assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
1367
1380
 
1368
1381
  var accuracy = if (overwritten) state else @as(u16, Gen12.percent(move.accuracy));
1369
- var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
1382
+ var boost = BOOSTS[@intCast(@as(i8, side.active.boosts.accuracy) + 6)];
1370
1383
  accuracy = accuracy * boost[0] / boost[1];
1371
- boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
1384
+ boost = BOOSTS[@intCast(@as(i8, -foe.active.boosts.evasion) + 6)];
1372
1385
  accuracy = accuracy * boost[0] / boost[1];
1373
1386
  accuracy = @min(255, @max(1, accuracy));
1374
1387
 
@@ -1387,7 +1400,7 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
1387
1400
  }
1388
1401
 
1389
1402
  if (!miss) return true;
1390
- battle.last_damage = 0;
1403
+ if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
1391
1404
  side.active.volatiles.Binding = false;
1392
1405
  return false;
1393
1406
  }
@@ -1527,13 +1540,13 @@ fn endTurn(battle: anytype, log: anytype) @TypeOf(log).Error!Result {
1527
1540
  assert(!battle.side(.P1).active.volatiles.MultiHit);
1528
1541
  assert(!battle.side(.P2).active.volatiles.MultiHit);
1529
1542
 
1543
+ battle.turn += 1;
1544
+
1530
1545
  if (showdown and options.ebc and checkEBC(battle)) {
1531
1546
  try log.tie();
1532
1547
  return Result.Tie;
1533
1548
  }
1534
1549
 
1535
- battle.turn += 1;
1536
-
1537
1550
  if (showdown and battle.turn >= 1000) {
1538
1551
  try log.tie();
1539
1552
  return Result.Tie;
@@ -1548,7 +1561,7 @@ fn endTurn(battle: anytype, log: anytype) @TypeOf(log).Error!Result {
1548
1561
 
1549
1562
  fn checkEBC(battle: anytype) bool {
1550
1563
  for (battle.sides, 0..) |side, i| {
1551
- const foe = battle.sides[~@intCast(u1, i)];
1564
+ const foe = battle.sides[~@as(u1, @intCast(i))];
1552
1565
 
1553
1566
  var foe_all_ghosts = true;
1554
1567
  var foe_all_transform = true;
@@ -1842,7 +1855,7 @@ pub const Effects = struct {
1842
1855
  return Result.Error;
1843
1856
  }
1844
1857
 
1845
- volatiles.disabled_move = @intCast(u3, Rolls.moveSlot(battle, &foe.active.moves, n));
1858
+ volatiles.disabled_move = @intCast(Rolls.moveSlot(battle, &foe.active.moves, n));
1846
1859
  volatiles.disabled_duration = Rolls.disableDuration(battle);
1847
1860
 
1848
1861
  try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
@@ -1941,7 +1954,7 @@ pub const Effects = struct {
1941
1954
  // Pokémon Showdown clears P1 then P2 instead of status -> side -> foe
1942
1955
  if (showdown) {
1943
1956
  for (&battle.sides, 0..) |*s, i| {
1944
- const p = @intToEnum(Player, i);
1957
+ const p: Player = @enumFromInt(i);
1945
1958
  // Pokémon Showdown incorrectly does not prevent sleep/freeze from moving
1946
1959
  if (p != player and Status.any(s.stored().status)) {
1947
1960
  try log.curestatus(foe_ident, foe_stored.status, .Silent);
@@ -1972,7 +1985,9 @@ pub const Effects = struct {
1972
1985
 
1973
1986
  // GLITCH: HP recovery move failure glitches
1974
1987
  const delta = stored.stats.hp - stored.hp;
1975
- if (delta == 0 or delta & 255 == 255) return try log.fail(ident, .None);
1988
+ if (delta == 0 or (delta & 255 == 255 and stored.hp % 256 != 0)) {
1989
+ return try log.fail(ident, .None);
1990
+ }
1976
1991
 
1977
1992
  const rest = side.last_selected_move == .Rest;
1978
1993
  if (rest) {
@@ -2040,7 +2055,7 @@ pub const Effects = struct {
2040
2055
  const has_mimic = has_mimic: {
2041
2056
  for (side.active.moves, 0..) |m, i| {
2042
2057
  if (m.id == .Mimic) {
2043
- oslot = @intCast(u8, i + 1);
2058
+ oslot = @intCast(i + 1);
2044
2059
  break :has_mimic true;
2045
2060
  }
2046
2061
  }
@@ -2186,9 +2201,12 @@ pub const Effects = struct {
2186
2201
  var side = battle.side(player);
2187
2202
  var stored = side.stored();
2188
2203
 
2189
- const damage = @intCast(i16, @max(battle.last_damage /
2204
+ assert(showdown or battle.last_damage > 0);
2205
+ if (showdown and battle.last_damage == 0) return;
2206
+
2207
+ const damage: i16 = @intCast(@max(battle.last_damage /
2190
2208
  @as(u8, if (side.last_selected_move == .Struggle) 2 else 4), 1));
2191
- stored.hp = @intCast(u16, @max(@intCast(i16, stored.hp) - damage, 0));
2209
+ stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
2192
2210
 
2193
2211
  try log.damageOf(battle.active(player), stored, .RecoilOf, battle.active(player.foe()));
2194
2212
  if (showdown and stored.hp == 0) residual.* = false;
@@ -2248,7 +2266,7 @@ pub const Effects = struct {
2248
2266
 
2249
2267
  assert(side.stored().stats.hp <= 1023);
2250
2268
  // Will be 0 if HP is <= 3 meaning that the user gets a 1 HP Substitute for "free"
2251
- const hp = @intCast(u8, side.stored().stats.hp / 4);
2269
+ const hp: u8 = @intCast(side.stored().stats.hp / 4);
2252
2270
  // Pokénon Showdown incorrectly checks for 1/4 HP based on `target.maxhp / 4` which returns
2253
2271
  // a floating point value and thus only correctly implements the Substitute 1/4 glitch when
2254
2272
  // the target's HP is exactly divisible by 4 (here we're using an inlined divCeil routine to
@@ -2343,7 +2361,7 @@ pub const Effects = struct {
2343
2361
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2344
2362
  if (boosts.atk == 6) return try log.fail(ident, .None);
2345
2363
  const n: u2 = if (move.effect == .AttackUp2) 2 else 1;
2346
- boosts.atk = @intCast(i4, @min(6, @as(i8, boosts.atk) + n));
2364
+ boosts.atk = @intCast(@min(6, @as(i8, boosts.atk) + n));
2347
2365
  const reason = if (move.effect == .Rage) Boost.Rage else Boost.Attack;
2348
2366
  if (stats.atk == MAX_STAT_VALUE) {
2349
2367
  boosts.atk -= 1;
@@ -2354,7 +2372,7 @@ pub const Effects = struct {
2354
2372
  }
2355
2373
  return try log.fail(ident, .None);
2356
2374
  }
2357
- var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2375
+ var mod = BOOSTS[@intCast(@as(i8, boosts.atk) + 6)];
2358
2376
  const stat = unmodifiedStats(battle, side).atk;
2359
2377
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2360
2378
  try log.boost(ident, reason, n);
@@ -2365,7 +2383,7 @@ pub const Effects = struct {
2365
2383
  assert(boosts.def >= -6 and boosts.def <= 6);
2366
2384
  if (boosts.def == 6) return try log.fail(ident, .None);
2367
2385
  const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
2368
- boosts.def = @intCast(i4, @min(6, @as(i8, boosts.def) + n));
2386
+ boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
2369
2387
  if (stats.def == MAX_STAT_VALUE) {
2370
2388
  boosts.def -= 1;
2371
2389
  if (showdown) {
@@ -2374,7 +2392,7 @@ pub const Effects = struct {
2374
2392
  }
2375
2393
  return try log.fail(ident, .None);
2376
2394
  }
2377
- var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2395
+ var mod = BOOSTS[@intCast(@as(i8, boosts.def) + 6)];
2378
2396
  const stat = unmodifiedStats(battle, side).def;
2379
2397
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2380
2398
  try log.boost(ident, .Defense, n);
@@ -2382,7 +2400,7 @@ pub const Effects = struct {
2382
2400
  .SpeedUp2 => {
2383
2401
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2384
2402
  if (boosts.spe == 6) return try log.fail(ident, .None);
2385
- boosts.spe = @intCast(i4, @min(6, @as(i8, boosts.spe) + 2));
2403
+ boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
2386
2404
  if (stats.spe == MAX_STAT_VALUE) {
2387
2405
  boosts.spe -= 1;
2388
2406
  if (showdown) {
@@ -2391,7 +2409,7 @@ pub const Effects = struct {
2391
2409
  }
2392
2410
  return try log.fail(ident, .None);
2393
2411
  }
2394
- var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2412
+ var mod = BOOSTS[@intCast(@as(i8, boosts.spe) + 6)];
2395
2413
  const stat = unmodifiedStats(battle, side).spe;
2396
2414
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2397
2415
  try log.boost(ident, .Speed, 2);
@@ -2400,7 +2418,7 @@ pub const Effects = struct {
2400
2418
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2401
2419
  if (boosts.spc == 6) return try log.fail(ident, .None);
2402
2420
  const n: u2 = if (move.effect == .SpecialUp2) 2 else 1;
2403
- boosts.spc = @intCast(i4, @min(6, @as(i8, boosts.spc) + n));
2421
+ boosts.spc = @intCast(@min(6, @as(i8, boosts.spc) + n));
2404
2422
  if (stats.spc == MAX_STAT_VALUE) {
2405
2423
  boosts.spc -= 1;
2406
2424
  if (showdown) {
@@ -2411,7 +2429,7 @@ pub const Effects = struct {
2411
2429
  }
2412
2430
  return try log.fail(ident, .None);
2413
2431
  }
2414
- var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2432
+ var mod = BOOSTS[@intCast(@as(i8, boosts.spc) + 6)];
2415
2433
  const stat = unmodifiedStats(battle, side).spc;
2416
2434
  stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2417
2435
  try log.boost(ident, .SpecialAttack, n);
@@ -2420,7 +2438,7 @@ pub const Effects = struct {
2420
2438
  .EvasionUp1 => {
2421
2439
  assert(boosts.evasion >= -6 and boosts.evasion <= 6);
2422
2440
  if (boosts.evasion == 6) return try log.fail(ident, .None);
2423
- boosts.evasion = @intCast(i4, @min(6, @as(i8, boosts.evasion) + 1));
2441
+ boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
2424
2442
  try log.boost(ident, .Evasion, 1);
2425
2443
  },
2426
2444
  else => unreachable,
@@ -2433,12 +2451,11 @@ pub const Effects = struct {
2433
2451
  fn unboost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
2434
2452
  var foe = battle.foe(player);
2435
2453
  const foe_ident = battle.active(player.foe());
2454
+ const secondary = move.effect.isStatDownChance();
2436
2455
 
2437
- if (foe.active.volatiles.Substitute) {
2438
- return if (!move.effect.isStatDownChance()) log.fail(foe_ident, .None);
2439
- }
2456
+ if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(foe_ident, .None);
2440
2457
 
2441
- if (move.effect.isStatDownChance()) {
2458
+ if (secondary) {
2442
2459
  if (!Rolls.unboost(battle) or foe.active.volatiles.Invulnerable) return;
2443
2460
  } else if (!showdown and !try checkHit(battle, player, move, log)) {
2444
2461
  return; // checkHit already checks for Invulnerable
@@ -2446,12 +2463,13 @@ pub const Effects = struct {
2446
2463
 
2447
2464
  var stats = &foe.active.stats;
2448
2465
  var boosts = &foe.active.boosts;
2466
+ const fail = showdown or !secondary;
2449
2467
 
2450
2468
  switch (move.effect) {
2451
2469
  .AttackDown1, .AttackDownChance => {
2452
2470
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2453
- if (boosts.atk == -6) return try log.fail(foe_ident, .None);
2454
- boosts.atk = @intCast(i4, @max(-6, @as(i8, boosts.atk) - 1));
2471
+ if (boosts.atk == -6) return if (fail) try log.fail(foe_ident, .None);
2472
+ boosts.atk = @intCast(@max(-6, @as(i8, boosts.atk) - 1));
2455
2473
  if (stats.atk == 1) {
2456
2474
  boosts.atk += 1;
2457
2475
  if (showdown) {
@@ -2460,16 +2478,16 @@ pub const Effects = struct {
2460
2478
  }
2461
2479
  return try log.fail(foe_ident, .None);
2462
2480
  }
2463
- var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2481
+ var mod = BOOSTS[@intCast(@as(i8, boosts.atk) + 6)];
2464
2482
  const stat = unmodifiedStats(battle, foe).atk;
2465
2483
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2466
2484
  try log.boost(foe_ident, .Attack, -1);
2467
2485
  },
2468
2486
  .DefenseDown1, .DefenseDown2, .DefenseDownChance => {
2469
2487
  assert(boosts.def >= -6 and boosts.def <= 6);
2470
- if (boosts.def == -6) return try log.fail(foe_ident, .None);
2488
+ if (boosts.def == -6) return if (fail) try log.fail(foe_ident, .None);
2471
2489
  const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
2472
- boosts.def = @intCast(i4, @max(-6, @as(i8, boosts.def) - n));
2490
+ boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
2473
2491
  if (stats.def == 1) {
2474
2492
  boosts.def += 1;
2475
2493
  if (showdown) {
@@ -2478,15 +2496,15 @@ pub const Effects = struct {
2478
2496
  }
2479
2497
  return try log.fail(foe_ident, .None);
2480
2498
  }
2481
- var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2499
+ var mod = BOOSTS[@intCast(@as(i8, boosts.def) + 6)];
2482
2500
  const stat = unmodifiedStats(battle, foe).def;
2483
2501
  stats.def = @max(1, stat * mod[0] / mod[1]);
2484
2502
  try log.boost(foe_ident, .Defense, -@as(i8, n));
2485
2503
  },
2486
2504
  .SpeedDown1, .SpeedDownChance => {
2487
2505
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2488
- if (boosts.spe == -6) return try log.fail(foe_ident, .None);
2489
- boosts.spe = @intCast(i4, @max(-6, @as(i8, boosts.spe) - 1));
2506
+ if (boosts.spe == -6) return if (fail) try log.fail(foe_ident, .None);
2507
+ boosts.spe = @intCast(@max(-6, @as(i8, boosts.spe) - 1));
2490
2508
  if (stats.spe == 1) {
2491
2509
  boosts.spe += 1;
2492
2510
  if (showdown) {
@@ -2495,7 +2513,7 @@ pub const Effects = struct {
2495
2513
  }
2496
2514
  return try log.fail(foe_ident, .None);
2497
2515
  }
2498
- var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2516
+ var mod = BOOSTS[@intCast(@as(i8, boosts.spe) + 6)];
2499
2517
  const stat = unmodifiedStats(battle, foe).spe;
2500
2518
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2501
2519
  try log.boost(foe_ident, .Speed, -1);
@@ -2503,8 +2521,8 @@ pub const Effects = struct {
2503
2521
  },
2504
2522
  .SpecialDownChance => {
2505
2523
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2506
- if (boosts.spc == -6) return try log.fail(foe_ident, .None);
2507
- boosts.spc = @intCast(i4, @max(-6, @as(i8, boosts.spc) - 1));
2524
+ if (boosts.spc == -6) return if (fail) try log.fail(foe_ident, .None);
2525
+ boosts.spc = @intCast(@max(-6, @as(i8, boosts.spc) - 1));
2508
2526
  if (stats.spc == 1) {
2509
2527
  boosts.spc += 1;
2510
2528
  if (showdown) {
@@ -2515,7 +2533,7 @@ pub const Effects = struct {
2515
2533
  }
2516
2534
  return try log.fail(foe_ident, .None);
2517
2535
  }
2518
- var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2536
+ var mod = BOOSTS[@intCast(@as(i8, boosts.spc) + 6)];
2519
2537
  const stat = unmodifiedStats(battle, foe).spc;
2520
2538
  stats.spc = @max(1, stat * mod[0] / mod[1]);
2521
2539
  try log.boost(foe_ident, .SpecialAttack, -1);
@@ -2523,8 +2541,8 @@ pub const Effects = struct {
2523
2541
  },
2524
2542
  .AccuracyDown1 => {
2525
2543
  assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
2526
- if (boosts.accuracy == -6) return try log.fail(foe_ident, .None);
2527
- boosts.accuracy = @intCast(i4, @max(-6, @as(i8, boosts.accuracy) - 1));
2544
+ if (boosts.accuracy == -6) return if (fail) try log.fail(foe_ident, .None);
2545
+ boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
2528
2546
  try log.boost(foe_ident, .Accuracy, -1);
2529
2547
  },
2530
2548
  else => unreachable,
@@ -2608,7 +2626,7 @@ pub const Rolls = struct {
2608
2626
  }
2609
2627
 
2610
2628
  inline fn criticalHit(battle: anytype, chance: u16) bool {
2611
- if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
2629
+ if (showdown) return battle.rng.chance(u8, @as(u8, @intCast(chance)), 256);
2612
2630
  return std.math.rotl(u8, battle.rng.next(), 3) < chance;
2613
2631
  }
2614
2632
 
@@ -2621,13 +2639,13 @@ pub const Rolls = struct {
2621
2639
  }
2622
2640
 
2623
2641
  inline fn hit(battle: anytype, accuracy: u16) bool {
2624
- if (showdown) return battle.rng.chance(u8, @intCast(u8, accuracy), 256);
2642
+ if (showdown) return battle.rng.chance(u8, @as(u8, @intCast(accuracy)), 256);
2625
2643
  return battle.rng.next() < accuracy;
2626
2644
  }
2627
2645
 
2628
2646
  inline fn confusionDuration(battle: anytype) u3 {
2629
- if (showdown) return @intCast(u3, battle.rng.range(u8, 2, 6));
2630
- return @intCast(u3, (battle.rng.next() & 3) + 2);
2647
+ if (showdown) return @intCast(battle.rng.range(u8, 2, 6));
2648
+ return @intCast((battle.rng.next() & 3) + 2);
2631
2649
  }
2632
2650
 
2633
2651
  inline fn confused(battle: anytype) bool {
@@ -2656,16 +2674,16 @@ pub const Rolls = struct {
2656
2674
  }
2657
2675
 
2658
2676
  inline fn sleepDuration(battle: anytype) u3 {
2659
- if (showdown) return @intCast(u3, battle.rng.range(u8, 1, 8));
2677
+ if (showdown) return @intCast(battle.rng.range(u8, 1, 8));
2660
2678
  while (true) {
2661
2679
  const r = battle.rng.next() & 7;
2662
- if (r != 0) return @intCast(u3, r);
2680
+ if (r != 0) return @intCast(r);
2663
2681
  }
2664
2682
  }
2665
2683
 
2666
2684
  inline fn bideThrashDuration(battle: anytype) u3 {
2667
- if (showdown) return @intCast(u3, battle.rng.range(u4, 2, 4));
2668
- return @intCast(u3, (battle.rng.next() & 1) + 2);
2685
+ if (showdown) return @intCast(battle.rng.range(u4, 2, 4));
2686
+ return @intCast((battle.rng.next() & 1) + 2);
2669
2687
  }
2670
2688
 
2671
2689
  fn moveSlot(battle: anytype, moves: []MoveSlot, check_pp: u4) u4 {
@@ -2675,11 +2693,11 @@ pub const Rolls = struct {
2675
2693
  while (i > 0) {
2676
2694
  i -= 1;
2677
2695
  if (moves[i].id != .None) {
2678
- return battle.rng.range(u4, 0, @intCast(u4, i + 1)) + 1;
2696
+ return battle.rng.range(u4, 0, @as(u4, @intCast(i + 1))) + 1;
2679
2697
  }
2680
2698
  }
2681
2699
  } else {
2682
- var r = battle.rng.range(u4, 0, @intCast(u4, check_pp)) + 1;
2700
+ var r = battle.rng.range(u4, 0, check_pp) + 1;
2683
2701
  var i: usize = 0;
2684
2702
  while (i < moves.len and r > 0) : (i += 1) {
2685
2703
  if (moves[i].pp > 0) {
@@ -2687,32 +2705,32 @@ pub const Rolls = struct {
2687
2705
  if (r == 0) break;
2688
2706
  }
2689
2707
  }
2690
- return @intCast(u4, i + 1);
2708
+ return @intCast(i + 1);
2691
2709
  }
2692
2710
  }
2693
2711
 
2694
2712
  while (true) {
2695
- const r = @intCast(u4, battle.rng.next() & 3);
2713
+ const r: u4 = @intCast(battle.rng.next() & 3);
2696
2714
  if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
2697
2715
  }
2698
2716
  }
2699
2717
 
2700
2718
  inline fn disableDuration(battle: anytype) u4 {
2701
- if (showdown) return @intCast(u4, battle.rng.range(u8, 1, 9));
2702
- return @intCast(u4, (battle.rng.next() & 7) + 1);
2719
+ if (showdown) return @intCast(battle.rng.range(u8, 1, 9));
2720
+ return @intCast((battle.rng.next() & 7) + 1);
2703
2721
  }
2704
2722
 
2705
2723
  inline fn metronome(battle: anytype) Move {
2706
2724
  if (showdown) {
2707
- const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
2708
- const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
2709
- return @intToEnum(Move, r + mod);
2725
+ const r = battle.rng.range(u8, 0, @intFromEnum(Move.Struggle) - 2);
2726
+ const mod = @as(u2, (if (r < @intFromEnum(Move.Metronome) - 1) 1 else 2));
2727
+ return @enumFromInt(r + mod);
2710
2728
  }
2711
2729
  while (true) {
2712
2730
  const r = battle.rng.next();
2713
- if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
2714
- if (r >= @enumToInt(Move.Struggle)) continue;
2715
- return @intToEnum(Move, r);
2731
+ if (r == 0 or r == @intFromEnum(Move.Metronome)) continue;
2732
+ if (r >= @intFromEnum(Move.Struggle)) continue;
2733
+ return @enumFromInt(r);
2716
2734
  }
2717
2735
  }
2718
2736
 
@@ -2727,7 +2745,7 @@ pub const Rolls = struct {
2727
2745
  inline fn distribution(battle: anytype) u3 {
2728
2746
  if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
2729
2747
  const r = (battle.rng.next() & 3);
2730
- return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
2748
+ return @intCast((if (r < 2) r else battle.rng.next() & 3) + 2);
2731
2749
  }
2732
2750
 
2733
2751
  inline fn unboost(battle: anytype) bool {
@@ -2740,7 +2758,7 @@ test "RNG agreement" {
2740
2758
  if (!showdown) return;
2741
2759
  var expected: [256]u32 = undefined;
2742
2760
  for (0..expected.len) |i| {
2743
- expected[i] = @intCast(u32, i * 0x1000000);
2761
+ expected[i] = @intCast(i * 0x1000000);
2744
2762
  }
2745
2763
 
2746
2764
  var spe = rng.FixedRNG(1, expected.len){ .rolls = expected };
@@ -2797,7 +2815,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
2797
2815
  // strictly correct as the player would not have been presented the option to move or
2798
2816
  // switch at all.
2799
2817
  if (isForced(active)) {
2800
- out[n] = .{ .type = .Move, .data = @boolToInt(showdown) };
2818
+ out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
2801
2819
  n += 1;
2802
2820
  return n;
2803
2821
  }