@pkmn/engine 0.1.0-dev.89cc1c98 → 0.1.0-dev.900756a9

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.
@@ -37,6 +37,7 @@ const Gen12 = rng.Gen12;
37
37
  const PointerType = util.PointerType;
38
38
 
39
39
  const ActivePokemon = data.ActivePokemon;
40
+ const Boosts = data.Boosts;
40
41
  const Effectiveness = data.Effectiveness;
41
42
  const Item = data.Item;
42
43
  const Move = data.Move;
@@ -50,6 +51,7 @@ const TriAttack = data.TriAttack;
50
51
  const Type = data.Type;
51
52
  const Types = data.Types;
52
53
  const Volatiles = data.Volatiles;
54
+ const Weather = data.Weather;
53
55
 
54
56
  // zig fmt: off
55
57
  const STAT_BOOSTS = &[_][2]u8{
@@ -95,10 +97,13 @@ const CRITICAL_HITS = &[_]u8{
95
97
 
96
98
  const MAX_STAT_VALUE = 999;
97
99
 
100
+ const STATS = [_][]const u8{ "atk", "def", "spe", "spa", "spd" };
101
+
98
102
  pub const State = struct {
99
103
  damage: u16 = 0, // wCurDamage
100
104
  effectiveness: u16 = Effectiveness.neutral, // wTypeModifier
101
105
  bp: u8, // wPlayerMoveStructPower
106
+ type: Type, // BATTLE_VARS_MOVE_TYPE
102
107
  accuracy: u8, // wEnemyMoveStruct + MOVE_ACC
103
108
  move: Move, // wCurPlayerMove
104
109
  from: Move = .None,
@@ -110,7 +115,7 @@ pub const State = struct {
110
115
  proc: bool = true, // wEffectFailed
111
116
 
112
117
  comptime {
113
- assert(@sizeOf(State) == 14);
118
+ assert(@sizeOf(State) == 16);
114
119
  }
115
120
 
116
121
  pub fn immune(self: *State) bool {
@@ -269,7 +274,13 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice, options: anytype) !Player
269
274
  // TODO: add to pending and only commit roll on Pokémon Showdown if roll is used
270
275
  const qkc = showdown and try Rolls.quickClaw(battle, .P1, options);
271
276
 
272
- if ((c1.type == .Switch) != (c2.type == .Switch)) return if (c1.type == .Switch) .P1 else .P2;
277
+ if ((c1.type == .Switch) != (c2.type == .Switch)) {
278
+ const m1 = .Pound; // TODO battle.side(.P1).last_selected_move;
279
+ const m2 = .Pound; // TODO battle.side(.P2).last_selected_move;
280
+ return if (c1.type == .Switch)
281
+ if (m1 == .Pursuit) .P2 else .P1
282
+ else if (m2 == .Pursuit) .P1 else .P2;
283
+ }
273
284
 
274
285
  // https://www.smogon.com/forums/threads/adv-switch-priority.3622189/
275
286
  // > In Gen 1 it's irrelevant [which player switches first] because switches happen instantly on
@@ -398,8 +409,14 @@ fn executeMove(
398
409
 
399
410
  // FIXME: need damage from doTurn
400
411
  const m = Move.get(move);
401
- var state: State =
402
- .{ .move = move, .mslot = mslot, .first = first, .accuracy = m.accuracy, .bp = m.bp };
412
+ var state: State = .{
413
+ .move = move,
414
+ .mslot = mslot,
415
+ .first = first,
416
+ .accuracy = m.accuracy,
417
+ .bp = m.bp,
418
+ .type = m.type,
419
+ };
403
420
  try generated.runMove(battle, player, &state, options);
404
421
  return null;
405
422
  }
@@ -631,7 +648,7 @@ pub fn afterMove(
631
648
 
632
649
  pub fn checkCriticalHit(battle: anytype, player: Player, state: *State, options: anytype) !void {
633
650
  const move = Move.get(state.move);
634
- if (move.bp == 0) return;
651
+ if (state.bp == 0) return;
635
652
 
636
653
  const side = battle.side(player);
637
654
  const pokemon = side.stored();
@@ -669,31 +686,30 @@ pub fn adjustDamage(battle: anytype, player: Player, state: *State, _: anytype)
669
686
  const side = battle.side(player);
670
687
  const foe = battle.foe(player);
671
688
  const types = foe.active.types;
672
- const move = Move.get(state.move);
673
689
 
674
690
  var d = state.damage;
675
- if ((move.type == .Water and battle.field.weather == .Rain) or
676
- (move.type == .Fire and battle.field.weather == .Sun))
691
+ if ((state.type == .Water and battle.field.weather == .Rain) or
692
+ (state.type == .Fire and battle.field.weather == .Sun))
677
693
  {
678
694
  d = d *% @intFromEnum(Effectiveness.Super) / 10;
679
- } else if ((move.type == .Water and battle.field.weather == .Sun) or
680
- ((move.type == .Fire or state.move == .SolarBeam) and battle.field.weather == .Rain))
695
+ } else if ((state.type == .Water and battle.field.weather == .Sun) or
696
+ ((state.type == .Fire or state.move == .SolarBeam) and battle.field.weather == .Rain))
681
697
  {
682
698
  d = d *% @intFromEnum(Effectiveness.Resisted) / 10;
683
699
  }
684
700
 
685
- if (side.active.types.includes(move.type)) d +%= d / 2;
701
+ if (side.active.types.includes(state.type)) d +%= d / 2;
686
702
 
687
703
  const neutral = @intFromEnum(Effectiveness.Neutral);
688
- const foresight = foe.active.volatiles.Foresight and FORESIGHT.includes(move.type);
704
+ const foresight = foe.active.volatiles.Foresight and FORESIGHT.includes(state.type);
689
705
  const eff1: u16 = if (foresight and types.type1 == .Ghost)
690
706
  neutral
691
707
  else
692
- @intFromEnum(move.type.effectiveness(types.type1));
708
+ @intFromEnum(state.type.effectiveness(types.type1));
693
709
  const eff2: u16 = if (foresight and types.type2 == .Ghost)
694
710
  neutral
695
711
  else
696
- @intFromEnum(move.type.effectiveness(types.type2));
712
+ @intFromEnum(state.type.effectiveness(types.type2));
697
713
 
698
714
  // Type effectiveness matchup precedence only matters with (NVE, SE)
699
715
  if (!showdown and (eff1 + eff2) == Effectiveness.mismatch and
@@ -775,9 +791,15 @@ pub fn checkHit(
775
791
  return;
776
792
  }
777
793
  }
778
- if (move.effect == .Thunder and battle.field.weather == .Rain) {
779
- state.miss = false;
780
- return;
794
+ if (move.effect == .Thunder) {
795
+ if (battle.field.weather == .Rain) {
796
+ state.miss = false;
797
+ return;
798
+ } else if (battle.field.weather == .Sun) {
799
+ // This latter part gets applied in a separate Thunder accuracy handler
800
+ // on the cartridge but it makes more sense to just do it here
801
+ state.accuracy = Gen12.percent(50) + 1;
802
+ }
781
803
  }
782
804
  if (move.effect == .AlwaysHit) {
783
805
  state.miss = false;
@@ -931,7 +953,7 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !bool {
931
953
  const before = foe_stored.hp;
932
954
  foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
933
955
  // Pokémon Showdown uses the less specific heal here instead of drain... because reasons?
934
- if (foe_stored.hp > before) try options.log.heal(foe_ident, foe_stored, .Silent);
956
+ if (foe_stored.hp > before) try options.log.heal(.{ foe_ident, foe_stored, Heal.Silent });
935
957
  if (stored.hp == 0) return true;
936
958
  }
937
959
 
@@ -1047,7 +1069,9 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1047
1069
  if (stored.item == .Leftovers) {
1048
1070
  const before = stored.hp;
1049
1071
  stored.hp = @min(stored.hp + @max(stored.stats.hp / 16, 1), stored.stats.hp);
1050
- if (stored.hp > before) try options.log.heal(battle.active(player), stored, .Leftovers);
1072
+ if (stored.hp > before) {
1073
+ try options.log.heal(.{ battle.active(player), stored, Heal.Leftovers });
1074
+ }
1051
1075
  }
1052
1076
  }
1053
1077
 
@@ -1116,7 +1140,7 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1116
1140
  assert(offset < 3);
1117
1141
  stored.hp = @min(stored.hp + ((offset + 1) * 10), stored.stats.hp);
1118
1142
  const reason: Heal = @enumFromInt(@intFromEnum(Heal.Berry) + offset);
1119
- try options.log.heal(ident, stored, reason);
1143
+ try options.log.heal(.{ ident, stored, reason });
1120
1144
  }
1121
1145
  } else if (num < @intFromEnum(Item.BitterBerry)) {
1122
1146
  const proc = if (stored.item == .MiracleBerry)
@@ -1131,7 +1155,7 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1131
1155
 
1132
1156
  // FIXME wtf?
1133
1157
  // side.active.volatiles.Toxic = false;
1134
- // side.active.volatiles.NightMare = false;
1158
+ // side.active.volatiles.Nightmare = false;
1135
1159
  if (stored.item == .MiracleBerry) {
1136
1160
  side.active.volatiles.Confusion = false;
1137
1161
  try options.log.end(ident, .Confusion);
@@ -1247,10 +1271,6 @@ pub fn destinyBond(battle: anytype, player: Player, _: *State, options: anytype)
1247
1271
  }
1248
1272
 
1249
1273
  pub const Effects = struct {
1250
- pub fn alwaysHit(battle: anytype, player: Player, state: *State, options: anytype) !void {
1251
- _ = .{ battle, player, state, options }; // TODO
1252
- }
1253
-
1254
1274
  pub fn attract(battle: anytype, player: Player, _: *State, options: anytype) !void {
1255
1275
  const side = battle.side(player);
1256
1276
  var foe = battle.foe(player);
@@ -1258,12 +1278,10 @@ pub const Effects = struct {
1258
1278
  const gender: u8 = @intFromEnum(side.stored().dvs.gender);
1259
1279
  const foe_gender: u8 = @intFromEnum(foe.stored().dvs.gender);
1260
1280
  if (gender + foe_gender != 1) {
1261
- try options.log.immune(battle.active(player.foe()), .None);
1262
- return;
1281
+ return options.log.immune(battle.active(player.foe()), .None);
1263
1282
  } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
1264
1283
  try options.log.lastmiss();
1265
- try options.log.miss(battle.active(player));
1266
- return;
1284
+ return options.log.miss(battle.active(player));
1267
1285
  }
1268
1286
 
1269
1287
  foe.active.volatiles.Attract = true;
@@ -1280,14 +1298,59 @@ pub const Effects = struct {
1280
1298
 
1281
1299
  pub fn bellyDrum(battle: anytype, player: Player, state: *State, options: anytype) !void {
1282
1300
  _ = .{ battle, player, state, options }; // TODO
1301
+ // assert(!state.miss);
1302
+ // var side = battle.side(player);
1303
+ // try boostStat(battle, side, &side.active.stats, &side.active.boosts, "atk", 2);
1304
+ // TODO
1305
+ // |-boost|p1a: Poliwag|atk|2|[silent]
1306
+ // |-fail|p1a: Poliwag
1307
+ //
1283
1308
  }
1284
1309
 
1285
- pub fn bide(battle: anytype, player: Player, state: *State, options: anytype) !void {
1286
- _ = .{ battle, player, state, options }; // TODO
1287
- }
1310
+ pub const bide = struct {
1311
+ pub fn store(battle: anytype, player: Player, state: *State, options: anytype) !void {
1312
+ var side = battle.side(player);
1313
+ var volatiles = &side.active.volatiles;
1314
+
1315
+ const ident = battle.active(player);
1316
+
1317
+ if (!volatiles.Bide) return;
1318
+
1319
+ assert(volatiles.attacks > 0);
1320
+ // if (options.calc.modify(player, .attacking)) |extend| {
1321
+ // if (!extend) volatiles.attacks = 0;
1322
+ // } else {
1323
+ volatiles.attacks -= 1;
1324
+ // }
1325
+ try options.chance.attacking(player, volatiles.attacks);
1326
+
1327
+ if (volatiles.attacks != 0) return options.log.activate(ident, .Bide);
1328
+
1329
+ volatiles.Bide = false;
1330
+ try options.log.end(ident, .Bide);
1331
+
1332
+ state.damage = volatiles.bide *% 2;
1333
+ volatiles.bide = 0;
1334
+
1335
+ if (state.damage == 0) state.miss = true;
1336
+ }
1337
+
1338
+ pub fn unleash(battle: anytype, player: Player, state: *State, options: anytype) !void {
1339
+ _ = .{ battle, player, state, options }; // TODO
1340
+ }
1341
+ };
1288
1342
 
1289
1343
  pub fn binding(battle: anytype, player: Player, state: *State, options: anytype) !void {
1290
- _ = .{ battle, player, state, options }; // TODO
1344
+ var foe = battle.foe(player);
1345
+
1346
+ if (state.miss) return;
1347
+ if (foe.active.volatiles.bind.duration > 0) return;
1348
+ if (foe.active.volatiles.Substitute) return;
1349
+
1350
+ foe.active.volatiles.bind = .{
1351
+ .duration = Rolls.bindingDuration(battle, player, options),
1352
+ .reason = Move.get(state.move).extra.protocol,
1353
+ };
1291
1354
  }
1292
1355
 
1293
1356
  pub fn burnChance(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1297,7 +1360,7 @@ pub const Effects = struct {
1297
1360
  if (foe.active.volatiles.Substitute) return;
1298
1361
  if (Status.any(foe_stored.status)) {
1299
1362
  if (Status.is(foe_stored.status, .FRZ)) {
1300
- assert(Move.get(state.move).type == .Fire);
1363
+ assert(Move.get(state.move).type == .Fire or state.move == .TriAttack);
1301
1364
  try options.log.curestatus(
1302
1365
  battle.active(player.foe()),
1303
1366
  foe_stored.status,
@@ -1311,7 +1374,9 @@ pub const Effects = struct {
1311
1374
  if (foe.conditions.Safeguard) return;
1312
1375
  // Nothing is immune to Fire-type, but Ghosts are immune to burnChance called by Tri Attack
1313
1376
  if (state.immune()) return;
1314
- if (foe.active.types.includes(Move.get(state.move).type)) return;
1377
+ if (foe.active.types.includes(Move.get(state.move).type) and state.move != .TriAttack) {
1378
+ return;
1379
+ }
1315
1380
 
1316
1381
  foe_stored.status = Status.init(.BRN);
1317
1382
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
@@ -1335,10 +1400,7 @@ pub const Effects = struct {
1335
1400
  for (side.active.moves) |m| {
1336
1401
  if (convertible(side.active, m.id)) n += 1;
1337
1402
  }
1338
- if (n == 0) {
1339
- try options.log.fail(battle.active(player), .None);
1340
- return;
1341
- }
1403
+ if (n == 0) return options.log.fail(battle.active(player), .None);
1342
1404
 
1343
1405
  side.active.types.type1 = try Rolls.conversion(battle, player, n, options);
1344
1406
  side.active.types.type2 = side.active.types.type1;
@@ -1348,26 +1410,87 @@ pub const Effects = struct {
1348
1410
  var side = battle.side(player);
1349
1411
 
1350
1412
  const last = battle.foe(player).lastMove(false);
1351
- if (last == .None) {
1352
- try options.log.fail(battle.active(player), .None);
1353
- return;
1354
- }
1413
+ if (last == .None) return options.log.fail(battle.active(player), .None);
1355
1414
  const move = Move.get(last);
1356
- if (move.type == .@"???") {
1357
- try options.log.fail(battle.active(player), .None);
1358
- return;
1359
- }
1415
+ if (move.type == .@"???") return options.log.fail(battle.active(player), .None);
1360
1416
 
1361
1417
  side.active.types.type1 = try Rolls.conversion2(battle, player, move.type, options);
1362
1418
  side.active.types.type2 = side.active.types.type1;
1363
1419
  }
1364
1420
 
1365
- pub fn counter(battle: anytype, player: Player, state: *State, options: anytype) !void {
1366
- _ = .{ battle, player, state, options }; // TODO
1421
+ pub fn counter(battle: anytype, player: Player, state: *State, _: anytype) !void {
1422
+ assert(state.move == .Counter or state.move == .MirrorCoat);
1423
+
1424
+ const foe = battle.foe(player);
1425
+
1426
+ state.miss = true;
1427
+ const last = foe.lastMove(false);
1428
+
1429
+ if (last == .None) return;
1430
+ if (last == state.move) return;
1431
+
1432
+ const immune = state.immune();
1433
+ state.effectiveness = Effectiveness.neutral;
1434
+ if (immune) {
1435
+ state.damage = 0;
1436
+ return;
1437
+ }
1438
+
1439
+ const move = Move.get(last);
1440
+ if (state.bp == 0) return;
1441
+ if (move.type.special() != (last == .MirrorCoat)) return;
1442
+ if (state.damage == 0) return;
1443
+
1444
+ state.damage *|= 2;
1445
+ state.miss = false;
1367
1446
  }
1368
1447
 
1369
1448
  pub fn curse(battle: anytype, player: Player, state: *State, options: anytype) !void {
1370
- _ = .{ battle, player, state, options }; // TODO
1449
+ var side = battle.side(player);
1450
+ var foe = battle.foe(player);
1451
+
1452
+ const ident = battle.active(player);
1453
+ const foe_ident = battle.active(player.foe());
1454
+
1455
+ if (side.active.types.includes(.Ghost)) {
1456
+ if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
1457
+ try options.log.lastmiss();
1458
+ return options.log.miss(battle.active(player));
1459
+ } else if (foe.active.volatiles.Substitute) {
1460
+ return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
1461
+ } else if (foe.active.volatiles.Curse) {
1462
+ return options.log.fail(foe_ident, .None);
1463
+ }
1464
+
1465
+ foe.active.volatiles.Curse = true;
1466
+ try options.log.start(foe_ident, .Curse);
1467
+
1468
+ side.stored().hp -= @max(side.stored().stats.hp / 2, 1);
1469
+ try options.log.damage(ident, side.stored(), .Curse);
1470
+ } else {
1471
+ var stats = &side.active.stats;
1472
+ var boosts = &side.active.boosts;
1473
+
1474
+ const max_atk = boosts.atk == 6 or stats.atk == MAX_STAT_VALUE;
1475
+ const max_def = boosts.def == 6 or stats.def == MAX_STAT_VALUE;
1476
+ if (max_atk and max_def) return options.log.fail(ident, .None);
1477
+
1478
+ if (!(boosts.spe == -6 or stats.spe == 1)) {
1479
+ assert(boosts.spe >= -6 and boosts.spe <= 6);
1480
+ try unboostStat(battle, side, stats, boosts, "spe", 1);
1481
+ try options.log.boost(ident, .Speed, @as(i8, -1));
1482
+ }
1483
+ if (!max_atk) {
1484
+ assert(boosts.atk >= -6 and boosts.atk <= 6);
1485
+ try boostStat(battle, side, stats, boosts, "atk", 1);
1486
+ try options.log.boost(ident, .Attack, @as(i8, 1));
1487
+ }
1488
+ if (!max_def) {
1489
+ assert(boosts.def >= -6 and boosts.def <= 6);
1490
+ try boostStat(battle, side, stats, boosts, "def", 1);
1491
+ try options.log.boost(ident, .Defense, @as(i8, 1));
1492
+ }
1493
+ }
1371
1494
  }
1372
1495
 
1373
1496
  pub fn defenseCurl(battle: anytype, player: Player, _: *State, _: anytype) !void {
@@ -1393,14 +1516,12 @@ pub const Effects = struct {
1393
1516
  const foe_ident = battle.active(player.foe());
1394
1517
 
1395
1518
  if (volatiles.disable.move != 0) {
1396
- try options.log.fail(foe_ident, .None);
1397
- return;
1519
+ return options.log.fail(foe_ident, .None);
1398
1520
  }
1399
1521
 
1400
1522
  const last = battle.foe(player).lastMove(false);
1401
1523
  if (last == .None or last == .Struggle) {
1402
- try options.log.fail(foe_ident, .None);
1403
- return;
1524
+ return options.log.fail(foe_ident, .None);
1404
1525
  }
1405
1526
 
1406
1527
  var slot: u3 = 0;
@@ -1410,10 +1531,7 @@ pub const Effects = struct {
1410
1531
  if (m.id == last) break;
1411
1532
  }
1412
1533
  const move = foe.active.move(slot);
1413
- if (move.pp == 0) {
1414
- try options.log.fail(foe_ident, .None);
1415
- return;
1416
- }
1534
+ if (move.pp == 0) return options.log.fail(foe_ident, .None);
1417
1535
 
1418
1536
  volatiles.disable.move = slot;
1419
1537
  volatiles.disable.duration = Rolls.disableDuration(battle, player, options);
@@ -1421,9 +1539,19 @@ pub const Effects = struct {
1421
1539
  try options.log.startEffect(foe_ident, .Disable, move.id);
1422
1540
  }
1423
1541
 
1424
- pub fn doubleHit(battle: anytype, player: Player, state: *State, options: anytype) !void {
1425
- _ = .{ battle, player, state, options }; // TODO
1426
- }
1542
+ pub const double = struct {
1543
+ pub fn flying(battle: anytype, player: Player, state: *State, _: anytype) !void {
1544
+ if (battle.foe(player).active.volatiles.Flying) state.damage *|= 2;
1545
+ }
1546
+
1547
+ pub fn underground(battle: anytype, player: Player, state: *State, _: anytype) !void {
1548
+ if (battle.foe(player).active.volatiles.Underground) state.damage *|= 2;
1549
+ }
1550
+
1551
+ pub fn minimize(battle: anytype, player: Player, state: *State, _: anytype) !void {
1552
+ if (battle.foe(player).active.volatiles.minimized) state.damage *|= 2;
1553
+ }
1554
+ };
1427
1555
 
1428
1556
  pub fn drainHP(battle: anytype, player: Player, state: *State, options: anytype) !void {
1429
1557
  var stored = battle.side(player).stored();
@@ -1445,10 +1573,7 @@ pub const Effects = struct {
1445
1573
  const failed = volatiles.Encore or
1446
1574
  (last == .None or last == .Struggle or
1447
1575
  last == .Encore or .last == .MirrorMove);
1448
- if (failed) {
1449
- try options.log.fail(foe_ident, .None);
1450
- return;
1451
- }
1576
+ if (failed) return options.log.fail(foe_ident, .None);
1452
1577
 
1453
1578
  var slot: u3 = 0;
1454
1579
  for (foe.active.moves) |m| {
@@ -1457,10 +1582,7 @@ pub const Effects = struct {
1457
1582
  if (m.id == last) break;
1458
1583
  }
1459
1584
  const move = foe.active.move(slot);
1460
- if (move.pp == 0) {
1461
- try options.log.fail(foe_ident, .None);
1462
- return;
1463
- }
1585
+ if (move.pp == 0) return options.log.fail(foe_ident, .None);
1464
1586
 
1465
1587
  volatiles.Encore = true;
1466
1588
  volatiles.encore = Rolls.encoreDuration(battle, player, options);
@@ -1500,10 +1622,6 @@ pub const Effects = struct {
1500
1622
  };
1501
1623
  }
1502
1624
 
1503
- pub fn flameWheel(battle: anytype, player: Player, state: *State, options: anytype) !void {
1504
- _ = .{ battle, player, state, options }; // TODO
1505
- }
1506
-
1507
1625
  pub fn flinchChance(battle: anytype, player: Player, state: *State, _: anytype) !void {
1508
1626
  const foe = battle.foe(player);
1509
1627
  const status = foe.stored().status;
@@ -1541,7 +1659,7 @@ pub const Effects = struct {
1541
1659
 
1542
1660
  const foe = battle.foe(player);
1543
1661
 
1544
- if (state.first) return try options.log.fail(battle.active(player), .None);
1662
+ if (state.first) return options.log.fail(battle.active(player), .None);
1545
1663
 
1546
1664
  var n: u4 = 0;
1547
1665
  var i: u4 = 0;
@@ -1555,10 +1673,7 @@ pub const Effects = struct {
1555
1673
  }
1556
1674
  }
1557
1675
 
1558
- if (n == 0) {
1559
- try options.log.fail(battle.active(player), .None);
1560
- return;
1561
- }
1676
+ if (n == 0) return options.log.fail(battle.active(player), .None);
1562
1677
 
1563
1678
  const slot = try Rolls.forceSwitch(battle, player, SLOTS[0..i], n, options);
1564
1679
  try switchIn(battle, player.foe(), slot, .Drag, options);
@@ -1569,11 +1684,9 @@ pub const Effects = struct {
1569
1684
 
1570
1685
  if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
1571
1686
  try options.log.lastmiss();
1572
- try options.log.miss(battle.active(player));
1573
- return;
1687
+ return options.log.miss(battle.active(player));
1574
1688
  } else if (foe.active.volatiles.Foresight) {
1575
- try options.log.fail(battle.active(player), .None);
1576
- return;
1689
+ return options.log.fail(battle.active(player), .None);
1577
1690
  }
1578
1691
 
1579
1692
  foe.active.volatiles.Foresight = true;
@@ -1589,8 +1702,11 @@ pub const Effects = struct {
1589
1702
  if (Status.any(foe_stored.status)) return;
1590
1703
 
1591
1704
  // Nothing is immune to Ice-type, but Ghosts are immune to freezeChance called by Tri Attack
1705
+ if (state.immune()) return;
1592
1706
  if (battle.field.weather == .Sun) return;
1593
- if (foe.active.types.includes(Move.get(state.move).type)) return;
1707
+ if (foe.active.types.includes(Move.get(state.move).type) and state.move != .TriAttack) {
1708
+ return;
1709
+ }
1594
1710
  if (!state.proc) return;
1595
1711
  if (foe.conditions.Safeguard) return;
1596
1712
  // Freeze Clause Mod
@@ -1619,73 +1735,200 @@ pub const Effects = struct {
1619
1735
  state.bp = ((if (frustration) 255 - val else val) * 10) / 25;
1620
1736
  }
1621
1737
 
1622
- pub fn haze(battle: anytype, player: Player, state: *State, options: anytype) !void {
1623
- _ = .{ battle, player, state, options }; // TODO
1738
+ pub fn haze(battle: anytype, player: Player, _: *State, options: anytype) !void {
1739
+ var side = battle.side(player);
1740
+ var stats = unmodifiedStats(battle, side);
1741
+ side.active.boosts = .{};
1742
+ inline for (STATS) |s| @field(side.active.stats, s) = @field(stats, s);
1743
+ statusModify(side.stored().status, &side.active.stats);
1744
+
1745
+ var foe = battle.foe(player);
1746
+ stats = unmodifiedStats(battle, foe);
1747
+ foe.active.boosts = .{};
1748
+ inline for (STATS) |s| @field(foe.active.stats, s) = @field(stats, s);
1749
+ statusModify(foe.stored().status, &foe.active.stats);
1750
+
1751
+ try options.log.clearallboost();
1624
1752
  }
1625
1753
 
1626
1754
  pub fn heal(battle: anytype, player: Player, state: *State, options: anytype) !void {
1627
- _ = .{ battle, player, state, options }; // TODO
1628
- }
1755
+ var side = battle.side(player);
1756
+ var stored = side.stored();
1757
+ const ident = battle.active(player);
1629
1758
 
1630
- pub fn healBell(battle: anytype, player: Player, state: *State, options: anytype) !void {
1631
- _ = .{ battle, player, state, options }; // TODO
1759
+ if (stored.stats.hp == stored.hp) return options.log.fail(ident, .None);
1760
+
1761
+ const rest = state.move == .Rest;
1762
+ if (rest) {
1763
+ // Adding the sleep status runs the sleep condition handler to roll duration
1764
+ if (showdown) battle.rng.advance(1);
1765
+ side.active.volatiles.Toxic = false;
1766
+ stored.status = Status.slf(2);
1767
+ recomputeStats(battle, side);
1768
+ try options.log.statusFrom(ident, stored.status, Move.Rest);
1769
+ stored.hp = stored.stats.hp;
1770
+ } else {
1771
+ stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
1772
+ }
1773
+ try options.log.heal(.{ ident, stored, if (rest) Heal.Silent else Heal.None });
1632
1774
  }
1633
1775
 
1634
- pub fn hiddenPower(battle: anytype, player: Player, state: *State, options: anytype) !void {
1635
- _ = .{ battle, player, state, options }; // TODO
1776
+ pub fn healBell(battle: anytype, player: Player, _: *State, options: anytype) !void {
1777
+ var side = battle.side(player);
1778
+ side.active.volatiles.Nightmare = false;
1779
+
1780
+ try options.log.cureteam(battle.active(player));
1781
+ for (&side.pokemon) |*p| p.status = 0;
1782
+
1783
+ // We technically only need to recompute stats if the active Pokémon was burned/paralyzed,
1784
+ // but adding a branch to check for that is probably more expensive than just recomputing
1785
+ recomputeStats(battle, side);
1636
1786
  }
1637
1787
 
1638
- pub fn hyperBeam(battle: anytype, player: Player, state: *State, options: anytype) !void {
1639
- _ = .{ battle, player, state, options }; // TODO
1788
+ pub fn hiddenPower(battle: anytype, player: Player, state: *State, _: anytype) !void {
1789
+ const side = battle.side(player);
1790
+ state.bp = side.stored().dvs.power();
1791
+ state.type = side.stored().dvs.type;
1640
1792
  }
1641
1793
 
1642
- pub fn jumpKick(battle: anytype, player: Player, state: *State, options: anytype) !void {
1643
- _ = .{ battle, player, state, options }; // TODO
1794
+ pub fn hyperBeam(battle: anytype, player: Player, _: *State, options: anytype) !void {
1795
+ battle.side(player).active.volatiles.Recharging = true;
1796
+ try options.log.mustrecharge(battle.active(player));
1644
1797
  }
1645
1798
 
1646
1799
  pub fn leechSeed(battle: anytype, player: Player, state: *State, options: anytype) !void {
1647
- _ = .{ battle, player, state, options }; // TODO
1800
+ var foe = battle.foe(player);
1801
+
1802
+ if (showdown) {
1803
+ // Invulnerability trumps type immunity on Pokémon Showdown
1804
+ const invulnerable = foe.active.volatiles.Underground or foe.active.volatiles.Flying;
1805
+ if (!invulnerable and foe.active.types.includes(.Grass)) {
1806
+ return options.log.immune(battle.active(player.foe()), .None);
1807
+ }
1808
+ try checkHit(battle, player, state, options);
1809
+ if (state.miss) {
1810
+ try options.log.lastmiss();
1811
+ return options.log.miss(battle.active(player));
1812
+ }
1813
+ if (foe.active.volatiles.LeechSeed) return;
1814
+ } else {
1815
+ try checkHit(battle, player, state, options);
1816
+ const miss =
1817
+ state.miss or foe.active.types.includes(.Grass) or foe.active.volatiles.LeechSeed;
1818
+ if (miss) {
1819
+ try options.log.lastmiss();
1820
+ return options.log.miss(battle.active(player));
1821
+ }
1822
+ }
1823
+
1824
+ foe.active.volatiles.LeechSeed = true;
1825
+
1826
+ try options.log.start(battle.active(player.foe()), .LeechSeed);
1648
1827
  }
1649
1828
 
1650
1829
  pub fn lockOn(battle: anytype, player: Player, state: *State, options: anytype) !void {
1651
- _ = .{ battle, player, state, options }; // TODO
1830
+ var foe = battle.foe(player);
1831
+ const foe_ident = battle.active(player.foe());
1832
+
1833
+ if (foe.active.volatiles.Substitute) {
1834
+ return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
1835
+ } else if (state.miss) {
1836
+ return options.log.fail(foe_ident, .None);
1837
+ }
1838
+
1839
+ foe.active.volatiles.LockOn = true;
1840
+ try options.log.activate(battle.active(player), .LockOn);
1652
1841
  }
1653
1842
 
1654
1843
  const MAGNITUDE_POWER = [_]u8{ 10, 30, 50, 70, 90, 110, 150 };
1655
1844
 
1656
1845
  pub fn magnitude(battle: anytype, player: Player, state: *State, options: anytype) !void {
1657
- _ = .{ battle, player, state, options }; // TODO
1846
+ const num = try Rolls.magnitude(battle, player, options);
1847
+ try options.log.magnitude(battle.active(player), num);
1848
+ state.bp = MAGNITUDE_POWER[num - 4];
1658
1849
  }
1659
1850
 
1660
- pub fn meanLook(battle: anytype, player: Player, state: *State, options: anytype) !void {
1661
- _ = .{ battle, player, state, options }; // TODO
1851
+ pub fn meanLook(battle: anytype, player: Player, _: *State, options: anytype) !void {
1852
+ var foe = battle.foe(player);
1853
+
1854
+ if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
1855
+ if (!showdown) return options.log.fail(battle.active(player.foe()), .None);
1856
+ try options.log.lastmiss();
1857
+ return options.log.miss(battle.active(player));
1858
+ } else if (foe.active.volatiles.Trapped) {
1859
+ return options.log.fail(battle.active(player.foe()), .None);
1860
+ }
1861
+
1862
+ foe.active.volatiles.Trapped = true;
1662
1863
  }
1663
1864
 
1664
- pub fn metronome(battle: anytype, player: Player, state: *State, options: anytype) !void {
1665
- _ = .{ battle, player, state, options }; // TODO
1865
+ pub fn metronome(
1866
+ battle: anytype,
1867
+ player: Player,
1868
+ state: *State,
1869
+ options: anytype,
1870
+ ) anyerror!void {
1871
+ var side = battle.side(player);
1872
+
1873
+ side.last_used_move = .None;
1874
+ side.active.volatiles.dirty = false;
1875
+
1876
+ // if (wPlayerCharging) ???
1877
+ // wPlayerCharging = 1
1878
+
1879
+ // FIXME
1880
+ state.move = try Rolls.metronome(battle, player, options);
1881
+ state.from = .Metronome;
1882
+ return generated.runMove(battle, player, state, options);
1666
1883
  }
1667
1884
 
1668
1885
  pub fn mimic(battle: anytype, player: Player, state: *State, options: anytype) !void {
1669
1886
  _ = .{ battle, player, state, options }; // TODO
1670
1887
  }
1671
1888
 
1672
- pub fn mirrorCoat(battle: anytype, player: Player, state: *State, options: anytype) !void {
1673
- _ = .{ battle, player, state, options }; // TODO
1674
- }
1889
+ pub fn mirrorMove(
1890
+ battle: anytype,
1891
+ player: Player,
1892
+ state: *State,
1893
+ options: anytype,
1894
+ ) anyerror!void {
1895
+ var side = battle.side(player);
1896
+ const foe = battle.foe(player);
1675
1897
 
1676
- pub fn mirrorMove(battle: anytype, player: Player, state: *State, options: anytype) !void {
1677
- _ = .{ battle, player, state, options }; // TODO
1898
+ side.last_used_move = .None;
1899
+ side.active.volatiles.dirty = false;
1900
+
1901
+ const last = foe.lastMove(false);
1902
+
1903
+ if (last == .None) return options.log.fail(battle.active(player), .None);
1904
+ const knows = knows: for (side.active.moves) |m| {
1905
+ if (last == m.id) break :knows true;
1906
+ } else false;
1907
+ if (knows) return options.log.fail(battle.active(player), .None);
1908
+
1909
+ // if (wPlayerCharging) ???
1910
+ // wPlayerCharging = 1
1911
+
1912
+ // FIXME
1913
+ state.move = last;
1914
+ state.from = .MirrorMove;
1915
+ return generated.runMove(battle, player, state, options);
1678
1916
  }
1679
1917
 
1680
- pub fn mist(battle: anytype, player: Player, state: *State, options: anytype) !void {
1681
- _ = .{ battle, player, state, options }; // TODO
1918
+ pub fn mist(battle: anytype, player: Player, _: *State, options: anytype) !void {
1919
+ var side = battle.side(player);
1920
+
1921
+ if (side.active.volatiles.Mist) return options.log.fail(battle.active(player), .None);
1922
+ side.active.volatiles.Mist = true;
1923
+
1924
+ try options.log.start(battle.active(player), .Mist);
1682
1925
  }
1683
1926
 
1684
1927
  pub fn weatherHeal(battle: anytype, player: Player, _: *State, options: anytype) !void {
1685
1928
  var stored = battle.side(player).stored();
1686
1929
  const ident = battle.active(player);
1687
1930
 
1688
- if (stored.hp == stored.stats.hp) return try options.log.fail(ident, .None);
1931
+ if (stored.hp == stored.stats.hp) return options.log.fail(ident, .None);
1689
1932
 
1690
1933
  stored.hp = switch (battle.field.weather) {
1691
1934
  .Sun => stored.stats.hp,
@@ -1693,7 +1936,7 @@ pub const Effects = struct {
1693
1936
  else => @min(stored.stats.hp, stored.hp + (stored.stats.hp / 4)),
1694
1937
  };
1695
1938
 
1696
- try options.log.heal(ident, stored, Heal.None);
1939
+ try options.log.heal(.{ ident, stored, Heal.None });
1697
1940
  }
1698
1941
 
1699
1942
  pub fn multiHit(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1701,32 +1944,94 @@ pub const Effects = struct {
1701
1944
  }
1702
1945
 
1703
1946
  pub fn nightmare(battle: anytype, player: Player, state: *State, options: anytype) !void {
1704
- _ = .{ battle, player, state, options }; // TODO
1947
+ var foe = battle.foe(player);
1948
+ var foe_volatiles = &foe.active.volatiles;
1949
+ const foe_ident = battle.active(player.foe());
1950
+
1951
+ if (foe_volatiles.Flying or foe_volatiles.Underground) {
1952
+ if (!showdown) return options.log.fail(battle.active(player.foe()), .None);
1953
+ try options.log.lastmiss();
1954
+ return options.log.miss(battle.active(player));
1955
+ } else if (foe_volatiles.Substitute) {
1956
+ return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
1957
+ } else if (!Status.is(foe.stored().status, .SLP) or foe_volatiles.Nightmare) {
1958
+ return options.log.fail(foe_ident, .None);
1959
+ }
1960
+
1961
+ foe_volatiles.Nightmare = true;
1962
+ try options.log.start(battle.active(player.foe()), .Nightmare);
1705
1963
  }
1706
1964
 
1707
- pub fn ohko(battle: anytype, player: Player, state: *State, options: anytype) !void {
1708
- const side = battle.side(player);
1709
- const foe = battle.foe(player);
1965
+ pub const ohko = struct {
1966
+ // OHKO calls adjustDamage but only actually cares about immunity
1967
+ pub fn immune(battle: anytype, player: Player, state: *State, _: anytype) !void {
1968
+ const move = Move.get(state.move);
1710
1969
 
1711
- const foe_ident = battle.active(player.foe());
1970
+ assert(move.type == .Normal or move.type == .Ground);
1971
+ const foe = battle.foe(player);
1972
+ const zero = if (move.type == .Normal)
1973
+ foe.active.types.includes(.Ghost) and !foe.active.volatiles.Foresight
1974
+ else
1975
+ foe.active.types.includes(.Flying);
1976
+ state.effectiveness = if (zero) 0 else Effectiveness.neutral;
1977
+ }
1712
1978
 
1713
- state.damage = 0;
1714
- if (state.immune()) return try options.log.immune(foe_ident, .None);
1715
- const delta = side.stored().level -| foe.stored().level;
1716
- if (delta == 0) return try options.log.immune(foe_ident, .OHKO);
1979
+ pub fn damage(battle: anytype, player: Player, state: *State, options: anytype) !void {
1980
+ const side = battle.side(player);
1981
+ const foe = battle.foe(player);
1717
1982
 
1718
- state.accuracy = 2 * delta +| Gen12.percent(30);
1719
- try checkHit(battle, player, state, options);
1720
- if (!state.miss) state.damage = 65535;
1721
- state.ohko = true;
1722
- }
1983
+ const foe_ident = battle.active(player.foe());
1984
+
1985
+ state.damage = 0;
1986
+ if (state.immune()) return options.log.immune(foe_ident, .None);
1987
+ const delta = side.stored().level -| foe.stored().level;
1988
+ if (delta == 0) return options.log.immune(foe_ident, .OHKO);
1989
+
1990
+ state.accuracy = 2 * delta +| Gen12.percent(30);
1991
+ try checkHit(battle, player, state, options);
1992
+ if (!state.miss) state.damage = 65535;
1993
+ state.ohko = true;
1994
+ }
1995
+ };
1723
1996
 
1724
1997
  pub fn painSplit(battle: anytype, player: Player, state: *State, options: anytype) !void {
1725
- _ = .{ battle, player, state, options }; // TODO
1998
+ var side = battle.side(player);
1999
+ var foe = battle.foe(player);
2000
+
2001
+ const ident = battle.active(player);
2002
+ const foe_ident = battle.active(player.foe());
2003
+
2004
+ if (state.miss) return options.log.fail(foe_ident, .None);
2005
+ if (foe.active.volatiles.Substitute) {
2006
+ return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
2007
+ }
2008
+
2009
+ const avg = @max((foe.stored().hp + side.stored().hp) / 2, 1);
2010
+ foe.stored().hp -= avg;
2011
+ side.stored().hp = avg;
2012
+
2013
+ try options.log.sethp(foe_ident, foe.stored(), .Silent);
2014
+ try options.log.sethp(ident, side.stored(), .None);
1726
2015
  }
1727
2016
 
1728
2017
  pub fn paralyze(battle: anytype, player: Player, state: *State, options: anytype) !void {
1729
- _ = .{ battle, player, state, options }; // TODO
2018
+ var foe = battle.foe(player);
2019
+ const foe_ident = battle.active(player.foe());
2020
+
2021
+ if (Status.is(foe.stored().status, .PAR)) return options.log.fail(foe_ident, .Paralysis);
2022
+ if (state.immune()) return options.log.immune(foe_ident, .None);
2023
+ if (Status.any(foe.stored().status)) return options.log.fail(foe_ident, .None);
2024
+ if (state.miss) {
2025
+ try options.log.lastmiss();
2026
+ return options.log.miss(battle.active(player));
2027
+ } else if (foe.active.volatiles.Substitute) {
2028
+ return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
2029
+ }
2030
+
2031
+ foe.stored().status = Status.init(.PAR);
2032
+ foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2033
+
2034
+ try options.log.status(foe_ident, foe.stored().status, .None);
1730
2035
  }
1731
2036
 
1732
2037
  pub fn paralyzeChance(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1746,12 +2051,28 @@ pub const Effects = struct {
1746
2051
  // TODO: check for status berry
1747
2052
  }
1748
2053
 
1749
- pub fn payDay(battle: anytype, player: Player, state: *State, options: anytype) !void {
1750
- _ = .{ battle, player, state, options }; // TODO
2054
+ pub fn payDay(battle: anytype, player: Player, _: *State, options: anytype) !void {
2055
+ if (!showdown or !battle.foe(player).active.volatiles.Substitute) {
2056
+ try options.log.fieldactivate(); // FIXME .PayDay
2057
+ }
1751
2058
  }
1752
2059
 
1753
- pub fn perishSong(battle: anytype, player: Player, state: *State, options: anytype) !void {
1754
- _ = .{ battle, player, state, options }; // TODO
2060
+ pub fn perishSong(battle: anytype, player: Player, _: *State, options: anytype) !void {
2061
+ var side = battle.side(player);
2062
+ var foe = battle.foe(player);
2063
+
2064
+ if (foe.active.volatiles.PerishSong) {
2065
+ return options.log.fail(battle.active(player.foe()), .None);
2066
+ } else if (!side.active.volatiles.PerishSong) {
2067
+ side.active.volatiles.PerishSong = true;
2068
+ side.active.volatiles.perish_song = 4;
2069
+ try options.log.start(battle.active(player), .PerishSong3Silent);
2070
+ }
2071
+ foe.active.volatiles.PerishSong = true;
2072
+ foe.active.volatiles.perish_song = 4;
2073
+ try options.log.start(battle.active(player.foe()), .PerishSong3Silent);
2074
+
2075
+ try options.log.fieldactivate(); // FIXME .PerishSong
1755
2076
  }
1756
2077
 
1757
2078
  pub fn poison(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1778,7 +2099,7 @@ pub const Effects = struct {
1778
2099
  return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
1779
2100
  } else if (state.miss) {
1780
2101
  try options.log.lastmiss();
1781
- try options.log.miss(battle.active(player));
2102
+ return options.log.miss(battle.active(player));
1782
2103
  }
1783
2104
 
1784
2105
  foe_stored.status = Status.init(.PSN);
@@ -1820,8 +2141,7 @@ pub const Effects = struct {
1820
2141
  const failed = !state.first or volatiles.Substitute or
1821
2142
  !try Rolls.protect(battle, player, volatiles.protect, options);
1822
2143
  if (failed) {
1823
- try options.log.fail(battle.active(player), .None);
1824
- return;
2144
+ return options.log.fail(battle.active(player), .None);
1825
2145
  }
1826
2146
 
1827
2147
  volatiles.protect = @max(8, volatiles.protect + 1);
@@ -1833,33 +2153,78 @@ pub const Effects = struct {
1833
2153
  try options.log.singleturn(battle.active(player), state.move);
1834
2154
  }
1835
2155
 
1836
- pub fn psychUp(battle: anytype, player: Player, state: *State, options: anytype) !void {
1837
- _ = .{ battle, player, state, options }; // TODO
1838
- }
2156
+ pub fn psychUp(battle: anytype, player: Player, _: *State, options: anytype) !void {
2157
+ var side = battle.side(player);
2158
+ const foe = battle.foe(player);
1839
2159
 
1840
- pub fn pursuit(battle: anytype, player: Player, state: *State, options: anytype) !void {
1841
- _ = .{ battle, player, state, options }; // TODO
1842
- }
2160
+ // Pokémon Showdown should fail when the enemy has no boosts/unboosts but doesn't
2161
+ if (!showdown and @as(u32, @bitCast(foe.active.boosts)) == 0) {
2162
+ return options.log.fail(battle.active(player), .None);
2163
+ }
1843
2164
 
1844
- pub fn rage(battle: anytype, player: Player, _: *State, options: anytype) !void {
1845
- battle.side(player).active.volatiles.Rage = true;
1846
- try options.log.singlemove(battle.active(player), Move.Rage);
2165
+ side.active.boosts = foe.active.boosts;
2166
+ try options.log.copyboost(battle.active(player), battle.active(player.foe()));
1847
2167
  }
1848
2168
 
1849
- pub fn rainDance(battle: anytype, player: Player, state: *State, options: anytype) !void {
1850
- _ = .{ battle, player, state, options }; // TODO
2169
+ pub fn pursuit(battle: anytype, player: Player, state: *State, _: anytype) !void {
2170
+ // TODO: remove switching variable and just look at Choice saved in Battle struct?
2171
+ if (battle.foe(player).active.volatiles.switching) state.damage *|= 2;
1851
2172
  }
1852
2173
 
1853
- pub fn rapidSpin(battle: anytype, player: Player, state: *State, options: anytype) !void {
1854
- _ = .{ battle, player, state, options }; // TODO
2174
+ pub const rage = struct {
2175
+ pub fn start(battle: anytype, player: Player, _: *State, options: anytype) !void {
2176
+ battle.side(player).active.volatiles.Rage = true;
2177
+ try options.log.singlemove(battle.active(player), Move.Rage);
2178
+ }
2179
+
2180
+ pub fn damage(battle: anytype, player: Player, state: *State, _: anytype) !void {
2181
+ var side = battle.side(player);
2182
+ var volatiles = &side.active.volatiles;
2183
+
2184
+ assert(volatiles.Rage);
2185
+ state.damage *|= (volatiles.rage +| 1);
2186
+ }
2187
+ };
2188
+
2189
+ pub fn rainDance(battle: anytype, _: Player, _: *State, options: anytype) !void {
2190
+ return weather(battle, .Rain, options);
1855
2191
  }
1856
2192
 
1857
- pub fn razorWind(battle: anytype, player: Player, state: *State, options: anytype) !void {
1858
- _ = .{ battle, player, state, options }; // TODO
2193
+ pub fn rapidSpin(battle: anytype, player: Player, _: *State, options: anytype) !void {
2194
+ var side = battle.side(player);
2195
+
2196
+ if (side.active.volatiles.LeechSeed) {
2197
+ try options.log.end(battle.active(player), .LeechSeedFrom);
2198
+ side.active.volatiles.LeechSeed = false;
2199
+ }
2200
+
2201
+ if (side.conditions.Spikes) {
2202
+ try options.log.spikesend(battle.active(player));
2203
+ side.conditions.Spikes = false;
2204
+ }
2205
+
2206
+ if (side.active.volatiles.bind.duration > 0) {
2207
+ const reason = @intFromEnum(End.Bind) + side.active.volatiles.bind.reason - 1;
2208
+ try options.log.end(battle.active(player), @enumFromInt(reason));
2209
+ side.active.volatiles.bind = .{};
2210
+ }
1859
2211
  }
1860
2212
 
1861
2213
  pub fn recoil(battle: anytype, player: Player, state: *State, options: anytype) !void {
1862
- _ = .{ battle, player, state, options }; // TODO
2214
+ var side = battle.side(player);
2215
+ var stored = side.stored();
2216
+
2217
+ assert(state.damage > 0);
2218
+
2219
+ const damage: i16 = @intCast(@max(state.damage / 4, 1));
2220
+ stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
2221
+
2222
+ try options.log.damageOf(
2223
+ battle.active(player),
2224
+ stored,
2225
+ .RecoilOf,
2226
+ battle.active(player.foe()),
2227
+ );
1863
2228
  }
1864
2229
 
1865
2230
  pub fn reversal(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1887,36 +2252,84 @@ pub const Effects = struct {
1887
2252
  try calcDamage(battle, player, state, options);
1888
2253
  }
1889
2254
 
1890
- pub fn rollout(battle: anytype, player: Player, state: *State, options: anytype) !void {
1891
- _ = .{ battle, player, state, options }; // TODO
1892
- }
2255
+ pub const rollout = struct {
2256
+ pub fn check(battle: anytype, player: Player, _: *State, _: anytype) !void {
2257
+ var side = battle.side(player);
1893
2258
 
1894
- pub fn sacredFire(battle: anytype, player: Player, state: *State, options: anytype) !void {
1895
- _ = .{ battle, player, state, options }; // TODO
1896
- }
2259
+ if (!side.active.volatiles.Rollout) {
2260
+ side.active.volatiles.attacks = 0;
2261
+ } else {
2262
+ // TODO SkipToBattleCommand(doturn_command)
2263
+ }
2264
+ }
1897
2265
 
1898
- pub fn safeguard(battle: anytype, player: Player, state: *State, options: anytype) !void {
1899
- _ = .{ battle, player, state, options }; // TODO
2266
+ pub fn power(battle: anytype, player: Player, state: *State, _: anytype) !void {
2267
+ var side = battle.side(player);
2268
+
2269
+ if (Status.is(side.stored().status, .SLP)) return;
2270
+
2271
+ if (side.active.volatiles.attacks > 0) {
2272
+ // TODO set wSomeoneIsRampaging
2273
+ }
2274
+
2275
+ if (state.miss) {
2276
+ side.active.volatiles.Rollout = false;
2277
+ return;
2278
+ }
2279
+
2280
+ assert(side.active.volatiles.attacks < 5);
2281
+ side.active.volatiles.attacks += 1;
2282
+ side.active.volatiles.Rollout = side.active.volatiles.attacks != 5;
2283
+
2284
+ if (side.active.volatiles.DefenseCurl) state.damage *|= 2;
2285
+ }
2286
+ };
2287
+
2288
+ pub fn sandstorm(battle: anytype, player: Player, _: *State, options: anytype) !void {
2289
+ if (battle.field.weather != .Sandstorm) return weather(battle, .Sandstorm, options);
2290
+ return options.log.fail(battle.active(player), .None);
1900
2291
  }
1901
2292
 
1902
- pub fn sandstorm(battle: anytype, player: Player, state: *State, options: anytype) !void {
1903
- _ = .{ battle, player, state, options }; // TODO
2293
+ pub fn safeguard(battle: anytype, player: Player, _: *State, options: anytype) !void {
2294
+ return screens(battle, player, "Safeguard", "safeguard", options);
1904
2295
  }
1905
2296
 
1906
- pub fn screens(battle: anytype, player: Player, state: *State, options: anytype) !void {
1907
- _ = .{ battle, player, state, options }; // TODO
2297
+ pub fn reflect(battle: anytype, player: Player, _: *State, options: anytype) !void {
2298
+ return screens(battle, player, "Reflect", "reflect", options);
1908
2299
  }
1909
2300
 
1910
- pub fn sketch(battle: anytype, player: Player, state: *State, options: anytype) !void {
1911
- _ = .{ battle, player, state, options }; // TODO
2301
+ pub fn lightScreen(battle: anytype, player: Player, _: *State, options: anytype) !void {
2302
+ return screens(battle, player, "LightScreen", "light_screen", options);
1912
2303
  }
1913
2304
 
1914
- pub fn skullBash(battle: anytype, player: Player, state: *State, options: anytype) !void {
1915
- _ = .{ battle, player, state, options }; // TODO
2305
+ fn screens(
2306
+ battle: anytype,
2307
+ player: Player,
2308
+ comptime key: []const u8,
2309
+ comptime value: []const u8,
2310
+ options: anytype,
2311
+ ) !void {
2312
+ var side = battle.side(player);
2313
+
2314
+ if (@field(side.conditions, key)) return options.log.fail(battle.active(player), .None);
2315
+
2316
+ @field(side.conditions, key) = true;
2317
+ @field(side.conditions, value) = 5;
2318
+ try options.log.sidestart(player, @field(protocol.Side, key));
1916
2319
  }
1917
2320
 
1918
- pub fn skyAttack(battle: anytype, player: Player, state: *State, options: anytype) !void {
1919
- _ = .{ battle, player, state, options }; // TODO
2321
+ pub fn sketch(battle: anytype, player: Player, _: *State, options: anytype) !void {
2322
+ var side = battle.side(player);
2323
+
2324
+ side.last_used_move = .None;
2325
+ side.active.volatiles.dirty = false;
2326
+
2327
+ if (showdown) {
2328
+ // yes, actually "Splash". '|nothing' gets blindly mapped to '|-activate||move:Splash'
2329
+ try options.log.activate(battle.active(player), .Splash);
2330
+ } else {
2331
+ try options.log.fail(battle.active(player), .None);
2332
+ }
1920
2333
  }
1921
2334
 
1922
2335
  pub fn sleep(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1958,64 +2371,148 @@ pub const Effects = struct {
1958
2371
  for (side.active.moves, 0..) |m, i| {
1959
2372
  if (state.mslot - 1 != i and Move.get(m.id).extra.sleep_talk) n += 1;
1960
2373
  }
1961
- if (n == 0) {
1962
- try options.log.fail(battle.active(player), .None);
1963
- return;
1964
- }
2374
+ if (n == 0) return options.log.fail(battle.active(player), .None);
1965
2375
 
1966
2376
  _ = try Rolls.sleepTalk(battle, player, n, state.mslot, options); // TODO
1967
2377
  }
1968
2378
 
1969
- pub fn snore(battle: anytype, player: Player, state: *State, options: anytype) !void {
1970
- _ = .{ battle, player, state, options }; // TODO
2379
+ pub fn snore(battle: anytype, player: Player, state: *State, options: anytype) !bool {
2380
+ if (Status.is(battle.side(player).stored().status, .SLP)) return true;
2381
+ state.damage = 0;
2382
+ state.miss = true; // TODO is this necessary?
2383
+ if (!showdown) try options.log.fail(battle.active(player), .None);
2384
+ return false;
1971
2385
  }
1972
2386
 
1973
- pub fn spikes(battle: anytype, player: Player, state: *State, options: anytype) !void {
1974
- _ = .{ battle, player, state, options }; // TODO
2387
+ pub fn spikes(battle: anytype, player: Player, _: *State, options: anytype) !void {
2388
+ var foe = battle.foe(player);
2389
+ if (foe.conditions.Spikes) return options.log.fail(battle.active(player), .None);
2390
+
2391
+ foe.conditions.Spikes = true;
2392
+ try options.log.sidestart(player.foe(), .Spikes);
1975
2393
  }
1976
2394
 
1977
2395
  pub fn spite(battle: anytype, player: Player, state: *State, options: anytype) !void {
1978
2396
  _ = .{ battle, player, state, options }; // TODO
1979
- }
2397
+ // var foe = battle.foe(player);
1980
2398
 
1981
- pub fn splash(battle: anytype, player: Player, state: *State, options: anytype) !void {
1982
- _ = .{ battle, player, state, options }; // TODO
1983
- }
2399
+ // if (state.miss) return options.log.fail(battle.active(player), .None);
1984
2400
 
1985
- pub fn stomp(battle: anytype, player: Player, state: *State, options: anytype) !void {
1986
- _ = .{ battle, player, state, options }; // TODO
2401
+ // const last = foe.lastMove(false);
2402
+ // if (showdown) {
2403
+ // const roll = try Rolls.spite(battle, player, options);
2404
+ // } else {
2405
+ // if (last != .None) {}
2406
+ // }
1987
2407
  }
1988
2408
 
1989
- pub fn substitute(battle: anytype, player: Player, state: *State, options: anytype) !void {
1990
- _ = .{ battle, player, state, options }; // TODO
2409
+ pub fn splash(battle: anytype, player: Player, _: *State, options: anytype) !void {
2410
+ try options.log.activate(battle.active(player), .Splash);
1991
2411
  }
1992
2412
 
1993
- pub fn sunnyDay(battle: anytype, player: Player, state: *State, options: anytype) !void {
1994
- _ = .{ battle, player, state, options }; // TODO
2413
+ pub fn substitute(battle: anytype, player: Player, _: *State, options: anytype) !void {
2414
+ var side = battle.side(player);
2415
+ if (side.active.volatiles.Substitute) {
2416
+ return options.log.fail(battle.active(player), .Substitute);
2417
+ }
2418
+
2419
+ assert(side.stored().stats.hp <= 1023);
2420
+ const hp: u8 = @intCast(side.stored().stats.hp / 4);
2421
+
2422
+ if (hp == 0 or side.stored().hp <= hp) {
2423
+ return options.log.fail(battle.active(player), .Weak);
2424
+ }
2425
+
2426
+ side.stored().hp -= hp;
2427
+ side.active.volatiles.substitute = hp;
2428
+ side.active.volatiles.Substitute = true;
2429
+ side.active.volatiles.bind = .{};
2430
+ try options.log.start(battle.active(player), .Substitute);
2431
+ try options.log.damage(battle.active(player), side.stored(), .None);
1995
2432
  }
1996
2433
 
1997
- pub fn swagger(battle: anytype, player: Player, state: *State, options: anytype) !void {
1998
- _ = .{ battle, player, state, options }; // TODO
2434
+ pub fn sunnyDay(battle: anytype, _: Player, _: *State, options: anytype) !void {
2435
+ return weather(battle, .Sun, options);
1999
2436
  }
2000
2437
 
2001
- pub fn synthesis(battle: anytype, player: Player, state: *State, options: anytype) !void {
2002
- _ = .{ battle, player, state, options }; // TODO
2438
+ fn weather(battle: anytype, w: Weather, options: anytype) !void {
2439
+ battle.field.weather = w;
2440
+ battle.field.weather_duration = 5;
2441
+ try options.log.weather(w, .None);
2003
2442
  }
2004
2443
 
2005
- pub fn teleport(battle: anytype, player: Player, state: *State, options: anytype) !void {
2006
- _ = .{ battle, player, state, options }; // TODO
2444
+ pub fn teleport(battle: anytype, player: Player, _: *State, options: anytype) !void {
2445
+ if (showdown) return;
2446
+
2447
+ try options.log.fail(battle.active(player), .None);
2448
+ try options.log.laststill();
2007
2449
  }
2008
2450
 
2009
2451
  pub fn thief(battle: anytype, player: Player, state: *State, options: anytype) !void {
2010
- _ = .{ battle, player, state, options }; // TODO
2452
+ const side = battle.side(player);
2453
+ const foe = battle.foe(player);
2454
+
2455
+ if (side.stored().item != .None or foe.stored().item == .None) return;
2456
+ // Pokémon Showdown incorrectly allows you steal Mail
2457
+ if (!showdown and foe.stored().item.isMail()) return;
2458
+ if (!state.proc) return;
2459
+
2460
+ side.stored().item = foe.stored().item;
2461
+ foe.stored().item = .None;
2462
+
2463
+ try options.log.item(
2464
+ battle.active(player),
2465
+ side.stored().item,
2466
+ battle.active(player.foe()),
2467
+ );
2011
2468
  }
2012
2469
 
2013
- pub fn thrashing(battle: anytype, player: Player, state: *State, options: anytype) !void {
2014
- _ = .{ battle, player, state, options }; // TODO
2470
+ pub fn thrashing(battle: anytype, player: Player, _: *State, options: anytype) !void {
2471
+ var side = battle.side(player);
2472
+ if (Status.is(side.stored().status, .SLP)) return;
2473
+
2474
+ assert(!side.active.volatiles.Thrashing);
2475
+ side.active.volatiles.Thrashing = true;
2476
+ side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
2477
+ // TODO wSomeoneIsRampaging = true;
2015
2478
  }
2016
2479
 
2017
- pub fn transform(battle: anytype, player: Player, state: *State, options: anytype) !void {
2018
- _ = .{ battle, player, state, options }; // TODO
2480
+ pub fn transform(battle: anytype, player: Player, _: *State, options: anytype) !void {
2481
+ var side = battle.side(player);
2482
+ const foe = battle.foe(player);
2483
+ const foe_ident = battle.active(player.foe());
2484
+
2485
+ side.last_used_move = .None;
2486
+ side.active.volatiles.dirty = false;
2487
+
2488
+ if (foe.active.volatiles.Transform) {
2489
+ return options.log.fail(battle.active(player), .None);
2490
+ } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2491
+ if (!showdown) return options.log.fail(battle.active(player), .None);
2492
+ try options.log.lastmiss();
2493
+ return options.log.miss(battle.active(player));
2494
+ }
2495
+
2496
+ side.active.volatiles.Transform = true;
2497
+ side.active.volatiles.transform = foe_ident.int();
2498
+ side.active.volatiles.disable = .{};
2499
+
2500
+ // HP is not copied by Transform
2501
+ side.active.stats.atk = foe.active.stats.atk;
2502
+ side.active.stats.def = foe.active.stats.def;
2503
+ side.active.stats.spe = foe.active.stats.spe;
2504
+ side.active.stats.spa = foe.active.stats.spa;
2505
+ side.active.stats.spd = foe.active.stats.spd;
2506
+
2507
+ side.active.species = foe.active.species;
2508
+ side.active.types = foe.active.types;
2509
+ side.active.boosts = foe.active.boosts;
2510
+ for (foe.active.moves, 0..) |m, i| {
2511
+ side.active.moves[i].id = m.id;
2512
+ side.active.moves[i].pp = if (m.id != .None) if (m.id == .Sketch) 1 else 5 else 0;
2513
+ }
2514
+
2515
+ try options.log.transform(battle.active(player), foe_ident);
2019
2516
  }
2020
2517
 
2021
2518
  pub fn triAttack(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -2027,19 +2524,14 @@ pub const Effects = struct {
2027
2524
  .FRZ => freezeChance(battle, player, state, options),
2028
2525
  .BRN => burnChance(battle, player, state, options),
2029
2526
  };
2030
- } else if (!showdown and pkmn.options.advance) _ = battle.rng.next();
2527
+ } else if (!showdown) battle.rng.advance(1);
2031
2528
  }
2032
2529
 
2033
2530
  pub fn tripleKick(battle: anytype, player: Player, state: *State, options: anytype) !void {
2034
2531
  _ = .{ battle, player, state, options }; // TODO
2035
2532
  }
2036
2533
 
2037
- pub fn twister(battle: anytype, player: Player, state: *State, options: anytype) !void {
2038
- _ = .{ battle, player, state, options }; // TODO
2039
- }
2040
-
2041
- const ALL_STAT_UP_STATS = [_][]const u8{ "atk", "def", "spe", "spa", "spd" };
2042
- const ALL_STAT_UP_REASONS =
2534
+ const STAT_UP_REASONS =
2043
2535
  [_]Boost{ .Attack, .Defense, .Speed, .SpecialAttack, .SpecialDefense };
2044
2536
 
2045
2537
  pub fn allStatUpChance(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -2049,20 +2541,15 @@ pub const Effects = struct {
2049
2541
  var boosts = &side.active.boosts;
2050
2542
 
2051
2543
  if (!state.proc) return;
2052
- inline for (ALL_STAT_UP_STATS, ALL_STAT_UP_REASONS) |s, r| {
2544
+ inline for (STATS, STAT_UP_REASONS) |s, r| {
2053
2545
  assert(@field(boosts, s) >= -6 and @field(boosts, s) <= 6);
2054
2546
  if (@field(boosts, s) == 6 or @field(stats, s) == MAX_STAT_VALUE) {
2055
2547
  try options.log.boost(battle.active(player), r, 0);
2056
2548
  } else {
2057
- @field(boosts, s) = @intCast(@min(6, @as(i8, @field(boosts, s)) + 1));
2058
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, @field(boosts, s)) + 6))];
2059
- const stat = @field(unmodifiedStats(battle, side), s);
2060
- @field(stats, s) = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2549
+ try boostStat(battle, side, stats, boosts, s, 1);
2061
2550
  try options.log.boost(battle.active(player), r, 1);
2062
2551
  }
2063
2552
  }
2064
-
2065
- statusModify(side.stored().status, stats);
2066
2553
  }
2067
2554
 
2068
2555
  pub fn boost(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -2070,84 +2557,55 @@ pub const Effects = struct {
2070
2557
  var side = battle.side(player);
2071
2558
  const ident = battle.active(player);
2072
2559
 
2073
- if (state.miss) return try log.fail(ident, .None);
2560
+ if (state.miss) return log.fail(ident, .None);
2074
2561
 
2075
2562
  var stats = &side.active.stats;
2076
2563
  var boosts = &side.active.boosts;
2077
2564
 
2078
2565
  const move = Move.get(state.move);
2079
2566
  switch (move.effect) {
2080
- .AttackUp1, .AttackUp2, .Rage => {
2567
+ .AttackUp1, .AttackUp2, .Rage, .Swagger => {
2081
2568
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2082
- if (boosts.atk == 6 or stats.atk == MAX_STAT_VALUE) {
2083
- return try log.fail(ident, .None);
2084
- }
2085
- const n: u2 = if (move.effect == .AttackUp2) 2 else 1;
2086
- boosts.atk = @as(i4, @intCast(@min(6, @as(i8, boosts.atk) + n)));
2569
+ if (boosts.atk == 6 or stats.atk == MAX_STAT_VALUE) return log.fail(ident, .None);
2570
+ const n: u2 = if (move.effect == .AttackUp2 or move.effect == .Swagger) 2 else 1;
2571
+ try boostStat(battle, side, stats, boosts, "atk", n);
2087
2572
  const reason = if (showdown and move.effect == .Rage) Boost.Rage else Boost.Attack;
2088
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2089
- const stat = unmodifiedStats(battle, side).atk;
2090
- stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2091
2573
  try log.boost(ident, reason, n);
2092
2574
  },
2093
- .DefenseUp1, .DefenseUp2 => {
2575
+ .DefenseUp1, .DefenseUp2, .SkullBash => {
2094
2576
  assert(boosts.def >= -6 and boosts.def <= 6);
2095
- if (boosts.def == 6 or stats.def == MAX_STAT_VALUE) {
2096
- return try log.fail(ident, .None);
2097
- }
2577
+ if (boosts.def == 6 or stats.def == MAX_STAT_VALUE) return log.fail(ident, .None);
2098
2578
  const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
2099
- boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
2100
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2101
- const stat = unmodifiedStats(battle, side).def;
2102
- stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2579
+ try boostStat(battle, side, stats, boosts, "def", n);
2103
2580
  try log.boost(ident, .Defense, n);
2104
2581
  },
2105
2582
  .SpeedUp2 => {
2106
2583
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2107
- if (boosts.spe == 6 or stats.spe == MAX_STAT_VALUE) {
2108
- return try log.fail(ident, .None);
2109
- }
2110
- boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
2111
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2112
- const stat = unmodifiedStats(battle, side).spe;
2113
- stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2584
+ if (boosts.spe == 6 or stats.spe == MAX_STAT_VALUE) return log.fail(ident, .None);
2585
+ try boostStat(battle, side, stats, boosts, "spe", 2);
2114
2586
  try log.boost(ident, .Speed, 2);
2115
2587
  },
2116
2588
  .SpAtkUp1 => {
2117
2589
  assert(boosts.spa >= -6 and boosts.spa <= 6);
2118
- if (boosts.spa == 6 or stats.spa == MAX_STAT_VALUE) {
2119
- return try log.fail(ident, .None);
2120
- }
2121
- boosts.spa = @intCast(@min(6, @as(i8, boosts.spa) + 1));
2122
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.spa) + 6))];
2123
- const stat = unmodifiedStats(battle, side).spa;
2124
- stats.spa = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2590
+ if (boosts.spa == 6 or stats.spa == MAX_STAT_VALUE) return log.fail(ident, .None);
2591
+ try boostStat(battle, side, stats, boosts, "spa", 1);
2125
2592
  try log.boost(ident, .SpecialAttack, 1);
2126
2593
  },
2127
2594
  .SpDefUp2 => {
2128
2595
  assert(boosts.spd >= -6 and boosts.spd <= 6);
2129
- if (boosts.spd == 6 or stats.spd == MAX_STAT_VALUE) {
2130
- return try log.fail(ident, .None);
2131
- }
2132
- boosts.spd = @intCast(@min(6, @as(i8, boosts.spd) + 2));
2133
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.spd) + 6))];
2134
- const stat = unmodifiedStats(battle, side).spd;
2135
- stats.spd = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2596
+ if (boosts.spd == 6 or stats.spd == MAX_STAT_VALUE) return log.fail(ident, .None);
2597
+ try boostStat(battle, side, stats, boosts, "spd", 2);
2136
2598
  try log.boost(ident, .SpecialDefense, 2);
2137
2599
  },
2138
2600
  .EvasionUp1 => {
2139
2601
  assert(boosts.evasion >= -6 and boosts.evasion <= 6);
2140
- if (boosts.evasion == 6) {
2141
- return try log.fail(ident, .None);
2142
- }
2602
+ if (boosts.evasion == 6) return log.fail(ident, .None);
2143
2603
  boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
2144
2604
  try log.boost(ident, .Evasion, 1);
2145
2605
  if (state.move == .Minimize) side.active.volatiles.minimized = true;
2146
2606
  },
2147
2607
  else => unreachable,
2148
2608
  }
2149
-
2150
- statusModify(side.stored().status, stats);
2151
2609
  }
2152
2610
 
2153
2611
  pub fn unboost(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -2157,7 +2615,7 @@ pub const Effects = struct {
2157
2615
 
2158
2616
  if (foe.active.volatiles.Mist) {
2159
2617
  try log.activate(foe_ident, .Mist);
2160
- return try log.fail(foe_ident, .None);
2618
+ return log.fail(foe_ident, .None);
2161
2619
  }
2162
2620
 
2163
2621
  const fail = foe.active.volatiles.Substitute or state.miss or !state.proc;
@@ -2173,16 +2631,12 @@ pub const Effects = struct {
2173
2631
  return if (move.effect != .AttackDownChance) try log.fail(foe_ident, .None);
2174
2632
  } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2175
2633
  try options.log.lastmiss();
2176
- try options.log.miss(battle.active(player));
2177
- return;
2634
+ return options.log.miss(battle.active(player));
2178
2635
  } else if (stats.atk == 1) {
2179
2636
  return if (move.effect != .AttackDownChance) try log.fail(foe_ident, .None);
2180
2637
  }
2181
2638
  const n: u2 = if (move.effect == .AttackDown2) 2 else 1;
2182
- boosts.atk = @intCast(@max(-6, @as(i8, boosts.atk) - n));
2183
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2184
- const stat = unmodifiedStats(battle, foe).atk;
2185
- stats.atk = @max(1, stat * mod[0] / mod[1]);
2639
+ try unboostStat(battle, foe, stats, boosts, "atk", n);
2186
2640
  try log.boost(foe_ident, .Attack, -@as(i8, n));
2187
2641
  },
2188
2642
  .DefenseDown1, .DefenseDown2, .DefenseDownChance => {
@@ -2191,16 +2645,12 @@ pub const Effects = struct {
2191
2645
  return if (move.effect != .DefenseDownChance) try log.fail(foe_ident, .None);
2192
2646
  } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2193
2647
  try options.log.lastmiss();
2194
- try options.log.miss(battle.active(player));
2195
- return;
2648
+ return options.log.miss(battle.active(player));
2196
2649
  } else if (stats.def == 1) {
2197
2650
  return if (move.effect != .DefenseDownChance) try log.fail(foe_ident, .None);
2198
2651
  }
2199
2652
  const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
2200
- boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
2201
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2202
- const stat = unmodifiedStats(battle, foe).def;
2203
- stats.def = @max(1, stat * mod[0] / mod[1]);
2653
+ try unboostStat(battle, foe, stats, boosts, "def", n);
2204
2654
  try log.boost(foe_ident, .Defense, -@as(i8, n));
2205
2655
  },
2206
2656
  .SpeedDown1, .SpeedDown2, .SpeedDownChance => {
@@ -2209,31 +2659,22 @@ pub const Effects = struct {
2209
2659
  return if (move.effect != .SpeedDownChance) try log.fail(foe_ident, .None);
2210
2660
  } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2211
2661
  try options.log.lastmiss();
2212
- try options.log.miss(battle.active(player));
2213
- return;
2662
+ return options.log.miss(battle.active(player));
2214
2663
  } else if (stats.spe == 1) {
2215
2664
  return if (move.effect != .SpeedDownChance) try log.fail(foe_ident, .None);
2216
2665
  }
2217
2666
  const n: u2 = if (move.effect == .SpeedDown2) 2 else 1;
2218
- boosts.spe = @intCast(@max(-6, @as(i8, boosts.spe) - 1));
2219
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2220
- const stat = unmodifiedStats(battle, foe).spe;
2221
- stats.spe = @max(1, stat * mod[0] / mod[1]);
2667
+ try unboostStat(battle, foe, stats, boosts, "spe", n);
2222
2668
  try log.boost(foe_ident, .Speed, -@as(i8, n));
2223
- assert(boosts.spe >= -6);
2224
2669
  },
2225
2670
  .SpDefDownChance => {
2226
2671
  assert(boosts.spd >= -6 and boosts.spd <= 6);
2227
2672
  if (fail or boosts.spd == -6) return;
2228
2673
  if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2229
2674
  try options.log.lastmiss();
2230
- try options.log.miss(battle.active(player));
2231
- return;
2675
+ return options.log.miss(battle.active(player));
2232
2676
  } else if (stats.spd == 1) return;
2233
- boosts.spd = @intCast(@max(-6, @as(i8, boosts.spd) - 1));
2234
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.spd) + 6))];
2235
- const stat = unmodifiedStats(battle, foe).spd;
2236
- stats.spd = @max(1, stat * mod[0] / mod[1]);
2677
+ try unboostStat(battle, foe, stats, boosts, "spd", 1);
2237
2678
  try log.boost(foe_ident, .SpecialDefense, -1);
2238
2679
  },
2239
2680
  .AccuracyDown1, .AccuracyDownChance => {
@@ -2242,16 +2683,13 @@ pub const Effects = struct {
2242
2683
  return if (move.effect != .AccuracyDownChance) try log.fail(foe_ident, .None);
2243
2684
  } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2244
2685
  try options.log.lastmiss();
2245
- try options.log.miss(battle.active(player));
2246
- return;
2686
+ return options.log.miss(battle.active(player));
2247
2687
  }
2248
2688
  boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
2249
2689
  try log.boost(foe_ident, .Accuracy, -1);
2250
2690
  },
2251
2691
  else => unreachable,
2252
2692
  }
2253
-
2254
- statusModify(foe.stored().status, stats);
2255
2693
  }
2256
2694
  };
2257
2695
 
@@ -2261,6 +2699,15 @@ fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
2261
2699
  return &battle.side(id.player).pokemon[id.id - 1].stats;
2262
2700
  }
2263
2701
 
2702
+ fn recomputeStats(battle: anytype, side: *Side) void {
2703
+ var stats = unmodifiedStats(battle, side);
2704
+ inline for (STATS) |s| {
2705
+ var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, @field(side.active.boosts, s)) + 6))];
2706
+ const val = @field(stats, s) * mod[0] / mod[1];
2707
+ @field(side.active.stats, s) = @max(1, @min(MAX_STAT_VALUE, val));
2708
+ }
2709
+ }
2710
+
2264
2711
  fn statusModify(status: u8, stats: *Stats(u16)) void {
2265
2712
  if (Status.is(status, .PAR)) {
2266
2713
  stats.spe = @max(stats.spe / 4, 1);
@@ -2269,6 +2716,52 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
2269
2716
  }
2270
2717
  }
2271
2718
 
2719
+ fn boostStat(
2720
+ battle: anytype,
2721
+ side: *Side,
2722
+ stats: *Stats(u16),
2723
+ boosts: *Boosts,
2724
+ comptime s: []const u8,
2725
+ n: u2,
2726
+ ) !void {
2727
+ @field(boosts, s) = @intCast(@min(6, @as(i8, @field(boosts, s)) + n));
2728
+ var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, @field(boosts, s)) + 6))];
2729
+ const stat = @field(unmodifiedStats(battle, side), s);
2730
+ @field(stats, s) = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2731
+ if (comptime std.mem.eql(u8, s, "atk")) {
2732
+ if (Status.is(side.stored().status, .BRN)) {
2733
+ @field(stats, s) = @max(@field(stats, s) / 2, 1);
2734
+ }
2735
+ } else if (comptime std.mem.eql(u8, s, "spe")) {
2736
+ if (Status.is(side.stored().status, .PAR)) {
2737
+ @field(stats, s) = @max(@field(stats, s) / 4, 1);
2738
+ }
2739
+ }
2740
+ }
2741
+
2742
+ fn unboostStat(
2743
+ battle: anytype,
2744
+ side: *Side,
2745
+ stats: *Stats(u16),
2746
+ boosts: *Boosts,
2747
+ comptime s: []const u8,
2748
+ n: u2,
2749
+ ) !void {
2750
+ @field(boosts, s) = @intCast(@max(-6, @as(i8, @field(boosts, s)) - n));
2751
+ var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, @field(boosts, s)) + 6))];
2752
+ const stat = @field(unmodifiedStats(battle, side), s);
2753
+ @field(stats, s) = @max(1, stat * mod[0] / mod[1]);
2754
+ if (comptime std.mem.eql(u8, s, "atk")) {
2755
+ if (Status.is(side.stored().status, .BRN)) {
2756
+ @field(stats, s) = @max(@field(stats, s) / 2, 1);
2757
+ }
2758
+ } else if (comptime std.mem.eql(u8, s, "spe")) {
2759
+ if (Status.is(side.stored().status, .PAR)) {
2760
+ @field(stats, s) = @max(@field(stats, s) / 4, 1);
2761
+ }
2762
+ }
2763
+ }
2764
+
2272
2765
  fn isForced(active: anytype) bool {
2273
2766
  return active.volatiles.Recharging or active.volatiles.Rage or
2274
2767
  active.volatiles.Thrashing or active.volatiles.Charging or
@@ -2458,7 +2951,7 @@ pub const Rolls = struct {
2458
2951
  }
2459
2952
 
2460
2953
  fn magnitude(battle: anytype, player: Player, options: anytype) !u8 {
2461
- const num = if (options.calc.overridden(player, .magnitude)) |val|
2954
+ const num: u8 = if (options.calc.overridden(player, .magnitude)) |val|
2462
2955
  val + 3
2463
2956
  else if (showdown) num: {
2464
2957
  const r = battle.rng.range(u8, 0, 100);
@@ -2717,12 +3210,12 @@ pub const Rolls = struct {
2717
3210
  }
2718
3211
 
2719
3212
  fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2720
- const moves = battle.get(player).active.moves;
3213
+ const moves = battle.side(player).active.moves;
2721
3214
 
2722
3215
  var n: u2 = 0;
2723
3216
  for (moves) |m| {
2724
- if (m == .None) break;
2725
- if (Move.get(m).metronome) n += 1;
3217
+ if (m.id == .None) break;
3218
+ if (Move.get(m.id).extra.metronome) n += 1;
2726
3219
  }
2727
3220
 
2728
3221
  // TODO: consider throwing error instead of rerolling?
@@ -2733,10 +3226,11 @@ pub const Rolls = struct {
2733
3226
 
2734
3227
  const move: Move = overridden orelse move: {
2735
3228
  if (showdown) {
2736
- var r = battle.rng.range(u8, 0, Move.METRONOME.len - n);
3229
+ var r = battle.rng.range(u8, 0, @intCast(Move.METRONOME.len - n));
2737
3230
  for (moves) |m| {
2738
- if (m == .None or @intFromEnum(m) > @intFromEnum(Move.METRONOME[r])) break;
2739
- if (Move.get(m).metronome) r -= 1;
3231
+ if (m.id == .None) break;
3232
+ if (@intFromEnum(m.id) > @intFromEnum(Move.METRONOME[r])) break;
3233
+ if (Move.get(m.id).extra.metronome) r -= 1;
2740
3234
  }
2741
3235
  break :move Move.METRONOME[r];
2742
3236
  } else {
@@ -2751,7 +3245,7 @@ pub const Rolls = struct {
2751
3245
  }
2752
3246
  };
2753
3247
 
2754
- assert(Move.get(move).metronome);
3248
+ assert(Move.get(move).extra.metronome);
2755
3249
  try options.chance.metronome(player, move, n);
2756
3250
  return move;
2757
3251
  }
@@ -2975,7 +3469,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
2975
3469
  }
2976
3470
 
2977
3471
  var slot: u4 = 2;
2978
- while (!active.volatiles.trapped and slot <= 6) : (slot += 1) {
3472
+ while (!active.volatiles.Trapped and slot <= 6) : (slot += 1) {
2979
3473
  const id = side.order[slot - 1];
2980
3474
  if (id == 0 or side.pokemon[id - 1].hp == 0) continue;
2981
3475
  out[n] = .{ .type = .Switch, .data = slot };