@pkmn/engine 0.1.0-dev.dfd46bbb → 0.1.0-dev.e0e37c1b

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.
@@ -26,9 +26,9 @@ const Result = common.Result;
26
26
 
27
27
  const Activate = protocol.Activate;
28
28
  const Boost = protocol.Boost;
29
- const Cant = protocol.Cant;
30
29
  const Damage = protocol.Damage;
31
30
  const End = protocol.End;
31
+ const Fail = protocol.Fail;
32
32
  const Heal = protocol.Heal;
33
33
  const Start = protocol.Start;
34
34
 
@@ -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;
@@ -96,10 +97,13 @@ const CRITICAL_HITS = &[_]u8{
96
97
 
97
98
  const MAX_STAT_VALUE = 999;
98
99
 
100
+ const STATS = [_][]const u8{ "atk", "def", "spe", "spa", "spd" };
101
+
99
102
  pub const State = struct {
100
103
  damage: u16 = 0, // wCurDamage
101
104
  effectiveness: u16 = Effectiveness.neutral, // wTypeModifier
102
105
  bp: u8, // wPlayerMoveStructPower
106
+ type: Type, // BATTLE_VARS_MOVE_TYPE
103
107
  accuracy: u8, // wEnemyMoveStruct + MOVE_ACC
104
108
  move: Move, // wCurPlayerMove
105
109
  from: Move = .None,
@@ -111,7 +115,7 @@ pub const State = struct {
111
115
  proc: bool = true, // wEffectFailed
112
116
 
113
117
  comptime {
114
- assert(@sizeOf(State) == 14);
118
+ assert(@sizeOf(State) == 16);
115
119
  }
116
120
 
117
121
  pub fn immune(self: *State) bool {
@@ -248,15 +252,15 @@ fn switchIn(battle: anytype, player: Player, slot: u8, kind: SwitchIn, options:
248
252
  statusModify(incoming.status, &active.stats);
249
253
 
250
254
  if (kind == .Drag) {
251
- try options.log.drag(battle.active(player), incoming);
255
+ try options.log.drag(.{ battle.active(player), incoming });
252
256
  } else {
253
- try options.log.switched(battle.active(player), incoming);
257
+ try options.log.switched(.{ battle.active(player), incoming });
254
258
  }
255
259
  options.chance.switched(player, side.pokemon[0].position, out);
256
260
 
257
261
  if (side.conditions.Spikes and !active.types.includes(.Flying)) {
258
262
  incoming.hp -|= @max(incoming.stats.hp / 8, 1);
259
- try options.log.damage(battle.active(player), incoming, .Spikes);
263
+ try options.log.damage(.{ battle.active(player), incoming, .Spikes });
260
264
  }
261
265
  }
262
266
 
@@ -405,8 +409,14 @@ fn executeMove(
405
409
 
406
410
  // FIXME: need damage from doTurn
407
411
  const m = Move.get(move);
408
- var state: State =
409
- .{ .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
+ };
410
420
  try generated.runMove(battle, player, &state, options);
411
421
  return null;
412
422
  }
@@ -423,7 +433,7 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
423
433
  if (volatiles.Recharging) {
424
434
  volatiles.Recharging = false;
425
435
  resetCant(volatiles);
426
- try log.cant(ident, .Recharge);
436
+ try log.cant(.{ ident, .Recharge });
427
437
  return false;
428
438
  }
429
439
 
@@ -442,12 +452,12 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
442
452
  }
443
453
 
444
454
  if (duration == 0) {
445
- try log.curestatus(ident, before, .Message);
455
+ try log.curestatus(.{ ident, before, .Message });
446
456
  stored.status = 0; // clears EXT if present
447
457
  resetCant(volatiles);
448
458
  volatiles.Nightmare = false;
449
459
  } else {
450
- try log.cant(ident, .Sleep);
460
+ try log.cant(.{ ident, .Sleep });
451
461
  if (move == .Snore or move == .SleepTalk) break :slp;
452
462
  resetCant(volatiles);
453
463
  return false;
@@ -456,7 +466,7 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
456
466
 
457
467
  if (Status.is(stored.status, .FRZ) and !(move == .FlameWheel or move == .SacredFire)) {
458
468
  resetCant(volatiles);
459
- try log.cant(ident, .Freeze);
469
+ try log.cant(.{ ident, .Freeze });
460
470
  return false;
461
471
  }
462
472
 
@@ -466,7 +476,7 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
466
476
  // if (!showdown) volatiles.Flinch = false;
467
477
  volatiles.Flinch = false;
468
478
  resetCant(volatiles);
469
- try log.cant(ident, .Flinch);
479
+ try log.cant(.{ ident, .Flinch });
470
480
  return false;
471
481
  }
472
482
 
@@ -480,7 +490,7 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
480
490
 
481
491
  if (volatiles.disable.duration == 0) {
482
492
  volatiles.disable.move = 0;
483
- try log.end(ident, .Disable);
493
+ try log.end(.{ ident, .Disable });
484
494
  }
485
495
  }
486
496
 
@@ -496,9 +506,9 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
496
506
 
497
507
  if (volatiles.confusion == 0) {
498
508
  volatiles.Confusion = false;
499
- try log.end(ident, .Confusion);
509
+ try log.end(.{ ident, .Confusion });
500
510
  } else {
501
- try log.activate(ident, .Confusion);
511
+ try log.activate(.{ ident, .Confusion });
502
512
 
503
513
  if (try Rolls.confused(battle, player, options)) {
504
514
  volatiles.BeatUp = false;
@@ -513,11 +523,11 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
513
523
  }
514
524
 
515
525
  if (volatiles.Attract) {
516
- try log.activate(ident, .Attract);
526
+ try log.activate(.{ ident, .Attract });
517
527
 
518
528
  if (try Rolls.attract(battle, player, options)) {
519
529
  resetCant(volatiles);
520
- try log.cant(ident, .Attract);
530
+ try log.cant(.{ ident, .Attract });
521
531
  return false;
522
532
  }
523
533
  }
@@ -528,14 +538,14 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
528
538
  if (m != .None and m == move) {
529
539
  side.active.volatiles.Charging = false;
530
540
  resetCant(volatiles);
531
- try options.log.disabled(ident, move);
541
+ try options.log.cant(.{ ident, .Disable, move });
532
542
  return false;
533
543
  }
534
544
  }
535
545
 
536
546
  if (Status.is(stored.status, .PAR) and try Rolls.paralyzed(battle, player, options)) {
537
547
  resetCant(volatiles);
538
- try log.cant(ident, .Paralysis);
548
+ try log.cant(.{ ident, .Paralysis });
539
549
  return false;
540
550
  }
541
551
 
@@ -559,7 +569,7 @@ pub fn canMove(battle: anytype, player: Player, state: *State, options: anytype)
559
569
 
560
570
  // FIXME: pass in target to avoid lookup?
561
571
  const target = if (Move.get(state.move).target == .Self) player else player.foe();
562
- try options.log.moveFrom(battle.active(player), state.move, battle.active(target), state.from);
572
+ try options.log.move(.{ battle.active(player), state.move, battle.active(target), state.from });
563
573
 
564
574
  const charging = false; // TODO charging uses doUse
565
575
  const skip_pp = charging or state.move == .Struggle or
@@ -638,7 +648,7 @@ pub fn afterMove(
638
648
 
639
649
  pub fn checkCriticalHit(battle: anytype, player: Player, state: *State, options: anytype) !void {
640
650
  const move = Move.get(state.move);
641
- if (move.bp == 0) return;
651
+ if (state.bp == 0) return;
642
652
 
643
653
  const side = battle.side(player);
644
654
  const pokemon = side.stored();
@@ -676,31 +686,30 @@ pub fn adjustDamage(battle: anytype, player: Player, state: *State, _: anytype)
676
686
  const side = battle.side(player);
677
687
  const foe = battle.foe(player);
678
688
  const types = foe.active.types;
679
- const move = Move.get(state.move);
680
689
 
681
690
  var d = state.damage;
682
- if ((move.type == .Water and battle.field.weather == .Rain) or
683
- (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))
684
693
  {
685
694
  d = d *% @intFromEnum(Effectiveness.Super) / 10;
686
- } else if ((move.type == .Water and battle.field.weather == .Sun) or
687
- ((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))
688
697
  {
689
698
  d = d *% @intFromEnum(Effectiveness.Resisted) / 10;
690
699
  }
691
700
 
692
- if (side.active.types.includes(move.type)) d +%= d / 2;
701
+ if (side.active.types.includes(state.type)) d +%= d / 2;
693
702
 
694
703
  const neutral = @intFromEnum(Effectiveness.Neutral);
695
- const foresight = foe.active.volatiles.Foresight and FORESIGHT.includes(move.type);
704
+ const foresight = foe.active.volatiles.Foresight and FORESIGHT.includes(state.type);
696
705
  const eff1: u16 = if (foresight and types.type1 == .Ghost)
697
706
  neutral
698
707
  else
699
- @intFromEnum(move.type.effectiveness(types.type1));
708
+ @intFromEnum(state.type.effectiveness(types.type1));
700
709
  const eff2: u16 = if (foresight and types.type2 == .Ghost)
701
710
  neutral
702
711
  else
703
- @intFromEnum(move.type.effectiveness(types.type2));
712
+ @intFromEnum(state.type.effectiveness(types.type2));
704
713
 
705
714
  // Type effectiveness matchup precedence only matters with (NVE, SE)
706
715
  if (!showdown and (eff1 + eff2) == Effectiveness.mismatch and
@@ -840,13 +849,13 @@ fn checkFaint(
840
849
  if (foe_fainted) try faint(battle, player.foe(), !more, options);
841
850
 
842
851
  if (player_out and foe_out) {
843
- try options.log.tie();
852
+ try options.log.tie(.{});
844
853
  return Result.Tie;
845
854
  } else if (player_out) {
846
- try options.log.win(player.foe());
855
+ try options.log.win(.{player.foe()});
847
856
  return if (player == .P1) Result.Lose else Result.Win;
848
857
  } else if (foe_out) {
849
- try options.log.win(player);
858
+ try options.log.win(.{player});
850
859
  return if (player == .P1) Result.Win else Result.Lose;
851
860
  }
852
861
 
@@ -876,19 +885,19 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !void {
876
885
  side.stored().stats.spe;
877
886
  }
878
887
 
879
- try options.log.faint(battle.active(player), done);
888
+ try options.log.faint(.{ battle.active(player), done });
880
889
  options.calc.capped(player);
881
890
  }
882
891
 
883
892
  // TODO: see if we can move *before* applyDamage and include failuretext logic
884
893
  pub fn reportOutcome(battle: anytype, player: Player, state: *State, options: anytype) !void {
885
894
  const foe_ident = battle.active(player.foe());
886
- if (state.crit) try options.log.crit(foe_ident);
895
+ if (state.crit) try options.log.crit(.{foe_ident});
887
896
  if (!state.immune()) {
888
897
  if (state.effectiveness > Effectiveness.neutral) {
889
- try options.log.supereffective(foe_ident);
898
+ try options.log.supereffective(.{foe_ident});
890
899
  } else if (state.effectiveness < Effectiveness.neutral) {
891
- try options.log.resisted(foe_ident);
900
+ try options.log.resisted(.{foe_ident});
892
901
  }
893
902
  }
894
903
  }
@@ -921,8 +930,8 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !bool {
921
930
  }
922
931
 
923
932
  stored.hp -|= damage;
924
- // Pokémon Showdown uses damageOf here but its not relevant in Generation II
925
- try options.log.damage(ident, stored, if (brn) Damage.Burn else Damage.Poison);
933
+ // Pokémon Showdown uses damage [of] here but its not relevant in Generation II
934
+ try options.log.damage(.{ ident, stored, if (brn) Damage.Burn else Damage.Poison });
926
935
  if (stored.hp == 0) return true;
927
936
  }
928
937
 
@@ -938,27 +947,27 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !bool {
938
947
 
939
948
  const damage = @max(stored.stats.hp / 8, 1);
940
949
  stored.hp -|= damage;
941
- // As above, Pokémon Showdown uses damageOf but its not relevant
942
- try options.log.damage(ident, stored, .LeechSeed);
950
+ // As above, Pokémon Showdown uses damage [of] but its not relevant
951
+ try options.log.damage(.{ ident, stored, .LeechSeed });
943
952
 
944
953
  const before = foe_stored.hp;
945
954
  foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
946
955
  // Pokémon Showdown uses the less specific heal here instead of drain... because reasons?
947
- 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, .Silent });
948
957
  if (stored.hp == 0) return true;
949
958
  }
950
959
 
951
960
  if (volatiles.Nightmare) {
952
961
  stored.hp -= @max(stored.stats.hp / 4, 1);
953
962
  // ibid
954
- try options.log.damage(ident, stored, .Nightmare);
963
+ try options.log.damage(.{ ident, stored, .Nightmare });
955
964
  if (stored.hp == 0) return true;
956
965
  }
957
966
 
958
967
  if (volatiles.Curse) {
959
968
  stored.hp -= @max(stored.stats.hp / 4, 1);
960
969
  // ibid
961
- try options.log.damage(ident, stored, .Curse);
970
+ try options.log.damage(.{ ident, stored, .Curse });
962
971
  if (stored.hp == 0) return true;
963
972
  }
964
973
 
@@ -979,7 +988,7 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
979
988
  future_sight.count -= 1;
980
989
  assert(future_sight.count != 0);
981
990
  if (future_sight.count == 1) {
982
- try options.log.end(battle.active(player), .FutureSight);
991
+ try options.log.end(.{ battle.active(player), .FutureSight });
983
992
  // TODO doMove
984
993
  }
985
994
  }
@@ -993,16 +1002,20 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
993
1002
 
994
1003
  if (battle.field.weather_duration == 0) {
995
1004
  battle.field.weather = .None;
996
- try options.log.weather(battle.field.weather, .None);
1005
+ try options.log.weather(.{ battle.field.weather, protocol.Weather.None });
997
1006
  } else {
998
- try options.log.weather(battle.field.weather, .Upkeep);
1007
+ try options.log.weather(.{ battle.field.weather, protocol.Weather.Upkeep });
999
1008
  if (battle.field.weather == .Sandstorm) {
1000
1009
  inline for (players) |player| {
1001
1010
  var side = battle.side(player);
1002
1011
  const active = side.active;
1003
1012
  if (!(active.volatiles.Underground or active.types.sandstormImmune())) {
1004
1013
  side.stored().hp -|= @max(side.stored().stats.hp / 8, 1);
1005
- try options.log.damage(battle.active(player), side.stored(), .Sandstorm);
1014
+ try options.log.damage(.{
1015
+ battle.active(player),
1016
+ side.stored(),
1017
+ .Sandstorm,
1018
+ });
1006
1019
  }
1007
1020
  }
1008
1021
  if (try checkFaint(battle, .P1, options)) |r| return r;
@@ -1014,19 +1027,24 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1014
1027
  inline for (players) |player| {
1015
1028
  var side = battle.side(player);
1016
1029
  var volatiles = &side.active.volatiles;
1030
+ const ident = battle.active(player);
1017
1031
 
1018
1032
  if (volatiles.bind.duration > 0 and !volatiles.Substitute) {
1019
1033
  volatiles.bind.duration -= 1;
1020
1034
  if (volatiles.bind.duration == 0) {
1021
1035
  const reason = @intFromEnum(End.Bind) + volatiles.bind.reason - 1;
1022
- try options.log.end(battle.active(player), @enumFromInt(reason));
1036
+ try options.log.end(.{ ident, @as(End, @enumFromInt(reason)) });
1023
1037
  } else {
1024
1038
  var reason = @intFromEnum(Activate.Bind) + volatiles.bind.reason - 1;
1025
- try options.log.activate(battle.active(player), @enumFromInt(reason));
1039
+ try options.log.activate(.{ ident, @as(Activate, @enumFromInt(reason)) });
1026
1040
 
1027
1041
  reason = @intFromEnum(Damage.Bind) + volatiles.bind.reason - 1;
1028
1042
  side.stored().hp -|= @max(side.stored().stats.hp / 16, 1);
1029
- try options.log.damage(battle.active(player), side.stored(), @enumFromInt(reason));
1043
+ try options.log.damage(.{
1044
+ ident,
1045
+ side.stored(),
1046
+ @as(Damage, @enumFromInt(reason)),
1047
+ });
1030
1048
  }
1031
1049
  }
1032
1050
  }
@@ -1043,7 +1061,7 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1043
1061
 
1044
1062
  assert(volatiles.perish_song < 3);
1045
1063
  const reason = @intFromEnum(Start.PerishSong0) + volatiles.perish_song;
1046
- try options.log.start(battle.active(player), @enumFromInt(reason));
1064
+ try options.log.start(.{ battle.active(player), @as(Start, @enumFromInt(reason)) });
1047
1065
 
1048
1066
  if (volatiles.perish_song == 0) {
1049
1067
  volatiles.PerishSong = false;
@@ -1060,7 +1078,9 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1060
1078
  if (stored.item == .Leftovers) {
1061
1079
  const before = stored.hp;
1062
1080
  stored.hp = @min(stored.hp + @max(stored.stats.hp / 16, 1), stored.stats.hp);
1063
- if (stored.hp > before) try options.log.heal(battle.active(player), stored, .Leftovers);
1081
+ if (stored.hp > before) {
1082
+ try options.log.heal(.{ battle.active(player), stored, .Leftovers });
1083
+ }
1064
1084
  }
1065
1085
  }
1066
1086
 
@@ -1081,7 +1101,7 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1081
1101
  !side.active.volatiles.frozen and
1082
1102
  try Rolls.defrost(battle, player, options))
1083
1103
  {
1084
- try options.log.curestatus(battle.active(player), side.stored().status, .Message);
1104
+ try options.log.curestatus(.{ battle.active(player), side.stored().status, .Message });
1085
1105
  side.stored().status = 0;
1086
1106
  }
1087
1107
  }
@@ -1122,14 +1142,14 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1122
1142
  stored.hp < stored.stats.hp;
1123
1143
  if (proc) {
1124
1144
  assert(stored.hp != 0);
1125
- try options.log.enditem(ident, stored.item, .Eat);
1145
+ try options.log.enditem(.{ ident, stored.item, .Eat });
1126
1146
  stored.item = .None;
1127
1147
 
1128
1148
  const offset = num - @intFromEnum(Item.Berry);
1129
1149
  assert(offset < 3);
1130
1150
  stored.hp = @min(stored.hp + ((offset + 1) * 10), stored.stats.hp);
1131
1151
  const reason: Heal = @enumFromInt(@intFromEnum(Heal.Berry) + offset);
1132
- try options.log.heal(ident, stored, reason);
1152
+ try options.log.heal(.{ ident, stored, reason });
1133
1153
  }
1134
1154
  } else if (num < @intFromEnum(Item.BitterBerry)) {
1135
1155
  const proc = if (stored.item == .MiracleBerry)
@@ -1139,7 +1159,7 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1139
1159
 
1140
1160
  if (proc) {
1141
1161
  assert(Status.any(stored.status));
1142
- try options.log.enditem(ident, stored.item, .Eat);
1162
+ try options.log.enditem(.{ ident, stored.item, .Eat });
1143
1163
  stored.item = .None;
1144
1164
 
1145
1165
  // FIXME wtf?
@@ -1147,21 +1167,21 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1147
1167
  // side.active.volatiles.Nightmare = false;
1148
1168
  if (stored.item == .MiracleBerry) {
1149
1169
  side.active.volatiles.Confusion = false;
1150
- try options.log.end(ident, .Confusion);
1170
+ try options.log.end(.{ ident, .Confusion });
1151
1171
  }
1152
1172
  stored.status = 0;
1153
1173
 
1154
1174
  // FIXME recalc stats
1155
1175
 
1156
- try options.log.curestatus(ident, stored.status, .Message);
1176
+ try options.log.curestatus(.{ ident, stored.status, .Message });
1157
1177
  }
1158
1178
  } else if (num == @intFromEnum(Item.BitterBerry)) {
1159
1179
  if (side.active.volatiles.Confusion) {
1160
- try options.log.enditem(ident, stored.item, .Eat);
1180
+ try options.log.enditem(.{ ident, stored.item, .Eat });
1161
1181
  stored.item = .None;
1162
1182
 
1163
1183
  side.active.volatiles.Confusion = false;
1164
- try options.log.end(ident, .Confusion);
1184
+ try options.log.end(.{ ident, .Confusion });
1165
1185
  }
1166
1186
  }
1167
1187
  }
@@ -1178,7 +1198,7 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1178
1198
 
1179
1199
  if (volatiles.encore == 0 or side.active.move(mslot).pp == 0) {
1180
1200
  volatiles.Encore = false;
1181
- try options.log.end(battle.active(player), .Encore);
1201
+ try options.log.end(.{ battle.active(player), .Encore });
1182
1202
  }
1183
1203
  }
1184
1204
  }
@@ -1200,7 +1220,7 @@ fn updateSideCondition(
1200
1220
 
1201
1221
  if (@field(side.conditions, value) == 0) {
1202
1222
  @field(side.conditions, key) = false;
1203
- try options.log.sideend(player, @field(protocol.Side, key));
1223
+ try options.log.sideend(.{ player, @field(protocol.Side, key) });
1204
1224
  }
1205
1225
  }
1206
1226
  }
@@ -1209,13 +1229,13 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
1209
1229
  battle.turn += 1;
1210
1230
 
1211
1231
  if (showdown and battle.turn >= 1000) {
1212
- try options.log.tie();
1232
+ try options.log.tie(.{});
1213
1233
  return Result.Tie;
1214
1234
  } else if (battle.turn >= 65535) {
1215
1235
  return Result.Error;
1216
1236
  }
1217
1237
 
1218
- try options.log.turn(battle.turn);
1238
+ try options.log.turn(.{battle.turn});
1219
1239
 
1220
1240
  return Result.Default;
1221
1241
  }
@@ -1231,7 +1251,7 @@ pub fn buildRage(battle: anytype, player: Player, state: *State, options: anytyp
1231
1251
  }
1232
1252
  } else {
1233
1253
  foe.active.volatiles.rage +|= 1;
1234
- try options.log.activate(battle.active(player), .Rage);
1254
+ try options.log.activate(.{ battle.active(player), .Rage });
1235
1255
  }
1236
1256
  }
1237
1257
  }
@@ -1254,7 +1274,7 @@ pub fn destinyBond(battle: anytype, player: Player, _: *State, options: anytype)
1254
1274
 
1255
1275
  if (foe.active.volatiles.DestinyBond) {
1256
1276
  battle.side(player).stored().hp = 0;
1257
- try options.log.activate(battle.active(player.foe()), .DestinyBond);
1277
+ try options.log.activate(.{ battle.active(player.foe()), .DestinyBond });
1258
1278
  }
1259
1279
  return true;
1260
1280
  }
@@ -1267,14 +1287,14 @@ pub const Effects = struct {
1267
1287
  const gender: u8 = @intFromEnum(side.stored().dvs.gender);
1268
1288
  const foe_gender: u8 = @intFromEnum(foe.stored().dvs.gender);
1269
1289
  if (gender + foe_gender != 1) {
1270
- return options.log.immune(battle.active(player.foe()), .None);
1290
+ return options.log.immune(.{ battle.active(player.foe()), .None });
1271
1291
  } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
1272
- try options.log.lastmiss();
1273
- return options.log.miss(battle.active(player));
1292
+ try options.log.lastmiss(.{});
1293
+ return options.log.miss(.{battle.active(player)});
1274
1294
  }
1275
1295
 
1276
1296
  foe.active.volatiles.Attract = true;
1277
- try options.log.start(battle.active(player.foe()), .Attract);
1297
+ try options.log.start(.{ battle.active(player.foe()), .Attract });
1278
1298
  }
1279
1299
 
1280
1300
  pub fn batonPass(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1287,6 +1307,13 @@ pub const Effects = struct {
1287
1307
 
1288
1308
  pub fn bellyDrum(battle: anytype, player: Player, state: *State, options: anytype) !void {
1289
1309
  _ = .{ battle, player, state, options }; // TODO
1310
+ // assert(!state.miss);
1311
+ // var side = battle.side(player);
1312
+ // try boostStat(battle, side, &side.active.stats, &side.active.boosts, "atk", 2);
1313
+ // TODO
1314
+ // |-boost|p1a: Poliwag|atk|2|[silent]
1315
+ // |-fail|p1a: Poliwag
1316
+ //
1290
1317
  }
1291
1318
 
1292
1319
  pub const bide = struct {
@@ -1306,10 +1333,10 @@ pub const Effects = struct {
1306
1333
  // }
1307
1334
  try options.chance.attacking(player, volatiles.attacks);
1308
1335
 
1309
- if (volatiles.attacks != 0) return options.log.activate(ident, .Bide);
1336
+ if (volatiles.attacks != 0) return options.log.activate(.{ ident, .Bide });
1310
1337
 
1311
1338
  volatiles.Bide = false;
1312
- try options.log.end(ident, .Bide);
1339
+ try options.log.end(.{ ident, .Bide });
1313
1340
 
1314
1341
  state.damage = volatiles.bide *% 2;
1315
1342
  volatiles.bide = 0;
@@ -1323,7 +1350,16 @@ pub const Effects = struct {
1323
1350
  };
1324
1351
 
1325
1352
  pub fn binding(battle: anytype, player: Player, state: *State, options: anytype) !void {
1326
- _ = .{ battle, player, state, options }; // TODO
1353
+ var foe = battle.foe(player);
1354
+
1355
+ if (state.miss) return;
1356
+ if (foe.active.volatiles.bind.duration > 0) return;
1357
+ if (foe.active.volatiles.Substitute) return;
1358
+
1359
+ foe.active.volatiles.bind = .{
1360
+ .duration = Rolls.bindingDuration(battle, player, options),
1361
+ .reason = Move.get(state.move).extra.protocol,
1362
+ };
1327
1363
  }
1328
1364
 
1329
1365
  pub fn burnChance(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1334,11 +1370,11 @@ pub const Effects = struct {
1334
1370
  if (Status.any(foe_stored.status)) {
1335
1371
  if (Status.is(foe_stored.status, .FRZ)) {
1336
1372
  assert(Move.get(state.move).type == .Fire or state.move == .TriAttack);
1337
- try options.log.curestatus(
1373
+ try options.log.curestatus(.{
1338
1374
  battle.active(player.foe()),
1339
1375
  foe_stored.status,
1340
1376
  .Message,
1341
- );
1377
+ });
1342
1378
  foe_stored.status = 0;
1343
1379
  }
1344
1380
  return;
@@ -1353,7 +1389,8 @@ pub const Effects = struct {
1353
1389
 
1354
1390
  foe_stored.status = Status.init(.BRN);
1355
1391
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
1356
- try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
1392
+
1393
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
1357
1394
 
1358
1395
  // TODO: check for status berry
1359
1396
  }
@@ -1373,7 +1410,7 @@ pub const Effects = struct {
1373
1410
  for (side.active.moves) |m| {
1374
1411
  if (convertible(side.active, m.id)) n += 1;
1375
1412
  }
1376
- if (n == 0) return options.log.fail(battle.active(player), .None);
1413
+ if (n == 0) return options.log.fail(.{ battle.active(player), .None });
1377
1414
 
1378
1415
  side.active.types.type1 = try Rolls.conversion(battle, player, n, options);
1379
1416
  side.active.types.type2 = side.active.types.type1;
@@ -1383,20 +1420,87 @@ pub const Effects = struct {
1383
1420
  var side = battle.side(player);
1384
1421
 
1385
1422
  const last = battle.foe(player).lastMove(false);
1386
- if (last == .None) return options.log.fail(battle.active(player), .None);
1423
+ if (last == .None) return options.log.fail(.{ battle.active(player), .None });
1387
1424
  const move = Move.get(last);
1388
- if (move.type == .@"???") return options.log.fail(battle.active(player), .None);
1425
+ if (move.type == .@"???") return options.log.fail(.{ battle.active(player), .None });
1389
1426
 
1390
1427
  side.active.types.type1 = try Rolls.conversion2(battle, player, move.type, options);
1391
1428
  side.active.types.type2 = side.active.types.type1;
1392
1429
  }
1393
1430
 
1394
- pub fn counter(battle: anytype, player: Player, state: *State, options: anytype) !void {
1395
- _ = .{ battle, player, state, options }; // TODO
1431
+ pub fn counter(battle: anytype, player: Player, state: *State, _: anytype) !void {
1432
+ assert(state.move == .Counter or state.move == .MirrorCoat);
1433
+
1434
+ const foe = battle.foe(player);
1435
+
1436
+ state.miss = true;
1437
+ const last = foe.lastMove(false);
1438
+
1439
+ if (last == .None) return;
1440
+ if (last == state.move) return;
1441
+
1442
+ const immune = state.immune();
1443
+ state.effectiveness = Effectiveness.neutral;
1444
+ if (immune) {
1445
+ state.damage = 0;
1446
+ return;
1447
+ }
1448
+
1449
+ const move = Move.get(last);
1450
+ if (state.bp == 0) return;
1451
+ if (move.type.special() != (last == .MirrorCoat)) return;
1452
+ if (state.damage == 0) return;
1453
+
1454
+ state.damage *|= 2;
1455
+ state.miss = false;
1396
1456
  }
1397
1457
 
1398
1458
  pub fn curse(battle: anytype, player: Player, state: *State, options: anytype) !void {
1399
- _ = .{ battle, player, state, options }; // TODO
1459
+ var side = battle.side(player);
1460
+ var foe = battle.foe(player);
1461
+
1462
+ const ident = battle.active(player);
1463
+ const foe_ident = battle.active(player.foe());
1464
+
1465
+ if (side.active.types.includes(.Ghost)) {
1466
+ if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
1467
+ try options.log.lastmiss(.{});
1468
+ return options.log.miss(.{battle.active(player)});
1469
+ } else if (foe.active.volatiles.Substitute) {
1470
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
1471
+ } else if (foe.active.volatiles.Curse) {
1472
+ return options.log.fail(.{ foe_ident, .None });
1473
+ }
1474
+
1475
+ foe.active.volatiles.Curse = true;
1476
+ try options.log.start(.{ foe_ident, .Curse });
1477
+
1478
+ side.stored().hp -= @max(side.stored().stats.hp / 2, 1);
1479
+ try options.log.damage(.{ ident, side.stored(), .Curse });
1480
+ } else {
1481
+ var stats = &side.active.stats;
1482
+ var boosts = &side.active.boosts;
1483
+
1484
+ const max_atk = boosts.atk == 6 or stats.atk == MAX_STAT_VALUE;
1485
+ const max_def = boosts.def == 6 or stats.def == MAX_STAT_VALUE;
1486
+ if (max_atk and max_def) return options.log.fail(.{ ident, .None });
1487
+
1488
+ if (!(boosts.spe == -6 or stats.spe == 1)) {
1489
+ assert(boosts.spe >= -6 and boosts.spe <= 6);
1490
+ try unboostStat(battle, side, stats, boosts, "spe", 1);
1491
+ try options.log.boost(.{ ident, .Speed, @as(i8, -1) });
1492
+ }
1493
+ if (!max_atk) {
1494
+ assert(boosts.atk >= -6 and boosts.atk <= 6);
1495
+ try boostStat(battle, side, stats, boosts, "atk", 1);
1496
+ try options.log.boost(.{ ident, .Attack, @as(i8, 1) });
1497
+ }
1498
+ if (!max_def) {
1499
+ assert(boosts.def >= -6 and boosts.def <= 6);
1500
+ try boostStat(battle, side, stats, boosts, "def", 1);
1501
+ try options.log.boost(.{ ident, .Defense, @as(i8, 1) });
1502
+ }
1503
+ }
1400
1504
  }
1401
1505
 
1402
1506
  pub fn defenseCurl(battle: anytype, player: Player, _: *State, _: anytype) !void {
@@ -1406,14 +1510,13 @@ pub const Effects = struct {
1406
1510
  pub fn defrost(battle: anytype, player: Player, _: *State, options: anytype) !void {
1407
1511
  var side = battle.side(player);
1408
1512
  assert(Status.is(side.stored().status, .FRZ));
1409
-
1410
- try options.log.curestatus(battle.active(player), side.stored().status, .Message);
1513
+ try options.log.curestatus(.{ battle.active(player), side.stored().status, .Message });
1411
1514
  side.stored().status = 0;
1412
1515
  }
1413
1516
 
1414
1517
  pub fn destinyBond(battle: anytype, player: Player, _: *State, options: anytype) !void {
1415
1518
  battle.side(player).active.volatiles.DestinyBond = true;
1416
- try options.log.singlemove(battle.active(player), Move.DestinyBond);
1519
+ try options.log.singlemove(.{ battle.active(player), Move.DestinyBond });
1417
1520
  }
1418
1521
 
1419
1522
  pub fn disable(battle: anytype, player: Player, _: *State, options: anytype) !void {
@@ -1421,14 +1524,10 @@ pub const Effects = struct {
1421
1524
  var volatiles = &foe.active.volatiles;
1422
1525
  const foe_ident = battle.active(player.foe());
1423
1526
 
1424
- if (volatiles.disable.move != 0) {
1425
- return options.log.fail(foe_ident, .None);
1426
- }
1527
+ if (volatiles.disable.move != 0) return options.log.fail(.{ foe_ident, .None });
1427
1528
 
1428
1529
  const last = battle.foe(player).lastMove(false);
1429
- if (last == .None or last == .Struggle) {
1430
- return options.log.fail(foe_ident, .None);
1431
- }
1530
+ if (last == .None or last == .Struggle) return options.log.fail(.{ foe_ident, .None });
1432
1531
 
1433
1532
  var slot: u3 = 0;
1434
1533
  for (foe.active.moves) |m| {
@@ -1437,12 +1536,12 @@ pub const Effects = struct {
1437
1536
  if (m.id == last) break;
1438
1537
  }
1439
1538
  const move = foe.active.move(slot);
1440
- if (move.pp == 0) return options.log.fail(foe_ident, .None);
1539
+ if (move.pp == 0) return options.log.fail(.{ foe_ident, .None });
1441
1540
 
1442
1541
  volatiles.disable.move = slot;
1443
1542
  volatiles.disable.duration = Rolls.disableDuration(battle, player, options);
1444
1543
 
1445
- try options.log.startEffect(foe_ident, .Disable, move.id);
1544
+ try options.log.start(.{ foe_ident, .Disable, move.id });
1446
1545
  }
1447
1546
 
1448
1547
  pub const double = struct {
@@ -1461,13 +1560,14 @@ pub const Effects = struct {
1461
1560
 
1462
1561
  pub fn drainHP(battle: anytype, player: Player, state: *State, options: anytype) !void {
1463
1562
  var stored = battle.side(player).stored();
1563
+ const ident = battle.active(player);
1464
1564
 
1465
1565
  assert(state.damage != 0);
1466
1566
 
1467
1567
  if (stored.hp == stored.stats.hp) return;
1468
1568
  stored.hp = @min(stored.stats.hp, stored.hp + @max(state.damage / 2, 1));
1469
1569
 
1470
- try options.log.drain(battle.active(player), stored, battle.active(player.foe()));
1570
+ try options.log.heal(.{ ident, stored, .Drain, battle.active(player.foe()) });
1471
1571
  }
1472
1572
 
1473
1573
  pub fn encore(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1479,7 +1579,7 @@ pub const Effects = struct {
1479
1579
  const failed = volatiles.Encore or
1480
1580
  (last == .None or last == .Struggle or
1481
1581
  last == .Encore or .last == .MirrorMove);
1482
- if (failed) return options.log.fail(foe_ident, .None);
1582
+ if (failed) return options.log.fail(.{ foe_ident, .None });
1483
1583
 
1484
1584
  var slot: u3 = 0;
1485
1585
  for (foe.active.moves) |m| {
@@ -1488,7 +1588,7 @@ pub const Effects = struct {
1488
1588
  if (m.id == last) break;
1489
1589
  }
1490
1590
  const move = foe.active.move(slot);
1491
- if (move.pp == 0) return options.log.fail(foe_ident, .None);
1591
+ if (move.pp == 0) return options.log.fail(.{ foe_ident, .None });
1492
1592
 
1493
1593
  volatiles.Encore = true;
1494
1594
  volatiles.encore = Rolls.encoreDuration(battle, player, options);
@@ -1496,7 +1596,7 @@ pub const Effects = struct {
1496
1596
  // TODO ???
1497
1597
  _ = state;
1498
1598
 
1499
- try options.log.startEffect(foe_ident, .Encore, move.id);
1599
+ try options.log.start(.{ foe_ident, .Encore, move.id });
1500
1600
  }
1501
1601
 
1502
1602
  pub fn explode(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1550,12 +1650,11 @@ pub const Effects = struct {
1550
1650
  var side = battle.side(player);
1551
1651
 
1552
1652
  if (side.active.volatiles.FocusEnergy) {
1553
- if (!showdown) try options.log.fail(battle.active(player), .None);
1554
- return;
1653
+ return if (!showdown) try options.log.fail(.{ battle.active(player), .None });
1555
1654
  }
1556
1655
 
1557
1656
  side.active.volatiles.FocusEnergy = true;
1558
- try options.log.start(battle.active(player), .FocusEnergy);
1657
+ try options.log.start(.{ battle.active(player), .FocusEnergy });
1559
1658
  }
1560
1659
 
1561
1660
  var SLOTS: [5]u4 = .{ 0, 0, 0, 0, 0 };
@@ -1565,7 +1664,7 @@ pub const Effects = struct {
1565
1664
 
1566
1665
  const foe = battle.foe(player);
1567
1666
 
1568
- if (state.first) return options.log.fail(battle.active(player), .None);
1667
+ if (state.first) return options.log.fail(.{ battle.active(player), .None });
1569
1668
 
1570
1669
  var n: u4 = 0;
1571
1670
  var i: u4 = 0;
@@ -1579,7 +1678,7 @@ pub const Effects = struct {
1579
1678
  }
1580
1679
  }
1581
1680
 
1582
- if (n == 0) return options.log.fail(battle.active(player), .None);
1681
+ if (n == 0) return options.log.fail(.{ battle.active(player), .None });
1583
1682
 
1584
1683
  const slot = try Rolls.forceSwitch(battle, player, SLOTS[0..i], n, options);
1585
1684
  try switchIn(battle, player.foe(), slot, .Drag, options);
@@ -1589,14 +1688,14 @@ pub const Effects = struct {
1589
1688
  var foe = battle.foe(player);
1590
1689
 
1591
1690
  if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
1592
- try options.log.lastmiss();
1593
- return options.log.miss(battle.active(player));
1691
+ try options.log.lastmiss(.{});
1692
+ return options.log.miss(.{battle.active(player)});
1594
1693
  } else if (foe.active.volatiles.Foresight) {
1595
- return options.log.fail(battle.active(player), .None);
1694
+ return options.log.fail(.{ battle.active(player), .None });
1596
1695
  }
1597
1696
 
1598
1697
  foe.active.volatiles.Foresight = true;
1599
- try options.log.start(battle.active(player.foe()), .FocusEnergy);
1698
+ try options.log.start(.{ battle.active(player.foe()), .FocusEnergy });
1600
1699
  }
1601
1700
 
1602
1701
  pub fn freezeChance(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1619,7 +1718,7 @@ pub const Effects = struct {
1619
1718
  if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1620
1719
 
1621
1720
  foe_stored.status = Status.init(.FRZ);
1622
- try options.log.status(foe_ident, foe_stored.status, .None);
1721
+ try options.log.status(.{ foe_ident, foe_stored.status, .None });
1623
1722
 
1624
1723
  // TODO: check for status berry
1625
1724
 
@@ -1641,42 +1740,131 @@ pub const Effects = struct {
1641
1740
  state.bp = ((if (frustration) 255 - val else val) * 10) / 25;
1642
1741
  }
1643
1742
 
1644
- pub fn haze(battle: anytype, player: Player, state: *State, options: anytype) !void {
1645
- _ = .{ battle, player, state, options }; // TODO
1743
+ pub fn haze(battle: anytype, player: Player, _: *State, options: anytype) !void {
1744
+ var side = battle.side(player);
1745
+ var stats = unmodifiedStats(battle, side);
1746
+ side.active.boosts = .{};
1747
+ inline for (STATS) |s| @field(side.active.stats, s) = @field(stats, s);
1748
+ statusModify(side.stored().status, &side.active.stats);
1749
+
1750
+ var foe = battle.foe(player);
1751
+ stats = unmodifiedStats(battle, foe);
1752
+ foe.active.boosts = .{};
1753
+ inline for (STATS) |s| @field(foe.active.stats, s) = @field(stats, s);
1754
+ statusModify(foe.stored().status, &foe.active.stats);
1755
+
1756
+ try options.log.clearallboost(.{});
1646
1757
  }
1647
1758
 
1648
1759
  pub fn heal(battle: anytype, player: Player, state: *State, options: anytype) !void {
1649
- _ = .{ battle, player, state, options }; // TODO
1760
+ var side = battle.side(player);
1761
+ var stored = side.stored();
1762
+ const ident = battle.active(player);
1763
+
1764
+ if (stored.stats.hp == stored.hp) return options.log.fail(.{ ident, .None });
1765
+
1766
+ const rest = state.move == .Rest;
1767
+ if (rest) {
1768
+ // Adding the sleep status runs the sleep condition handler to roll duration
1769
+ if (showdown) battle.rng.advance(1);
1770
+ side.active.volatiles.Toxic = false;
1771
+ stored.status = Status.slf(2);
1772
+ recomputeStats(battle, side);
1773
+ try options.log.status(.{ ident, stored.status, .From, Move.Rest });
1774
+ stored.hp = stored.stats.hp;
1775
+ } else {
1776
+ stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
1777
+ }
1778
+ try options.log.heal(.{ ident, stored, if (rest) Heal.Silent else Heal.None });
1650
1779
  }
1651
1780
 
1652
- pub fn healBell(battle: anytype, player: Player, state: *State, options: anytype) !void {
1653
- _ = .{ battle, player, state, options }; // TODO
1781
+ pub fn healBell(battle: anytype, player: Player, _: *State, options: anytype) !void {
1782
+ var side = battle.side(player);
1783
+ side.active.volatiles.Nightmare = false;
1784
+
1785
+ try options.log.cureteam(.{battle.active(player)});
1786
+ for (&side.pokemon) |*p| p.status = 0;
1787
+
1788
+ // We technically only need to recompute stats if the active Pokémon was burned/paralyzed,
1789
+ // but adding a branch to check for that is probably more expensive than just recomputing
1790
+ recomputeStats(battle, side);
1654
1791
  }
1655
1792
 
1656
- pub fn hiddenPower(battle: anytype, player: Player, state: *State, options: anytype) !void {
1657
- _ = .{ battle, player, state, options }; // TODO
1793
+ pub fn hiddenPower(battle: anytype, player: Player, state: *State, _: anytype) !void {
1794
+ const side = battle.side(player);
1795
+ state.bp = side.stored().dvs.power();
1796
+ state.type = side.stored().dvs.type;
1658
1797
  }
1659
1798
 
1660
- pub fn hyperBeam(battle: anytype, player: Player, state: *State, options: anytype) !void {
1661
- _ = .{ battle, player, state, options }; // TODO
1799
+ pub fn hyperBeam(battle: anytype, player: Player, _: *State, options: anytype) !void {
1800
+ battle.side(player).active.volatiles.Recharging = true;
1801
+ try options.log.mustrecharge(.{battle.active(player)});
1662
1802
  }
1663
1803
 
1664
1804
  pub fn leechSeed(battle: anytype, player: Player, state: *State, options: anytype) !void {
1665
- _ = .{ battle, player, state, options }; // TODO
1805
+ var foe = battle.foe(player);
1806
+
1807
+ if (showdown) {
1808
+ // Invulnerability trumps type immunity on Pokémon Showdown
1809
+ const invulnerable = foe.active.volatiles.Underground or foe.active.volatiles.Flying;
1810
+ if (!invulnerable and foe.active.types.includes(.Grass)) {
1811
+ return options.log.immune(.{ battle.active(player.foe()), .None });
1812
+ }
1813
+ try checkHit(battle, player, state, options);
1814
+ if (state.miss) {
1815
+ try options.log.lastmiss(.{});
1816
+ return options.log.miss(.{battle.active(player)});
1817
+ }
1818
+ if (foe.active.volatiles.LeechSeed) return;
1819
+ } else {
1820
+ try checkHit(battle, player, state, options);
1821
+ const miss =
1822
+ state.miss or foe.active.types.includes(.Grass) or foe.active.volatiles.LeechSeed;
1823
+ if (miss) {
1824
+ try options.log.lastmiss(.{});
1825
+ return options.log.miss(.{battle.active(player)});
1826
+ }
1827
+ }
1828
+
1829
+ foe.active.volatiles.LeechSeed = true;
1830
+
1831
+ try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
1666
1832
  }
1667
1833
 
1668
1834
  pub fn lockOn(battle: anytype, player: Player, state: *State, options: anytype) !void {
1669
- _ = .{ battle, player, state, options }; // TODO
1835
+ var foe = battle.foe(player);
1836
+ const foe_ident = battle.active(player.foe());
1837
+
1838
+ if (foe.active.volatiles.Substitute) {
1839
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
1840
+ } else if (state.miss) {
1841
+ return options.log.fail(.{ foe_ident, .None });
1842
+ }
1843
+
1844
+ foe.active.volatiles.LockOn = true;
1845
+ try options.log.activate(.{ battle.active(player), .LockOn });
1670
1846
  }
1671
1847
 
1672
1848
  const MAGNITUDE_POWER = [_]u8{ 10, 30, 50, 70, 90, 110, 150 };
1673
1849
 
1674
1850
  pub fn magnitude(battle: anytype, player: Player, state: *State, options: anytype) !void {
1675
- _ = .{ battle, player, state, options }; // TODO
1851
+ const num = try Rolls.magnitude(battle, player, options);
1852
+ try options.log.activate(.{ battle.active(player), .Magnitude, num });
1853
+ state.bp = MAGNITUDE_POWER[num - 4];
1676
1854
  }
1677
1855
 
1678
- pub fn meanLook(battle: anytype, player: Player, state: *State, options: anytype) !void {
1679
- _ = .{ battle, player, state, options }; // TODO
1856
+ pub fn meanLook(battle: anytype, player: Player, _: *State, options: anytype) !void {
1857
+ var foe = battle.foe(player);
1858
+
1859
+ if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
1860
+ if (!showdown) return options.log.fail(.{ battle.active(player.foe()), .None });
1861
+ try options.log.lastmiss(.{});
1862
+ return options.log.miss(.{battle.active(player)});
1863
+ } else if (foe.active.volatiles.Trapped) {
1864
+ return options.log.fail(.{ battle.active(player.foe()), .None });
1865
+ }
1866
+
1867
+ foe.active.volatiles.Trapped = true;
1680
1868
  }
1681
1869
 
1682
1870
  pub fn metronome(
@@ -1703,10 +1891,6 @@ pub const Effects = struct {
1703
1891
  _ = .{ battle, player, state, options }; // TODO
1704
1892
  }
1705
1893
 
1706
- pub fn mirrorCoat(battle: anytype, player: Player, state: *State, options: anytype) !void {
1707
- _ = .{ battle, player, state, options }; // TODO
1708
- }
1709
-
1710
1894
  pub fn mirrorMove(
1711
1895
  battle: anytype,
1712
1896
  player: Player,
@@ -1721,11 +1905,11 @@ pub const Effects = struct {
1721
1905
 
1722
1906
  const last = foe.lastMove(false);
1723
1907
 
1724
- if (last == .None) return options.log.fail(battle.active(player), .None);
1908
+ if (last == .None) return options.log.fail(.{ battle.active(player), .None });
1725
1909
  const knows = knows: for (side.active.moves) |m| {
1726
1910
  if (last == m.id) break :knows true;
1727
1911
  } else false;
1728
- if (knows) return options.log.fail(battle.active(player), .None);
1912
+ if (knows) return options.log.fail(.{ battle.active(player), .None });
1729
1913
 
1730
1914
  // if (wPlayerCharging) ???
1731
1915
  // wPlayerCharging = 1
@@ -1739,17 +1923,17 @@ pub const Effects = struct {
1739
1923
  pub fn mist(battle: anytype, player: Player, _: *State, options: anytype) !void {
1740
1924
  var side = battle.side(player);
1741
1925
 
1742
- if (side.active.volatiles.Mist) return options.log.fail(battle.active(player), .None);
1926
+ if (side.active.volatiles.Mist) return options.log.fail(.{ battle.active(player), .None });
1743
1927
  side.active.volatiles.Mist = true;
1744
1928
 
1745
- try options.log.start(battle.active(player), .Mist);
1929
+ try options.log.start(.{ battle.active(player), .Mist });
1746
1930
  }
1747
1931
 
1748
1932
  pub fn weatherHeal(battle: anytype, player: Player, _: *State, options: anytype) !void {
1749
1933
  var stored = battle.side(player).stored();
1750
1934
  const ident = battle.active(player);
1751
1935
 
1752
- if (stored.hp == stored.stats.hp) return options.log.fail(ident, .None);
1936
+ if (stored.hp == stored.stats.hp) return options.log.fail(.{ ident, .None });
1753
1937
 
1754
1938
  stored.hp = switch (battle.field.weather) {
1755
1939
  .Sun => stored.stats.hp,
@@ -1757,7 +1941,7 @@ pub const Effects = struct {
1757
1941
  else => @min(stored.stats.hp, stored.hp + (stored.stats.hp / 4)),
1758
1942
  };
1759
1943
 
1760
- try options.log.heal(ident, stored, Heal.None);
1944
+ try options.log.heal(.{ ident, stored, .None });
1761
1945
  }
1762
1946
 
1763
1947
  pub fn multiHit(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1770,17 +1954,17 @@ pub const Effects = struct {
1770
1954
  const foe_ident = battle.active(player.foe());
1771
1955
 
1772
1956
  if (foe_volatiles.Flying or foe_volatiles.Underground) {
1773
- if (!showdown) return options.log.fail(battle.active(player.foe()), .None);
1774
- try options.log.lastmiss();
1775
- return options.log.miss(battle.active(player));
1957
+ if (!showdown) return options.log.fail(.{ battle.active(player.foe()), .None });
1958
+ try options.log.lastmiss(.{});
1959
+ return options.log.miss(.{battle.active(player)});
1776
1960
  } else if (foe_volatiles.Substitute) {
1777
- return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
1961
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
1778
1962
  } else if (!Status.is(foe.stored().status, .SLP) or foe_volatiles.Nightmare) {
1779
- return options.log.fail(foe_ident, .None);
1963
+ return options.log.fail(.{ foe_ident, .None });
1780
1964
  }
1781
1965
 
1782
1966
  foe_volatiles.Nightmare = true;
1783
- try options.log.start(battle.active(player.foe()), .Nightmare);
1967
+ try options.log.start(.{ battle.active(player.foe()), .Nightmare });
1784
1968
  }
1785
1969
 
1786
1970
  pub const ohko = struct {
@@ -1804,9 +1988,9 @@ pub const Effects = struct {
1804
1988
  const foe_ident = battle.active(player.foe());
1805
1989
 
1806
1990
  state.damage = 0;
1807
- if (state.immune()) return options.log.immune(foe_ident, .None);
1991
+ if (state.immune()) return options.log.immune(.{ foe_ident, .None });
1808
1992
  const delta = side.stored().level -| foe.stored().level;
1809
- if (delta == 0) return options.log.immune(foe_ident, .OHKO);
1993
+ if (delta == 0) return options.log.immune(.{ foe_ident, .OHKO });
1810
1994
 
1811
1995
  state.accuracy = 2 * delta +| Gen12.percent(30);
1812
1996
  try checkHit(battle, player, state, options);
@@ -1822,37 +2006,39 @@ pub const Effects = struct {
1822
2006
  const ident = battle.active(player);
1823
2007
  const foe_ident = battle.active(player.foe());
1824
2008
 
1825
- if (state.miss) return options.log.fail(foe_ident, .None);
2009
+ if (state.miss) return options.log.fail(.{ foe_ident, .None });
1826
2010
  if (foe.active.volatiles.Substitute) {
1827
- return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
2011
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
1828
2012
  }
1829
2013
 
1830
2014
  const avg = @max((foe.stored().hp + side.stored().hp) / 2, 1);
1831
2015
  foe.stored().hp -= avg;
1832
2016
  side.stored().hp = avg;
1833
2017
 
1834
- try options.log.sethp(foe_ident, foe.stored(), .Silent);
1835
- try options.log.sethp(ident, side.stored(), .None);
2018
+ try options.log.sethp(.{ foe_ident, foe.stored(), .Silent });
2019
+ try options.log.sethp(.{ ident, side.stored(), .None });
1836
2020
  }
1837
2021
 
1838
2022
  pub fn paralyze(battle: anytype, player: Player, state: *State, options: anytype) !void {
1839
2023
  var foe = battle.foe(player);
1840
2024
  const foe_ident = battle.active(player.foe());
1841
2025
 
1842
- if (Status.is(foe.stored().status, .PAR)) return options.log.fail(foe_ident, .Paralysis);
1843
- if (state.immune()) return options.log.immune(foe_ident, .None);
1844
- if (Status.any(foe.stored().status)) return options.log.fail(foe_ident, .None);
2026
+ if (Status.is(foe.stored().status, .PAR)) {
2027
+ return options.log.fail(.{ foe_ident, .Paralysis });
2028
+ }
2029
+ if (state.immune()) return options.log.immune(.{ foe_ident, .None });
2030
+ if (Status.any(foe.stored().status)) return options.log.fail(.{ foe_ident, .None });
1845
2031
  if (state.miss) {
1846
- try options.log.lastmiss();
1847
- return options.log.miss(battle.active(player));
2032
+ try options.log.lastmiss(.{});
2033
+ return options.log.miss(.{battle.active(player)});
1848
2034
  } else if (foe.active.volatiles.Substitute) {
1849
- return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
2035
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
1850
2036
  }
1851
2037
 
1852
2038
  foe.stored().status = Status.init(.PAR);
1853
2039
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
1854
2040
 
1855
- try options.log.status(foe_ident, foe.stored().status, .None);
2041
+ try options.log.status(.{ foe_ident, foe.stored().status, .None });
1856
2042
  }
1857
2043
 
1858
2044
  pub fn paralyzeChance(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1867,14 +2053,15 @@ pub const Effects = struct {
1867
2053
 
1868
2054
  foe_stored.status = Status.init(.PAR);
1869
2055
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
1870
- try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
2056
+
2057
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
1871
2058
 
1872
2059
  // TODO: check for status berry
1873
2060
  }
1874
2061
 
1875
2062
  pub fn payDay(battle: anytype, player: Player, _: *State, options: anytype) !void {
1876
2063
  if (!showdown or !battle.foe(player).active.volatiles.Substitute) {
1877
- try options.log.fieldactivate(); // FIXME .PayDay
2064
+ try options.log.fieldactivate(.{}); // FIXME .PayDay
1878
2065
  }
1879
2066
  }
1880
2067
 
@@ -1883,17 +2070,17 @@ pub const Effects = struct {
1883
2070
  var foe = battle.foe(player);
1884
2071
 
1885
2072
  if (foe.active.volatiles.PerishSong) {
1886
- return options.log.fail(battle.active(player.foe()), .None);
2073
+ return options.log.fail(.{ battle.active(player.foe()), .None });
1887
2074
  } else if (!side.active.volatiles.PerishSong) {
1888
2075
  side.active.volatiles.PerishSong = true;
1889
2076
  side.active.volatiles.perish_song = 4;
1890
- try options.log.start(battle.active(player), .PerishSong3Silent);
2077
+ try options.log.start(.{ battle.active(player), .PerishSong3Silent });
1891
2078
  }
1892
2079
  foe.active.volatiles.PerishSong = true;
1893
2080
  foe.active.volatiles.perish_song = 4;
1894
- try options.log.start(battle.active(player.foe()), .PerishSong3Silent);
2081
+ try options.log.start(.{ battle.active(player.foe()), .PerishSong3Silent });
1895
2082
 
1896
- try options.log.fieldactivate(); // FIXME .PerishSong
2083
+ try options.log.fieldactivate(.{}); // FIXME .PerishSong
1897
2084
  }
1898
2085
 
1899
2086
  pub fn poison(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1903,24 +2090,24 @@ pub const Effects = struct {
1903
2090
 
1904
2091
  const toxic = state.move == .Toxic;
1905
2092
  if (state.immune() or foe.active.types.includes(.Poison)) {
1906
- return options.log.immune(foe_ident, .None);
2093
+ return options.log.immune(.{ foe_ident, .None });
1907
2094
  } else if (Status.any(foe_stored.status)) {
1908
2095
  // Pokémon Showdown considers Toxic to be a status even in Generation II and so
1909
2096
  // will not include a fail reason for Toxic vs. Poison or vice-versa...
1910
- return options.log.fail(foe_ident, if (Status.is(foe_stored.status, .PSN))
2097
+ return options.log.fail(.{ foe_ident, if (Status.is(foe_stored.status, .PSN))
1911
2098
  if (!showdown)
1912
- .Poison
2099
+ Fail.Poison
1913
2100
  else if (toxic == (foe_stored.status == Status.TOX))
1914
- if (toxic) .Toxic else .Poison
2101
+ if (toxic) Fail.Toxic else Fail.Poison
1915
2102
  else
1916
- .None
2103
+ Fail.None
1917
2104
  else
1918
- .None);
2105
+ Fail.None });
1919
2106
  } else if (foe.active.volatiles.Substitute) {
1920
- return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
2107
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
1921
2108
  } else if (state.miss) {
1922
- try options.log.lastmiss();
1923
- return options.log.miss(battle.active(player));
2109
+ try options.log.lastmiss(.{});
2110
+ return options.log.miss(.{battle.active(player)});
1924
2111
  }
1925
2112
 
1926
2113
  foe_stored.status = Status.init(.PSN);
@@ -1930,7 +2117,7 @@ pub const Effects = struct {
1930
2117
  foe.active.volatiles.toxic = 0;
1931
2118
  }
1932
2119
 
1933
- try options.log.status(foe_ident, foe_stored.status, .None);
2120
+ try options.log.status(.{ foe_ident, foe_stored.status, .None });
1934
2121
 
1935
2122
  // TODO: check for status berry
1936
2123
  }
@@ -1946,7 +2133,8 @@ pub const Effects = struct {
1946
2133
  if (foe.conditions.Safeguard) return;
1947
2134
 
1948
2135
  foe_stored.status = Status.init(.PSN);
1949
- try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
2136
+
2137
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
1950
2138
 
1951
2139
  // TODO: check for status berry
1952
2140
  }
@@ -1961,9 +2149,7 @@ pub const Effects = struct {
1961
2149
 
1962
2150
  const failed = !state.first or volatiles.Substitute or
1963
2151
  !try Rolls.protect(battle, player, volatiles.protect, options);
1964
- if (failed) {
1965
- return options.log.fail(battle.active(player), .None);
1966
- }
2152
+ if (failed) return options.log.fail(.{ battle.active(player), .None });
1967
2153
 
1968
2154
  volatiles.protect = @max(8, volatiles.protect + 1);
1969
2155
  if (state.move == .Endure) {
@@ -1971,7 +2157,7 @@ pub const Effects = struct {
1971
2157
  } else {
1972
2158
  volatiles.Protect = true;
1973
2159
  }
1974
- try options.log.singleturn(battle.active(player), state.move);
2160
+ try options.log.singleturn(.{ battle.active(player), state.move });
1975
2161
  }
1976
2162
 
1977
2163
  pub fn psychUp(battle: anytype, player: Player, _: *State, options: anytype) !void {
@@ -1980,11 +2166,11 @@ pub const Effects = struct {
1980
2166
 
1981
2167
  // Pokémon Showdown should fail when the enemy has no boosts/unboosts but doesn't
1982
2168
  if (!showdown and @as(u32, @bitCast(foe.active.boosts)) == 0) {
1983
- return options.log.fail(battle.active(player), .None);
2169
+ return options.log.fail(.{ battle.active(player), .None });
1984
2170
  }
1985
2171
 
1986
2172
  side.active.boosts = foe.active.boosts;
1987
- try options.log.copyboost(battle.active(player), battle.active(player.foe()));
2173
+ try options.log.copyboost(.{ battle.active(player), battle.active(player.foe()) });
1988
2174
  }
1989
2175
 
1990
2176
  pub fn pursuit(battle: anytype, player: Player, state: *State, _: anytype) !void {
@@ -1995,7 +2181,7 @@ pub const Effects = struct {
1995
2181
  pub const rage = struct {
1996
2182
  pub fn start(battle: anytype, player: Player, _: *State, options: anytype) !void {
1997
2183
  battle.side(player).active.volatiles.Rage = true;
1998
- try options.log.singlemove(battle.active(player), Move.Rage);
2184
+ try options.log.singlemove(.{ battle.active(player), Move.Rage });
1999
2185
  }
2000
2186
 
2001
2187
  pub fn damage(battle: anytype, player: Player, state: *State, _: anytype) !void {
@@ -2015,18 +2201,18 @@ pub const Effects = struct {
2015
2201
  var side = battle.side(player);
2016
2202
 
2017
2203
  if (side.active.volatiles.LeechSeed) {
2018
- try options.log.end(battle.active(player), .LeechSeedFrom);
2204
+ try options.log.end(.{ battle.active(player), .LeechSeedFrom });
2019
2205
  side.active.volatiles.LeechSeed = false;
2020
2206
  }
2021
2207
 
2022
2208
  if (side.conditions.Spikes) {
2023
- try options.log.spikesend(battle.active(player));
2209
+ try options.log.sideend(.{ player, protocol.Side.Spikes, battle.active(player) });
2024
2210
  side.conditions.Spikes = false;
2025
2211
  }
2026
2212
 
2027
2213
  if (side.active.volatiles.bind.duration > 0) {
2028
2214
  const reason = @intFromEnum(End.Bind) + side.active.volatiles.bind.reason - 1;
2029
- try options.log.end(battle.active(player), @enumFromInt(reason));
2215
+ try options.log.end(.{ battle.active(player), @as(End, @enumFromInt(reason)) });
2030
2216
  side.active.volatiles.bind = .{};
2031
2217
  }
2032
2218
  }
@@ -2034,18 +2220,14 @@ pub const Effects = struct {
2034
2220
  pub fn recoil(battle: anytype, player: Player, state: *State, options: anytype) !void {
2035
2221
  var side = battle.side(player);
2036
2222
  var stored = side.stored();
2223
+ const ident = battle.active(player);
2037
2224
 
2038
2225
  assert(state.damage > 0);
2039
2226
 
2040
2227
  const damage: i16 = @intCast(@max(state.damage / 4, 1));
2041
2228
  stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
2042
2229
 
2043
- try options.log.damageOf(
2044
- battle.active(player),
2045
- stored,
2046
- .RecoilOf,
2047
- battle.active(player.foe()),
2048
- );
2230
+ try options.log.damage(.{ ident, stored, .RecoilOf, battle.active(player.foe()) });
2049
2231
  }
2050
2232
 
2051
2233
  pub fn reversal(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -2108,7 +2290,7 @@ pub const Effects = struct {
2108
2290
 
2109
2291
  pub fn sandstorm(battle: anytype, player: Player, _: *State, options: anytype) !void {
2110
2292
  if (battle.field.weather != .Sandstorm) return weather(battle, .Sandstorm, options);
2111
- return options.log.fail(battle.active(player), .None);
2293
+ return options.log.fail(.{ battle.active(player), .None });
2112
2294
  }
2113
2295
 
2114
2296
  pub fn safeguard(battle: anytype, player: Player, _: *State, options: anytype) !void {
@@ -2132,11 +2314,13 @@ pub const Effects = struct {
2132
2314
  ) !void {
2133
2315
  var side = battle.side(player);
2134
2316
 
2135
- if (@field(side.conditions, key)) return options.log.fail(battle.active(player), .None);
2317
+ if (@field(side.conditions, key)) {
2318
+ return options.log.fail(.{ battle.active(player), .None });
2319
+ }
2136
2320
 
2137
2321
  @field(side.conditions, key) = true;
2138
2322
  @field(side.conditions, value) = 5;
2139
- try options.log.sidestart(player, @field(protocol.Side, key));
2323
+ try options.log.sidestart(.{ player, @field(protocol.Side, key) });
2140
2324
  }
2141
2325
 
2142
2326
  pub fn sketch(battle: anytype, player: Player, _: *State, options: anytype) !void {
@@ -2147,9 +2331,9 @@ pub const Effects = struct {
2147
2331
 
2148
2332
  if (showdown) {
2149
2333
  // yes, actually "Splash". '|nothing' gets blindly mapped to '|-activate||move:Splash'
2150
- try options.log.activate(battle.active(player), .Splash);
2334
+ try options.log.activate(.{ battle.active(player), .Splash });
2151
2335
  } else {
2152
- try options.log.fail(battle.active(player), .None);
2336
+ try options.log.fail(.{ battle.active(player), .None });
2153
2337
  }
2154
2338
  }
2155
2339
 
@@ -2159,14 +2343,14 @@ pub const Effects = struct {
2159
2343
  const foe_ident = battle.active(player.foe());
2160
2344
 
2161
2345
  if (Status.any(foe_stored.status)) {
2162
- return options.log.fail(
2346
+ return options.log.fail(.{
2163
2347
  foe_ident,
2164
- if (Status.is(foe_stored.status, .SLP)) .Sleep else .None,
2165
- );
2348
+ if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
2349
+ });
2166
2350
  }
2167
2351
 
2168
2352
  if (foe.active.volatiles.Substitute) {
2169
- return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
2353
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
2170
2354
  }
2171
2355
 
2172
2356
  // Sleep Clause Mod
@@ -2177,22 +2361,21 @@ pub const Effects = struct {
2177
2361
  }
2178
2362
 
2179
2363
  foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2180
- try options.log.statusFrom(foe_ident, foe_stored.status, state.move);
2364
+ try options.log.status(.{ foe_ident, foe_stored.status, .From, state.move });
2181
2365
  }
2182
2366
 
2183
2367
  pub fn sleepTalk(battle: anytype, player: Player, state: *State, options: anytype) !void {
2184
2368
  const side = battle.side(player);
2185
2369
 
2186
2370
  if (!Status.is(side.stored().status, .SLP)) {
2187
- if (!showdown) try options.log.fail(battle.active(player), .None);
2188
- return;
2371
+ return if (!showdown) try options.log.fail(.{ battle.active(player), .None });
2189
2372
  }
2190
2373
 
2191
2374
  var n: u3 = 0;
2192
2375
  for (side.active.moves, 0..) |m, i| {
2193
2376
  if (state.mslot - 1 != i and Move.get(m.id).extra.sleep_talk) n += 1;
2194
2377
  }
2195
- if (n == 0) return options.log.fail(battle.active(player), .None);
2378
+ if (n == 0) return options.log.fail(.{ battle.active(player), .None });
2196
2379
 
2197
2380
  _ = try Rolls.sleepTalk(battle, player, n, state.mslot, options); // TODO
2198
2381
  }
@@ -2201,23 +2384,23 @@ pub const Effects = struct {
2201
2384
  if (Status.is(battle.side(player).stored().status, .SLP)) return true;
2202
2385
  state.damage = 0;
2203
2386
  state.miss = true; // TODO is this necessary?
2204
- if (!showdown) try options.log.fail(battle.active(player), .None);
2387
+ if (!showdown) try options.log.fail(.{ battle.active(player), .None });
2205
2388
  return false;
2206
2389
  }
2207
2390
 
2208
2391
  pub fn spikes(battle: anytype, player: Player, _: *State, options: anytype) !void {
2209
2392
  var foe = battle.foe(player);
2210
- if (foe.conditions.Spikes) return options.log.fail(battle.active(player), .None);
2393
+ if (foe.conditions.Spikes) return options.log.fail(.{ battle.active(player), .None });
2211
2394
 
2212
2395
  foe.conditions.Spikes = true;
2213
- try options.log.sidestart(player.foe(), .Spikes);
2396
+ try options.log.sidestart(.{ player.foe(), protocol.Side.Spikes });
2214
2397
  }
2215
2398
 
2216
2399
  pub fn spite(battle: anytype, player: Player, state: *State, options: anytype) !void {
2217
2400
  _ = .{ battle, player, state, options }; // TODO
2218
2401
  // var foe = battle.foe(player);
2219
2402
 
2220
- // if (state.miss) return options.log.fail(battle.active(player), .None);
2403
+ // if (state.miss) return options.log.fail(.{ battle.active(player), .None });
2221
2404
 
2222
2405
  // const last = foe.lastMove(false);
2223
2406
  // if (showdown) {
@@ -2228,28 +2411,28 @@ pub const Effects = struct {
2228
2411
  }
2229
2412
 
2230
2413
  pub fn splash(battle: anytype, player: Player, _: *State, options: anytype) !void {
2231
- try options.log.activate(battle.active(player), .Splash);
2414
+ try options.log.activate(.{ battle.active(player), .Splash });
2232
2415
  }
2233
2416
 
2234
2417
  pub fn substitute(battle: anytype, player: Player, _: *State, options: anytype) !void {
2235
2418
  var side = battle.side(player);
2236
2419
  if (side.active.volatiles.Substitute) {
2237
- return options.log.fail(battle.active(player), .Substitute);
2420
+ return options.log.fail(.{ battle.active(player), .Substitute });
2238
2421
  }
2239
2422
 
2240
2423
  assert(side.stored().stats.hp <= 1023);
2241
2424
  const hp: u8 = @intCast(side.stored().stats.hp / 4);
2242
2425
 
2243
2426
  if (hp == 0 or side.stored().hp <= hp) {
2244
- return options.log.fail(battle.active(player), .Weak);
2427
+ return options.log.fail(.{ battle.active(player), .Weak });
2245
2428
  }
2246
2429
 
2247
2430
  side.stored().hp -= hp;
2248
2431
  side.active.volatiles.substitute = hp;
2249
2432
  side.active.volatiles.Substitute = true;
2250
2433
  side.active.volatiles.bind = .{};
2251
- try options.log.start(battle.active(player), .Substitute);
2252
- try options.log.damage(battle.active(player), side.stored(), .None);
2434
+ try options.log.start(.{ battle.active(player), .Substitute });
2435
+ try options.log.damage(.{ battle.active(player), side.stored(), .None });
2253
2436
  }
2254
2437
 
2255
2438
  pub fn sunnyDay(battle: anytype, _: Player, _: *State, options: anytype) !void {
@@ -2259,14 +2442,14 @@ pub const Effects = struct {
2259
2442
  fn weather(battle: anytype, w: Weather, options: anytype) !void {
2260
2443
  battle.field.weather = w;
2261
2444
  battle.field.weather_duration = 5;
2262
- try options.log.weather(w, .None);
2445
+ try options.log.weather(.{ w, protocol.Weather.None });
2263
2446
  }
2264
2447
 
2265
2448
  pub fn teleport(battle: anytype, player: Player, _: *State, options: anytype) !void {
2266
2449
  if (showdown) return;
2267
2450
 
2268
- try options.log.fail(battle.active(player), .None);
2269
- try options.log.laststill();
2451
+ try options.log.fail(.{ battle.active(player), .None });
2452
+ try options.log.laststill(.{});
2270
2453
  }
2271
2454
 
2272
2455
  pub fn thief(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -2281,11 +2464,11 @@ pub const Effects = struct {
2281
2464
  side.stored().item = foe.stored().item;
2282
2465
  foe.stored().item = .None;
2283
2466
 
2284
- try options.log.item(
2467
+ try options.log.item(.{
2285
2468
  battle.active(player),
2286
2469
  side.stored().item,
2287
2470
  battle.active(player.foe()),
2288
- );
2471
+ });
2289
2472
  }
2290
2473
 
2291
2474
  pub fn thrashing(battle: anytype, player: Player, _: *State, options: anytype) !void {
@@ -2307,11 +2490,11 @@ pub const Effects = struct {
2307
2490
  side.active.volatiles.dirty = false;
2308
2491
 
2309
2492
  if (foe.active.volatiles.Transform) {
2310
- return options.log.fail(battle.active(player), .None);
2493
+ return options.log.fail(.{ battle.active(player), .None });
2311
2494
  } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2312
- if (!showdown) return options.log.fail(battle.active(player), .None);
2313
- try options.log.lastmiss();
2314
- return options.log.miss(battle.active(player));
2495
+ if (!showdown) return options.log.fail(.{ battle.active(player), .None });
2496
+ try options.log.lastmiss(.{});
2497
+ return options.log.miss(.{battle.active(player)});
2315
2498
  }
2316
2499
 
2317
2500
  side.active.volatiles.Transform = true;
@@ -2333,7 +2516,7 @@ pub const Effects = struct {
2333
2516
  side.active.moves[i].pp = if (m.id != .None) if (m.id == .Sketch) 1 else 5 else 0;
2334
2517
  }
2335
2518
 
2336
- try options.log.transform(battle.active(player), foe_ident);
2519
+ try options.log.transform(.{ battle.active(player), foe_ident });
2337
2520
  }
2338
2521
 
2339
2522
  pub fn triAttack(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -2352,8 +2535,7 @@ pub const Effects = struct {
2352
2535
  _ = .{ battle, player, state, options }; // TODO
2353
2536
  }
2354
2537
 
2355
- const ALL_STAT_UP_STATS = [_][]const u8{ "atk", "def", "spe", "spa", "spd" };
2356
- const ALL_STAT_UP_REASONS =
2538
+ const STAT_UP_REASONS =
2357
2539
  [_]Boost{ .Attack, .Defense, .Speed, .SpecialAttack, .SpecialDefense };
2358
2540
 
2359
2541
  pub fn allStatUpChance(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -2363,20 +2545,15 @@ pub const Effects = struct {
2363
2545
  var boosts = &side.active.boosts;
2364
2546
 
2365
2547
  if (!state.proc) return;
2366
- inline for (ALL_STAT_UP_STATS, ALL_STAT_UP_REASONS) |s, r| {
2548
+ inline for (STATS, STAT_UP_REASONS) |s, r| {
2367
2549
  assert(@field(boosts, s) >= -6 and @field(boosts, s) <= 6);
2368
2550
  if (@field(boosts, s) == 6 or @field(stats, s) == MAX_STAT_VALUE) {
2369
- try options.log.boost(battle.active(player), r, 0);
2551
+ try options.log.boost(.{ battle.active(player), r, 0 });
2370
2552
  } else {
2371
- @field(boosts, s) = @intCast(@min(6, @as(i8, @field(boosts, s)) + 1));
2372
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, @field(boosts, s)) + 6))];
2373
- const stat = @field(unmodifiedStats(battle, side), s);
2374
- @field(stats, s) = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2375
- try options.log.boost(battle.active(player), r, 1);
2553
+ try boostStat(battle, side, stats, boosts, s, 1);
2554
+ try options.log.boost(.{ battle.active(player), r, 1 });
2376
2555
  }
2377
2556
  }
2378
-
2379
- statusModify(side.stored().status, stats);
2380
2557
  }
2381
2558
 
2382
2559
  pub fn boost(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -2384,7 +2561,7 @@ pub const Effects = struct {
2384
2561
  var side = battle.side(player);
2385
2562
  const ident = battle.active(player);
2386
2563
 
2387
- if (state.miss) return log.fail(ident, .None);
2564
+ if (state.miss) return log.fail(.{ ident, .None });
2388
2565
 
2389
2566
  var stats = &side.active.stats;
2390
2567
  var boosts = &side.active.boosts;
@@ -2393,63 +2570,56 @@ pub const Effects = struct {
2393
2570
  switch (move.effect) {
2394
2571
  .AttackUp1, .AttackUp2, .Rage, .Swagger => {
2395
2572
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2396
- if (boosts.atk == 6 or stats.atk == MAX_STAT_VALUE) return log.fail(ident, .None);
2573
+ if (boosts.atk == 6 or stats.atk == MAX_STAT_VALUE) {
2574
+ return log.fail(.{ ident, .None });
2575
+ }
2397
2576
  const n: u2 = if (move.effect == .AttackUp2 or move.effect == .Swagger) 2 else 1;
2398
- boosts.atk = @as(i4, @intCast(@min(6, @as(i8, boosts.atk) + n)));
2577
+ try boostStat(battle, side, stats, boosts, "atk", n);
2399
2578
  const reason = if (showdown and move.effect == .Rage) Boost.Rage else Boost.Attack;
2400
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2401
- const stat = unmodifiedStats(battle, side).atk;
2402
- stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2403
- try log.boost(ident, reason, n);
2579
+ try log.boost(.{ ident, reason, n });
2404
2580
  },
2405
2581
  .DefenseUp1, .DefenseUp2, .SkullBash => {
2406
2582
  assert(boosts.def >= -6 and boosts.def <= 6);
2407
- if (boosts.def == 6 or stats.def == MAX_STAT_VALUE) return log.fail(ident, .None);
2583
+ if (boosts.def == 6 or stats.def == MAX_STAT_VALUE) {
2584
+ return log.fail(.{ ident, .None });
2585
+ }
2408
2586
  const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
2409
- boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
2410
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2411
- const stat = unmodifiedStats(battle, side).def;
2412
- stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2413
- try log.boost(ident, .Defense, n);
2587
+ try boostStat(battle, side, stats, boosts, "def", n);
2588
+ try log.boost(.{ ident, .Defense, n });
2414
2589
  },
2415
2590
  .SpeedUp2 => {
2416
2591
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2417
- if (boosts.spe == 6 or stats.spe == MAX_STAT_VALUE) return log.fail(ident, .None);
2418
- boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
2419
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2420
- const stat = unmodifiedStats(battle, side).spe;
2421
- stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2422
- try log.boost(ident, .Speed, 2);
2592
+ if (boosts.spe == 6 or stats.spe == MAX_STAT_VALUE) {
2593
+ return log.fail(.{ ident, .None });
2594
+ }
2595
+ try boostStat(battle, side, stats, boosts, "spe", 2);
2596
+ try log.boost(.{ ident, .Speed, 2 });
2423
2597
  },
2424
2598
  .SpAtkUp1 => {
2425
2599
  assert(boosts.spa >= -6 and boosts.spa <= 6);
2426
- if (boosts.spa == 6 or stats.spa == MAX_STAT_VALUE) return log.fail(ident, .None);
2427
- boosts.spa = @intCast(@min(6, @as(i8, boosts.spa) + 1));
2428
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.spa) + 6))];
2429
- const stat = unmodifiedStats(battle, side).spa;
2430
- stats.spa = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2431
- try log.boost(ident, .SpecialAttack, 1);
2600
+ if (boosts.spa == 6 or stats.spa == MAX_STAT_VALUE) {
2601
+ return log.fail(.{ ident, .None });
2602
+ }
2603
+ try boostStat(battle, side, stats, boosts, "spa", 1);
2604
+ try log.boost(.{ ident, .SpecialAttack, 1 });
2432
2605
  },
2433
2606
  .SpDefUp2 => {
2434
2607
  assert(boosts.spd >= -6 and boosts.spd <= 6);
2435
- if (boosts.spd == 6 or stats.spd == MAX_STAT_VALUE) return log.fail(ident, .None);
2436
- boosts.spd = @intCast(@min(6, @as(i8, boosts.spd) + 2));
2437
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.spd) + 6))];
2438
- const stat = unmodifiedStats(battle, side).spd;
2439
- stats.spd = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2440
- try log.boost(ident, .SpecialDefense, 2);
2608
+ if (boosts.spd == 6 or stats.spd == MAX_STAT_VALUE) {
2609
+ return log.fail(.{ ident, .None });
2610
+ }
2611
+ try boostStat(battle, side, stats, boosts, "spd", 2);
2612
+ try log.boost(.{ ident, .SpecialDefense, 2 });
2441
2613
  },
2442
2614
  .EvasionUp1 => {
2443
2615
  assert(boosts.evasion >= -6 and boosts.evasion <= 6);
2444
- if (boosts.evasion == 6) return log.fail(ident, .None);
2616
+ if (boosts.evasion == 6) return log.fail(.{ ident, .None });
2445
2617
  boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
2446
- try log.boost(ident, .Evasion, 1);
2618
+ try log.boost(.{ ident, .Evasion, 1 });
2447
2619
  if (state.move == .Minimize) side.active.volatiles.minimized = true;
2448
2620
  },
2449
2621
  else => unreachable,
2450
2622
  }
2451
-
2452
- statusModify(side.stored().status, stats);
2453
2623
  }
2454
2624
 
2455
2625
  pub fn unboost(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -2458,8 +2628,8 @@ pub const Effects = struct {
2458
2628
  const foe_ident = battle.active(player.foe());
2459
2629
 
2460
2630
  if (foe.active.volatiles.Mist) {
2461
- try log.activate(foe_ident, .Mist);
2462
- return log.fail(foe_ident, .None);
2631
+ try log.activate(.{ foe_ident, .Mist });
2632
+ return log.fail(.{ foe_ident, .None });
2463
2633
  }
2464
2634
 
2465
2635
  const fail = foe.active.volatiles.Substitute or state.miss or !state.proc;
@@ -2468,87 +2638,76 @@ pub const Effects = struct {
2468
2638
  var boosts = &foe.active.boosts;
2469
2639
 
2470
2640
  const move = Move.get(state.move);
2641
+ const none: Fail = .None;
2471
2642
  switch (move.effect) {
2472
2643
  .AttackDown1, .AttackDown2, .AttackDownChance => {
2473
2644
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2474
2645
  if (fail or boosts.atk == -6) {
2475
- return if (move.effect != .AttackDownChance) try log.fail(foe_ident, .None);
2646
+ return if (move.effect != .AttackDownChance) try log.fail(.{ foe_ident, none });
2476
2647
  } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2477
- try options.log.lastmiss();
2478
- return options.log.miss(battle.active(player));
2648
+ try options.log.lastmiss(.{});
2649
+ return options.log.miss(.{battle.active(player)});
2479
2650
  } else if (stats.atk == 1) {
2480
- return if (move.effect != .AttackDownChance) try log.fail(foe_ident, .None);
2651
+ return if (move.effect != .AttackDownChance) try log.fail(.{ foe_ident, none });
2481
2652
  }
2482
2653
  const n: u2 = if (move.effect == .AttackDown2) 2 else 1;
2483
- boosts.atk = @intCast(@max(-6, @as(i8, boosts.atk) - n));
2484
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2485
- const stat = unmodifiedStats(battle, foe).atk;
2486
- stats.atk = @max(1, stat * mod[0] / mod[1]);
2487
- try log.boost(foe_ident, .Attack, -@as(i8, n));
2654
+ try unboostStat(battle, foe, stats, boosts, "atk", n);
2655
+ try log.boost(.{ foe_ident, .Attack, -@as(i8, n) });
2488
2656
  },
2489
2657
  .DefenseDown1, .DefenseDown2, .DefenseDownChance => {
2490
2658
  assert(boosts.def >= -6 and boosts.def <= 6);
2491
2659
  if (fail or boosts.def == -6) {
2492
- return if (move.effect != .DefenseDownChance) try log.fail(foe_ident, .None);
2660
+ if (move.effect != .DefenseDownChance) try log.fail(.{ foe_ident, none });
2661
+ return;
2493
2662
  } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2494
- try options.log.lastmiss();
2495
- return options.log.miss(battle.active(player));
2663
+ try options.log.lastmiss(.{});
2664
+ return options.log.miss(.{battle.active(player)});
2496
2665
  } else if (stats.def == 1) {
2497
- return if (move.effect != .DefenseDownChance) try log.fail(foe_ident, .None);
2666
+ if (move.effect != .DefenseDownChance) try log.fail(.{ foe_ident, none });
2667
+ return;
2498
2668
  }
2499
2669
  const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
2500
- boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
2501
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2502
- const stat = unmodifiedStats(battle, foe).def;
2503
- stats.def = @max(1, stat * mod[0] / mod[1]);
2504
- try log.boost(foe_ident, .Defense, -@as(i8, n));
2670
+ try unboostStat(battle, foe, stats, boosts, "def", n);
2671
+ try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
2505
2672
  },
2506
2673
  .SpeedDown1, .SpeedDown2, .SpeedDownChance => {
2507
2674
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2508
2675
  if (fail or boosts.spe == -6) {
2509
- return if (move.effect != .SpeedDownChance) try log.fail(foe_ident, .None);
2676
+ return if (move.effect != .SpeedDownChance) try log.fail(.{ foe_ident, none });
2510
2677
  } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2511
- try options.log.lastmiss();
2512
- return options.log.miss(battle.active(player));
2678
+ try options.log.lastmiss(.{});
2679
+ return options.log.miss(.{battle.active(player)});
2513
2680
  } else if (stats.spe == 1) {
2514
- return if (move.effect != .SpeedDownChance) try log.fail(foe_ident, .None);
2681
+ return if (move.effect != .SpeedDownChance) try log.fail(.{ foe_ident, none });
2515
2682
  }
2516
2683
  const n: u2 = if (move.effect == .SpeedDown2) 2 else 1;
2517
- boosts.spe = @intCast(@max(-6, @as(i8, boosts.spe) - 1));
2518
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2519
- const stat = unmodifiedStats(battle, foe).spe;
2520
- stats.spe = @max(1, stat * mod[0] / mod[1]);
2521
- try log.boost(foe_ident, .Speed, -@as(i8, n));
2522
- assert(boosts.spe >= -6);
2684
+ try unboostStat(battle, foe, stats, boosts, "spe", n);
2685
+ try log.boost(.{ foe_ident, .Speed, -@as(i8, n) });
2523
2686
  },
2524
2687
  .SpDefDownChance => {
2525
2688
  assert(boosts.spd >= -6 and boosts.spd <= 6);
2526
2689
  if (fail or boosts.spd == -6) return;
2527
2690
  if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2528
- try options.log.lastmiss();
2529
- return options.log.miss(battle.active(player));
2691
+ try options.log.lastmiss(.{});
2692
+ return options.log.miss(.{battle.active(player)});
2530
2693
  } else if (stats.spd == 1) return;
2531
- boosts.spd = @intCast(@max(-6, @as(i8, boosts.spd) - 1));
2532
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.spd) + 6))];
2533
- const stat = unmodifiedStats(battle, foe).spd;
2534
- stats.spd = @max(1, stat * mod[0] / mod[1]);
2535
- try log.boost(foe_ident, .SpecialDefense, -1);
2694
+ try unboostStat(battle, foe, stats, boosts, "spd", 1);
2695
+ try log.boost(.{ foe_ident, .SpecialDefense, -1 });
2536
2696
  },
2537
2697
  .AccuracyDown1, .AccuracyDownChance => {
2538
2698
  assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
2539
2699
  if (fail or boosts.accuracy == -6) {
2540
- return if (move.effect != .AccuracyDownChance) try log.fail(foe_ident, .None);
2700
+ if (move.effect != .AccuracyDownChance) try log.fail(.{ foe_ident, none });
2701
+ return;
2541
2702
  } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2542
- try options.log.lastmiss();
2543
- return options.log.miss(battle.active(player));
2703
+ try options.log.lastmiss(.{});
2704
+ return options.log.miss(.{battle.active(player)});
2544
2705
  }
2545
2706
  boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
2546
- try log.boost(foe_ident, .Accuracy, -1);
2707
+ try log.boost(.{ foe_ident, .Accuracy, -1 });
2547
2708
  },
2548
2709
  else => unreachable,
2549
2710
  }
2550
-
2551
- statusModify(foe.stored().status, stats);
2552
2711
  }
2553
2712
  };
2554
2713
 
@@ -2558,6 +2717,15 @@ fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
2558
2717
  return &battle.side(id.player).pokemon[id.id - 1].stats;
2559
2718
  }
2560
2719
 
2720
+ fn recomputeStats(battle: anytype, side: *Side) void {
2721
+ var stats = unmodifiedStats(battle, side);
2722
+ inline for (STATS) |s| {
2723
+ var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, @field(side.active.boosts, s)) + 6))];
2724
+ const val = @field(stats, s) * mod[0] / mod[1];
2725
+ @field(side.active.stats, s) = @max(1, @min(MAX_STAT_VALUE, val));
2726
+ }
2727
+ }
2728
+
2561
2729
  fn statusModify(status: u8, stats: *Stats(u16)) void {
2562
2730
  if (Status.is(status, .PAR)) {
2563
2731
  stats.spe = @max(stats.spe / 4, 1);
@@ -2566,6 +2734,52 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
2566
2734
  }
2567
2735
  }
2568
2736
 
2737
+ fn boostStat(
2738
+ battle: anytype,
2739
+ side: *Side,
2740
+ stats: *Stats(u16),
2741
+ boosts: *Boosts,
2742
+ comptime s: []const u8,
2743
+ n: u2,
2744
+ ) !void {
2745
+ @field(boosts, s) = @intCast(@min(6, @as(i8, @field(boosts, s)) + n));
2746
+ var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, @field(boosts, s)) + 6))];
2747
+ const stat = @field(unmodifiedStats(battle, side), s);
2748
+ @field(stats, s) = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2749
+ if (comptime std.mem.eql(u8, s, "atk")) {
2750
+ if (Status.is(side.stored().status, .BRN)) {
2751
+ @field(stats, s) = @max(@field(stats, s) / 2, 1);
2752
+ }
2753
+ } else if (comptime std.mem.eql(u8, s, "spe")) {
2754
+ if (Status.is(side.stored().status, .PAR)) {
2755
+ @field(stats, s) = @max(@field(stats, s) / 4, 1);
2756
+ }
2757
+ }
2758
+ }
2759
+
2760
+ fn unboostStat(
2761
+ battle: anytype,
2762
+ side: *Side,
2763
+ stats: *Stats(u16),
2764
+ boosts: *Boosts,
2765
+ comptime s: []const u8,
2766
+ n: u2,
2767
+ ) !void {
2768
+ @field(boosts, s) = @intCast(@max(-6, @as(i8, @field(boosts, s)) - n));
2769
+ var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, @field(boosts, s)) + 6))];
2770
+ const stat = @field(unmodifiedStats(battle, side), s);
2771
+ @field(stats, s) = @max(1, stat * mod[0] / mod[1]);
2772
+ if (comptime std.mem.eql(u8, s, "atk")) {
2773
+ if (Status.is(side.stored().status, .BRN)) {
2774
+ @field(stats, s) = @max(@field(stats, s) / 2, 1);
2775
+ }
2776
+ } else if (comptime std.mem.eql(u8, s, "spe")) {
2777
+ if (Status.is(side.stored().status, .PAR)) {
2778
+ @field(stats, s) = @max(@field(stats, s) / 4, 1);
2779
+ }
2780
+ }
2781
+ }
2782
+
2569
2783
  fn isForced(active: anytype) bool {
2570
2784
  return active.volatiles.Recharging or active.volatiles.Rage or
2571
2785
  active.volatiles.Thrashing or active.volatiles.Charging or
@@ -2755,7 +2969,7 @@ pub const Rolls = struct {
2755
2969
  }
2756
2970
 
2757
2971
  fn magnitude(battle: anytype, player: Player, options: anytype) !u8 {
2758
- const num = if (options.calc.overridden(player, .magnitude)) |val|
2972
+ const num: u8 = if (options.calc.overridden(player, .magnitude)) |val|
2759
2973
  val + 3
2760
2974
  else if (showdown) num: {
2761
2975
  const r = battle.rng.range(u8, 0, 100);
@@ -3030,7 +3244,7 @@ pub const Rolls = struct {
3030
3244
 
3031
3245
  const move: Move = overridden orelse move: {
3032
3246
  if (showdown) {
3033
- var r = battle.rng.range(u8, 0, @intCast(Move.METRONOME.len - n));
3247
+ var r = battle.rng.range(u8, 0, @as(u8, @intCast(Move.METRONOME.len - n)));
3034
3248
  for (moves) |m| {
3035
3249
  if (m.id == .None) break;
3036
3250
  if (@intFromEnum(m.id) > @intFromEnum(Move.METRONOME[r])) break;
@@ -3273,7 +3487,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3273
3487
  }
3274
3488
 
3275
3489
  var slot: u4 = 2;
3276
- while (!active.volatiles.trapped and slot <= 6) : (slot += 1) {
3490
+ while (!active.volatiles.Trapped and slot <= 6) : (slot += 1) {
3277
3491
  const id = side.order[slot - 1];
3278
3492
  if (id == 0 or side.pokemon[id - 1].hp == 0) continue;
3279
3493
  out[n] = .{ .type = .Switch, .data = slot };