@pkmn/engine 0.1.0-dev.900756a9 → 0.1.0-dev.9257876b

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
 
@@ -140,11 +140,11 @@ fn start(battle: anytype, options: anytype) !Result {
140
140
  const p1 = battle.side(.P1);
141
141
  const p2 = battle.side(.P2);
142
142
 
143
- var p1_slot = findFirstAlive(p1);
143
+ const p1_slot = findFirstAlive(p1);
144
144
  assert(!showdown or p1_slot == 1);
145
145
  if (p1_slot == 0) return if (findFirstAlive(p2) == 0) Result.Tie else Result.Lose;
146
146
 
147
- var p2_slot = findFirstAlive(p2);
147
+ const p2_slot = findFirstAlive(p2);
148
148
  assert(!showdown or p2_slot == 1);
149
149
  if (p2_slot == 0) return Result.Win;
150
150
 
@@ -252,15 +252,15 @@ fn switchIn(battle: anytype, player: Player, slot: u8, kind: SwitchIn, options:
252
252
  statusModify(incoming.status, &active.stats);
253
253
 
254
254
  if (kind == .Drag) {
255
- try options.log.drag(battle.active(player), incoming);
255
+ try options.log.drag(.{ battle.active(player), incoming });
256
256
  } else {
257
- try options.log.switched(battle.active(player), incoming);
257
+ try options.log.switched(.{ battle.active(player), incoming });
258
258
  }
259
259
  options.chance.switched(player, side.pokemon[0].position, out);
260
260
 
261
261
  if (side.conditions.Spikes and !active.types.includes(.Flying)) {
262
262
  incoming.hp -|= @max(incoming.stats.hp / 8, 1);
263
- try options.log.damage(battle.active(player), incoming, .Spikes);
263
+ try options.log.damage(.{ battle.active(player), incoming, .Spikes });
264
264
  }
265
265
  }
266
266
 
@@ -403,7 +403,9 @@ fn executeMove(
403
403
 
404
404
  assert(choice.type == .Move);
405
405
  var mslot: u4 = @intCast(choice.data);
406
+ _ = &mslot;
406
407
  var move = Move.Pound; // TODO
408
+ _ = &move;
407
409
 
408
410
  if (!try beforeMove(battle, player, move, options)) return null;
409
411
 
@@ -433,7 +435,7 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
433
435
  if (volatiles.Recharging) {
434
436
  volatiles.Recharging = false;
435
437
  resetCant(volatiles);
436
- try log.cant(ident, .Recharge);
438
+ try log.cant(.{ ident, .Recharge });
437
439
  return false;
438
440
  }
439
441
 
@@ -452,12 +454,12 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
452
454
  }
453
455
 
454
456
  if (duration == 0) {
455
- try log.curestatus(ident, before, .Message);
457
+ try log.curestatus(.{ ident, before, .Message });
456
458
  stored.status = 0; // clears EXT if present
457
459
  resetCant(volatiles);
458
460
  volatiles.Nightmare = false;
459
461
  } else {
460
- try log.cant(ident, .Sleep);
462
+ try log.cant(.{ ident, .Sleep });
461
463
  if (move == .Snore or move == .SleepTalk) break :slp;
462
464
  resetCant(volatiles);
463
465
  return false;
@@ -466,7 +468,7 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
466
468
 
467
469
  if (Status.is(stored.status, .FRZ) and !(move == .FlameWheel or move == .SacredFire)) {
468
470
  resetCant(volatiles);
469
- try log.cant(ident, .Freeze);
471
+ try log.cant(.{ ident, .Freeze });
470
472
  return false;
471
473
  }
472
474
 
@@ -476,7 +478,7 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
476
478
  // if (!showdown) volatiles.Flinch = false;
477
479
  volatiles.Flinch = false;
478
480
  resetCant(volatiles);
479
- try log.cant(ident, .Flinch);
481
+ try log.cant(.{ ident, .Flinch });
480
482
  return false;
481
483
  }
482
484
 
@@ -490,7 +492,7 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
490
492
 
491
493
  if (volatiles.disable.duration == 0) {
492
494
  volatiles.disable.move = 0;
493
- try log.end(ident, .Disable);
495
+ try log.end(.{ ident, .Disable });
494
496
  }
495
497
  }
496
498
 
@@ -506,9 +508,9 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
506
508
 
507
509
  if (volatiles.confusion == 0) {
508
510
  volatiles.Confusion = false;
509
- try log.end(ident, .Confusion);
511
+ try log.end(.{ ident, .Confusion });
510
512
  } else {
511
- try log.activate(ident, .Confusion);
513
+ try log.activate(.{ ident, .Confusion });
512
514
 
513
515
  if (try Rolls.confused(battle, player, options)) {
514
516
  volatiles.BeatUp = false;
@@ -523,11 +525,11 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
523
525
  }
524
526
 
525
527
  if (volatiles.Attract) {
526
- try log.activate(ident, .Attract);
528
+ try log.activate(.{ ident, .Attract });
527
529
 
528
530
  if (try Rolls.attract(battle, player, options)) {
529
531
  resetCant(volatiles);
530
- try log.cant(ident, .Attract);
532
+ try log.cant(.{ ident, .Attract });
531
533
  return false;
532
534
  }
533
535
  }
@@ -538,14 +540,14 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
538
540
  if (m != .None and m == move) {
539
541
  side.active.volatiles.Charging = false;
540
542
  resetCant(volatiles);
541
- try options.log.disabled(ident, move);
543
+ try options.log.cant(.{ ident, .Disable, move });
542
544
  return false;
543
545
  }
544
546
  }
545
547
 
546
548
  if (Status.is(stored.status, .PAR) and try Rolls.paralyzed(battle, player, options)) {
547
549
  resetCant(volatiles);
548
- try log.cant(ident, .Paralysis);
550
+ try log.cant(.{ ident, .Paralysis });
549
551
  return false;
550
552
  }
551
553
 
@@ -565,11 +567,11 @@ fn resetCant(volatiles: *align(8) Volatiles) void {
565
567
 
566
568
  pub fn canMove(battle: anytype, player: Player, state: *State, options: anytype) !bool {
567
569
  var side = battle.side(player);
568
- var volatiles = &side.active.volatiles;
570
+ const volatiles = &side.active.volatiles;
569
571
 
570
572
  // FIXME: pass in target to avoid lookup?
571
573
  const target = if (Move.get(state.move).target == .Self) player else player.foe();
572
- try options.log.moveFrom(battle.active(player), state.move, battle.active(target), state.from);
574
+ try options.log.move(.{ battle.active(player), state.move, battle.active(target), state.from });
573
575
 
574
576
  const charging = false; // TODO charging uses doUse
575
577
  const skip_pp = charging or state.move == .Struggle or
@@ -849,13 +851,13 @@ fn checkFaint(
849
851
  if (foe_fainted) try faint(battle, player.foe(), !more, options);
850
852
 
851
853
  if (player_out and foe_out) {
852
- try options.log.tie();
854
+ try options.log.tie(.{});
853
855
  return Result.Tie;
854
856
  } else if (player_out) {
855
- try options.log.win(player.foe());
857
+ try options.log.win(.{player.foe()});
856
858
  return if (player == .P1) Result.Lose else Result.Win;
857
859
  } else if (foe_out) {
858
- try options.log.win(player);
860
+ try options.log.win(.{player});
859
861
  return if (player == .P1) Result.Win else Result.Lose;
860
862
  }
861
863
 
@@ -885,19 +887,19 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !void {
885
887
  side.stored().stats.spe;
886
888
  }
887
889
 
888
- try options.log.faint(battle.active(player), done);
890
+ try options.log.faint(.{ battle.active(player), done });
889
891
  options.calc.capped(player);
890
892
  }
891
893
 
892
894
  // TODO: see if we can move *before* applyDamage and include failuretext logic
893
895
  pub fn reportOutcome(battle: anytype, player: Player, state: *State, options: anytype) !void {
894
896
  const foe_ident = battle.active(player.foe());
895
- if (state.crit) try options.log.crit(foe_ident);
897
+ if (state.crit) try options.log.crit(.{foe_ident});
896
898
  if (!state.immune()) {
897
899
  if (state.effectiveness > Effectiveness.neutral) {
898
- try options.log.supereffective(foe_ident);
900
+ try options.log.supereffective(.{foe_ident});
899
901
  } else if (state.effectiveness < Effectiveness.neutral) {
900
- try options.log.resisted(foe_ident);
902
+ try options.log.resisted(.{foe_ident});
901
903
  }
902
904
  }
903
905
  }
@@ -930,8 +932,8 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !bool {
930
932
  }
931
933
 
932
934
  stored.hp -|= damage;
933
- // Pokémon Showdown uses damageOf here but its not relevant in Generation II
934
- try options.log.damage(ident, stored, if (brn) Damage.Burn else Damage.Poison);
935
+ // Pokémon Showdown uses damage [of] here but its not relevant in Generation II
936
+ try options.log.damage(.{ ident, stored, if (brn) Damage.Burn else Damage.Poison });
935
937
  if (stored.hp == 0) return true;
936
938
  }
937
939
 
@@ -947,27 +949,27 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !bool {
947
949
 
948
950
  const damage = @max(stored.stats.hp / 8, 1);
949
951
  stored.hp -|= damage;
950
- // As above, Pokémon Showdown uses damageOf but its not relevant
951
- try options.log.damage(ident, stored, .LeechSeed);
952
+ // As above, Pokémon Showdown uses damage [of] but its not relevant
953
+ try options.log.damage(.{ ident, stored, .LeechSeed });
952
954
 
953
955
  const before = foe_stored.hp;
954
956
  foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
955
957
  // Pokémon Showdown uses the less specific heal here instead of drain... because reasons?
956
- if (foe_stored.hp > before) try options.log.heal(.{ foe_ident, foe_stored, Heal.Silent });
958
+ if (foe_stored.hp > before) try options.log.heal(.{ foe_ident, foe_stored, .Silent });
957
959
  if (stored.hp == 0) return true;
958
960
  }
959
961
 
960
962
  if (volatiles.Nightmare) {
961
963
  stored.hp -= @max(stored.stats.hp / 4, 1);
962
964
  // ibid
963
- try options.log.damage(ident, stored, .Nightmare);
965
+ try options.log.damage(.{ ident, stored, .Nightmare });
964
966
  if (stored.hp == 0) return true;
965
967
  }
966
968
 
967
969
  if (volatiles.Curse) {
968
970
  stored.hp -= @max(stored.stats.hp / 4, 1);
969
971
  // ibid
970
- try options.log.damage(ident, stored, .Curse);
972
+ try options.log.damage(.{ ident, stored, .Curse });
971
973
  if (stored.hp == 0) return true;
972
974
  }
973
975
 
@@ -988,7 +990,7 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
988
990
  future_sight.count -= 1;
989
991
  assert(future_sight.count != 0);
990
992
  if (future_sight.count == 1) {
991
- try options.log.end(battle.active(player), .FutureSight);
993
+ try options.log.end(.{ battle.active(player), .FutureSight });
992
994
  // TODO doMove
993
995
  }
994
996
  }
@@ -997,21 +999,24 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
997
999
 
998
1000
  // Weather
999
1001
  if (battle.field.weather != .None) {
1000
- assert(battle.field.weather_duration > 0);
1001
1002
  battle.field.weather_duration -= 1;
1002
1003
 
1003
1004
  if (battle.field.weather_duration == 0) {
1004
1005
  battle.field.weather = .None;
1005
- try options.log.weather(battle.field.weather, .None);
1006
+ try options.log.weather(.{ battle.field.weather, protocol.Weather.None });
1006
1007
  } else {
1007
- try options.log.weather(battle.field.weather, .Upkeep);
1008
+ try options.log.weather(.{ battle.field.weather, protocol.Weather.Upkeep });
1008
1009
  if (battle.field.weather == .Sandstorm) {
1009
1010
  inline for (players) |player| {
1010
1011
  var side = battle.side(player);
1011
1012
  const active = side.active;
1012
1013
  if (!(active.volatiles.Underground or active.types.sandstormImmune())) {
1013
1014
  side.stored().hp -|= @max(side.stored().stats.hp / 8, 1);
1014
- try options.log.damage(battle.active(player), side.stored(), .Sandstorm);
1015
+ try options.log.damage(.{
1016
+ battle.active(player),
1017
+ side.stored(),
1018
+ .Sandstorm,
1019
+ });
1015
1020
  }
1016
1021
  }
1017
1022
  if (try checkFaint(battle, .P1, options)) |r| return r;
@@ -1023,19 +1028,24 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1023
1028
  inline for (players) |player| {
1024
1029
  var side = battle.side(player);
1025
1030
  var volatiles = &side.active.volatiles;
1031
+ const ident = battle.active(player);
1026
1032
 
1027
1033
  if (volatiles.bind.duration > 0 and !volatiles.Substitute) {
1028
1034
  volatiles.bind.duration -= 1;
1029
1035
  if (volatiles.bind.duration == 0) {
1030
1036
  const reason = @intFromEnum(End.Bind) + volatiles.bind.reason - 1;
1031
- try options.log.end(battle.active(player), @enumFromInt(reason));
1037
+ try options.log.end(.{ ident, @as(End, @enumFromInt(reason)) });
1032
1038
  } else {
1033
1039
  var reason = @intFromEnum(Activate.Bind) + volatiles.bind.reason - 1;
1034
- try options.log.activate(battle.active(player), @enumFromInt(reason));
1040
+ try options.log.activate(.{ ident, @as(Activate, @enumFromInt(reason)) });
1035
1041
 
1036
1042
  reason = @intFromEnum(Damage.Bind) + volatiles.bind.reason - 1;
1037
1043
  side.stored().hp -|= @max(side.stored().stats.hp / 16, 1);
1038
- try options.log.damage(battle.active(player), side.stored(), @enumFromInt(reason));
1044
+ try options.log.damage(.{
1045
+ ident,
1046
+ side.stored(),
1047
+ @as(Damage, @enumFromInt(reason)),
1048
+ });
1039
1049
  }
1040
1050
  }
1041
1051
  }
@@ -1047,12 +1057,11 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1047
1057
  var volatiles = &side.active.volatiles;
1048
1058
 
1049
1059
  if (volatiles.PerishSong) {
1050
- assert(volatiles.perish_song > 0);
1051
1060
  volatiles.perish_song -= 1;
1052
1061
 
1053
1062
  assert(volatiles.perish_song < 3);
1054
1063
  const reason = @intFromEnum(Start.PerishSong0) + volatiles.perish_song;
1055
- try options.log.start(battle.active(player), @enumFromInt(reason));
1064
+ try options.log.start(.{ battle.active(player), @as(Start, @enumFromInt(reason)) });
1056
1065
 
1057
1066
  if (volatiles.perish_song == 0) {
1058
1067
  volatiles.PerishSong = false;
@@ -1070,14 +1079,14 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1070
1079
  const before = stored.hp;
1071
1080
  stored.hp = @min(stored.hp + @max(stored.stats.hp / 16, 1), stored.stats.hp);
1072
1081
  if (stored.hp > before) {
1073
- try options.log.heal(.{ battle.active(player), stored, Heal.Leftovers });
1082
+ try options.log.heal(.{ battle.active(player), stored, .Leftovers });
1074
1083
  }
1075
1084
  }
1076
1085
  }
1077
1086
 
1078
1087
  // Mystery Berry
1079
1088
  inline for (players) |player| {
1080
- var stored = battle.side(player).stored();
1089
+ const stored = battle.side(player).stored();
1081
1090
 
1082
1091
  if (stored.item == .MysteryBerry) {
1083
1092
  // TODO stored and active moves, transform, etc
@@ -1092,7 +1101,7 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1092
1101
  !side.active.volatiles.frozen and
1093
1102
  try Rolls.defrost(battle, player, options))
1094
1103
  {
1095
- try options.log.curestatus(battle.active(player), side.stored().status, .Message);
1104
+ try options.log.curestatus(.{ battle.active(player), side.stored().status, .Message });
1096
1105
  side.stored().status = 0;
1097
1106
  }
1098
1107
  }
@@ -1133,7 +1142,7 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1133
1142
  stored.hp < stored.stats.hp;
1134
1143
  if (proc) {
1135
1144
  assert(stored.hp != 0);
1136
- try options.log.enditem(ident, stored.item, .Eat);
1145
+ try options.log.enditem(.{ ident, stored.item, .Eat });
1137
1146
  stored.item = .None;
1138
1147
 
1139
1148
  const offset = num - @intFromEnum(Item.Berry);
@@ -1150,7 +1159,7 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1150
1159
 
1151
1160
  if (proc) {
1152
1161
  assert(Status.any(stored.status));
1153
- try options.log.enditem(ident, stored.item, .Eat);
1162
+ try options.log.enditem(.{ ident, stored.item, .Eat });
1154
1163
  stored.item = .None;
1155
1164
 
1156
1165
  // FIXME wtf?
@@ -1158,21 +1167,21 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1158
1167
  // side.active.volatiles.Nightmare = false;
1159
1168
  if (stored.item == .MiracleBerry) {
1160
1169
  side.active.volatiles.Confusion = false;
1161
- try options.log.end(ident, .Confusion);
1170
+ try options.log.end(.{ ident, .Confusion });
1162
1171
  }
1163
1172
  stored.status = 0;
1164
1173
 
1165
1174
  // FIXME recalc stats
1166
1175
 
1167
- try options.log.curestatus(ident, stored.status, .Message);
1176
+ try options.log.curestatus(.{ ident, stored.status, .Message });
1168
1177
  }
1169
1178
  } else if (num == @intFromEnum(Item.BitterBerry)) {
1170
1179
  if (side.active.volatiles.Confusion) {
1171
- try options.log.enditem(ident, stored.item, .Eat);
1180
+ try options.log.enditem(.{ ident, stored.item, .Eat });
1172
1181
  stored.item = .None;
1173
1182
 
1174
1183
  side.active.volatiles.Confusion = false;
1175
- try options.log.end(ident, .Confusion);
1184
+ try options.log.end(.{ ident, .Confusion });
1176
1185
  }
1177
1186
  }
1178
1187
  }
@@ -1184,12 +1193,11 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1184
1193
  var volatiles = &side.active.volatiles;
1185
1194
 
1186
1195
  if (volatiles.Encore) {
1187
- assert(volatiles.encore > 0);
1188
1196
  volatiles.encore -= 1;
1189
1197
 
1190
1198
  if (volatiles.encore == 0 or side.active.move(mslot).pp == 0) {
1191
1199
  volatiles.Encore = false;
1192
- try options.log.end(battle.active(player), .Encore);
1200
+ try options.log.end(.{ battle.active(player), .Encore });
1193
1201
  }
1194
1202
  }
1195
1203
  }
@@ -1206,12 +1214,11 @@ fn updateSideCondition(
1206
1214
  ) !void {
1207
1215
  var side = battle.side(player);
1208
1216
  if (@field(side.conditions, key)) {
1209
- assert(@field(side.conditions, value) > 0);
1210
1217
  @field(side.conditions, value) -= 1;
1211
1218
 
1212
1219
  if (@field(side.conditions, value) == 0) {
1213
1220
  @field(side.conditions, key) = false;
1214
- try options.log.sideend(player, @field(protocol.Side, key));
1221
+ try options.log.sideend(.{ player, @field(protocol.Side, key) });
1215
1222
  }
1216
1223
  }
1217
1224
  }
@@ -1220,13 +1227,13 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
1220
1227
  battle.turn += 1;
1221
1228
 
1222
1229
  if (showdown and battle.turn >= 1000) {
1223
- try options.log.tie();
1230
+ try options.log.tie(.{});
1224
1231
  return Result.Tie;
1225
1232
  } else if (battle.turn >= 65535) {
1226
1233
  return Result.Error;
1227
1234
  }
1228
1235
 
1229
- try options.log.turn(battle.turn);
1236
+ try options.log.turn(.{battle.turn});
1230
1237
 
1231
1238
  return Result.Default;
1232
1239
  }
@@ -1242,7 +1249,7 @@ pub fn buildRage(battle: anytype, player: Player, state: *State, options: anytyp
1242
1249
  }
1243
1250
  } else {
1244
1251
  foe.active.volatiles.rage +|= 1;
1245
- try options.log.activate(battle.active(player), .Rage);
1252
+ try options.log.activate(.{ battle.active(player), .Rage });
1246
1253
  }
1247
1254
  }
1248
1255
  }
@@ -1265,7 +1272,7 @@ pub fn destinyBond(battle: anytype, player: Player, _: *State, options: anytype)
1265
1272
 
1266
1273
  if (foe.active.volatiles.DestinyBond) {
1267
1274
  battle.side(player).stored().hp = 0;
1268
- try options.log.activate(battle.active(player.foe()), .DestinyBond);
1275
+ try options.log.activate(.{ battle.active(player.foe()), .DestinyBond });
1269
1276
  }
1270
1277
  return true;
1271
1278
  }
@@ -1278,14 +1285,14 @@ pub const Effects = struct {
1278
1285
  const gender: u8 = @intFromEnum(side.stored().dvs.gender);
1279
1286
  const foe_gender: u8 = @intFromEnum(foe.stored().dvs.gender);
1280
1287
  if (gender + foe_gender != 1) {
1281
- return options.log.immune(battle.active(player.foe()), .None);
1288
+ return options.log.immune(.{ battle.active(player.foe()), .None });
1282
1289
  } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
1283
- try options.log.lastmiss();
1284
- return options.log.miss(battle.active(player));
1290
+ try options.log.lastmiss(.{});
1291
+ return options.log.miss(.{battle.active(player)});
1285
1292
  }
1286
1293
 
1287
1294
  foe.active.volatiles.Attract = true;
1288
- try options.log.start(battle.active(player.foe()), .Attract);
1295
+ try options.log.start(.{ battle.active(player.foe()), .Attract });
1289
1296
  }
1290
1297
 
1291
1298
  pub fn batonPass(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1297,14 +1304,32 @@ pub const Effects = struct {
1297
1304
  }
1298
1305
 
1299
1306
  pub fn bellyDrum(battle: anytype, player: Player, state: *State, options: anytype) !void {
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
- //
1307
+ assert(!state.miss);
1308
+ var side = battle.side(player);
1309
+
1310
+ const ident = battle.active(player);
1311
+
1312
+ assert(side.active.boosts.atk >= -6 and side.active.boosts.atk <= 6);
1313
+ if (side.active.boosts.atk == 6 or side.active.boosts.atk == MAX_STAT_VALUE) {
1314
+ return options.log.fail(.{ ident, .None });
1315
+ }
1316
+
1317
+ const hp = @max(side.stored().stats.hp / 2, 1);
1318
+ if (side.stored().hp <= hp) {
1319
+ try boostStat(battle, side, &side.active.stats, &side.active.boosts, "atk", 2);
1320
+ try options.log.boost(.{ ident, .Attack, @as(i8, 2), .Silent });
1321
+ return options.log.fail(.{ ident, .None });
1322
+ } else {
1323
+ side.stored().hp -= hp;
1324
+ try options.log.damage(.{ ident, side.stored(), .None });
1325
+
1326
+ const before = side.active.boosts.atk;
1327
+ var i: usize = 0;
1328
+ while (i < 6 and side.active.stats.atk < MAX_STAT_VALUE) : (i += 1) {
1329
+ try boostStat(battle, side, &side.active.stats, &side.active.boosts, "atk", 2);
1330
+ }
1331
+ try options.log.boost(.{ ident, .Attack, @as(i8, side.active.boosts.atk - before) });
1332
+ }
1308
1333
  }
1309
1334
 
1310
1335
  pub const bide = struct {
@@ -1316,7 +1341,6 @@ pub const Effects = struct {
1316
1341
 
1317
1342
  if (!volatiles.Bide) return;
1318
1343
 
1319
- assert(volatiles.attacks > 0);
1320
1344
  // if (options.calc.modify(player, .attacking)) |extend| {
1321
1345
  // if (!extend) volatiles.attacks = 0;
1322
1346
  // } else {
@@ -1324,10 +1348,10 @@ pub const Effects = struct {
1324
1348
  // }
1325
1349
  try options.chance.attacking(player, volatiles.attacks);
1326
1350
 
1327
- if (volatiles.attacks != 0) return options.log.activate(ident, .Bide);
1351
+ if (volatiles.attacks != 0) return options.log.activate(.{ ident, .Bide });
1328
1352
 
1329
1353
  volatiles.Bide = false;
1330
- try options.log.end(ident, .Bide);
1354
+ try options.log.end(.{ ident, .Bide });
1331
1355
 
1332
1356
  state.damage = volatiles.bide *% 2;
1333
1357
  volatiles.bide = 0;
@@ -1361,11 +1385,11 @@ pub const Effects = struct {
1361
1385
  if (Status.any(foe_stored.status)) {
1362
1386
  if (Status.is(foe_stored.status, .FRZ)) {
1363
1387
  assert(Move.get(state.move).type == .Fire or state.move == .TriAttack);
1364
- try options.log.curestatus(
1388
+ try options.log.curestatus(.{
1365
1389
  battle.active(player.foe()),
1366
1390
  foe_stored.status,
1367
1391
  .Message,
1368
- );
1392
+ });
1369
1393
  foe_stored.status = 0;
1370
1394
  }
1371
1395
  return;
@@ -1380,7 +1404,8 @@ pub const Effects = struct {
1380
1404
 
1381
1405
  foe_stored.status = Status.init(.BRN);
1382
1406
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
1383
- try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
1407
+
1408
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
1384
1409
 
1385
1410
  // TODO: check for status berry
1386
1411
  }
@@ -1400,7 +1425,7 @@ pub const Effects = struct {
1400
1425
  for (side.active.moves) |m| {
1401
1426
  if (convertible(side.active, m.id)) n += 1;
1402
1427
  }
1403
- if (n == 0) return options.log.fail(battle.active(player), .None);
1428
+ if (n == 0) return options.log.fail(.{ battle.active(player), .None });
1404
1429
 
1405
1430
  side.active.types.type1 = try Rolls.conversion(battle, player, n, options);
1406
1431
  side.active.types.type2 = side.active.types.type1;
@@ -1410,9 +1435,9 @@ pub const Effects = struct {
1410
1435
  var side = battle.side(player);
1411
1436
 
1412
1437
  const last = battle.foe(player).lastMove(false);
1413
- if (last == .None) return options.log.fail(battle.active(player), .None);
1438
+ if (last == .None) return options.log.fail(.{ battle.active(player), .None });
1414
1439
  const move = Move.get(last);
1415
- if (move.type == .@"???") return options.log.fail(battle.active(player), .None);
1440
+ if (move.type == .@"???") return options.log.fail(.{ battle.active(player), .None });
1416
1441
 
1417
1442
  side.active.types.type1 = try Rolls.conversion2(battle, player, move.type, options);
1418
1443
  side.active.types.type2 = side.active.types.type1;
@@ -1454,41 +1479,41 @@ pub const Effects = struct {
1454
1479
 
1455
1480
  if (side.active.types.includes(.Ghost)) {
1456
1481
  if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
1457
- try options.log.lastmiss();
1458
- return options.log.miss(battle.active(player));
1482
+ try options.log.lastmiss(.{});
1483
+ return options.log.miss(.{battle.active(player)});
1459
1484
  } else if (foe.active.volatiles.Substitute) {
1460
- return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
1485
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
1461
1486
  } else if (foe.active.volatiles.Curse) {
1462
- return options.log.fail(foe_ident, .None);
1487
+ return options.log.fail(.{ foe_ident, .None });
1463
1488
  }
1464
1489
 
1465
1490
  foe.active.volatiles.Curse = true;
1466
- try options.log.start(foe_ident, .Curse);
1491
+ try options.log.start(.{ foe_ident, .Curse });
1467
1492
 
1468
1493
  side.stored().hp -= @max(side.stored().stats.hp / 2, 1);
1469
- try options.log.damage(ident, side.stored(), .Curse);
1494
+ try options.log.damage(.{ ident, side.stored(), .Curse });
1470
1495
  } else {
1471
- var stats = &side.active.stats;
1472
- var boosts = &side.active.boosts;
1496
+ const stats = &side.active.stats;
1497
+ const boosts = &side.active.boosts;
1473
1498
 
1474
1499
  const max_atk = boosts.atk == 6 or stats.atk == MAX_STAT_VALUE;
1475
1500
  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);
1501
+ if (max_atk and max_def) return options.log.fail(.{ ident, .None });
1477
1502
 
1478
1503
  if (!(boosts.spe == -6 or stats.spe == 1)) {
1479
1504
  assert(boosts.spe >= -6 and boosts.spe <= 6);
1480
1505
  try unboostStat(battle, side, stats, boosts, "spe", 1);
1481
- try options.log.boost(ident, .Speed, @as(i8, -1));
1506
+ try options.log.boost(.{ ident, .Speed, @as(i8, -1) });
1482
1507
  }
1483
1508
  if (!max_atk) {
1484
1509
  assert(boosts.atk >= -6 and boosts.atk <= 6);
1485
1510
  try boostStat(battle, side, stats, boosts, "atk", 1);
1486
- try options.log.boost(ident, .Attack, @as(i8, 1));
1511
+ try options.log.boost(.{ ident, .Attack, @as(i8, 1) });
1487
1512
  }
1488
1513
  if (!max_def) {
1489
1514
  assert(boosts.def >= -6 and boosts.def <= 6);
1490
1515
  try boostStat(battle, side, stats, boosts, "def", 1);
1491
- try options.log.boost(ident, .Defense, @as(i8, 1));
1516
+ try options.log.boost(.{ ident, .Defense, @as(i8, 1) });
1492
1517
  }
1493
1518
  }
1494
1519
  }
@@ -1500,14 +1525,13 @@ pub const Effects = struct {
1500
1525
  pub fn defrost(battle: anytype, player: Player, _: *State, options: anytype) !void {
1501
1526
  var side = battle.side(player);
1502
1527
  assert(Status.is(side.stored().status, .FRZ));
1503
-
1504
- try options.log.curestatus(battle.active(player), side.stored().status, .Message);
1528
+ try options.log.curestatus(.{ battle.active(player), side.stored().status, .Message });
1505
1529
  side.stored().status = 0;
1506
1530
  }
1507
1531
 
1508
1532
  pub fn destinyBond(battle: anytype, player: Player, _: *State, options: anytype) !void {
1509
1533
  battle.side(player).active.volatiles.DestinyBond = true;
1510
- try options.log.singlemove(battle.active(player), Move.DestinyBond);
1534
+ try options.log.singlemove(.{ battle.active(player), Move.DestinyBond });
1511
1535
  }
1512
1536
 
1513
1537
  pub fn disable(battle: anytype, player: Player, _: *State, options: anytype) !void {
@@ -1515,14 +1539,10 @@ pub const Effects = struct {
1515
1539
  var volatiles = &foe.active.volatiles;
1516
1540
  const foe_ident = battle.active(player.foe());
1517
1541
 
1518
- if (volatiles.disable.move != 0) {
1519
- return options.log.fail(foe_ident, .None);
1520
- }
1542
+ if (volatiles.disable.move != 0) return options.log.fail(.{ foe_ident, .None });
1521
1543
 
1522
1544
  const last = battle.foe(player).lastMove(false);
1523
- if (last == .None or last == .Struggle) {
1524
- return options.log.fail(foe_ident, .None);
1525
- }
1545
+ if (last == .None or last == .Struggle) return options.log.fail(.{ foe_ident, .None });
1526
1546
 
1527
1547
  var slot: u3 = 0;
1528
1548
  for (foe.active.moves) |m| {
@@ -1531,12 +1551,12 @@ pub const Effects = struct {
1531
1551
  if (m.id == last) break;
1532
1552
  }
1533
1553
  const move = foe.active.move(slot);
1534
- if (move.pp == 0) return options.log.fail(foe_ident, .None);
1554
+ if (move.pp == 0) return options.log.fail(.{ foe_ident, .None });
1535
1555
 
1536
1556
  volatiles.disable.move = slot;
1537
1557
  volatiles.disable.duration = Rolls.disableDuration(battle, player, options);
1538
1558
 
1539
- try options.log.startEffect(foe_ident, .Disable, move.id);
1559
+ try options.log.start(.{ foe_ident, .Disable, move.id });
1540
1560
  }
1541
1561
 
1542
1562
  pub const double = struct {
@@ -1555,13 +1575,14 @@ pub const Effects = struct {
1555
1575
 
1556
1576
  pub fn drainHP(battle: anytype, player: Player, state: *State, options: anytype) !void {
1557
1577
  var stored = battle.side(player).stored();
1578
+ const ident = battle.active(player);
1558
1579
 
1559
1580
  assert(state.damage != 0);
1560
1581
 
1561
1582
  if (stored.hp == stored.stats.hp) return;
1562
1583
  stored.hp = @min(stored.stats.hp, stored.hp + @max(state.damage / 2, 1));
1563
1584
 
1564
- try options.log.drain(battle.active(player), stored, battle.active(player.foe()));
1585
+ try options.log.heal(.{ ident, stored, .Drain, battle.active(player.foe()) });
1565
1586
  }
1566
1587
 
1567
1588
  pub fn encore(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1573,7 +1594,7 @@ pub const Effects = struct {
1573
1594
  const failed = volatiles.Encore or
1574
1595
  (last == .None or last == .Struggle or
1575
1596
  last == .Encore or .last == .MirrorMove);
1576
- if (failed) return options.log.fail(foe_ident, .None);
1597
+ if (failed) return options.log.fail(.{ foe_ident, .None });
1577
1598
 
1578
1599
  var slot: u3 = 0;
1579
1600
  for (foe.active.moves) |m| {
@@ -1582,7 +1603,7 @@ pub const Effects = struct {
1582
1603
  if (m.id == last) break;
1583
1604
  }
1584
1605
  const move = foe.active.move(slot);
1585
- if (move.pp == 0) return options.log.fail(foe_ident, .None);
1606
+ if (move.pp == 0) return options.log.fail(.{ foe_ident, .None });
1586
1607
 
1587
1608
  volatiles.Encore = true;
1588
1609
  volatiles.encore = Rolls.encoreDuration(battle, player, options);
@@ -1590,11 +1611,19 @@ pub const Effects = struct {
1590
1611
  // TODO ???
1591
1612
  _ = state;
1592
1613
 
1593
- try options.log.startEffect(foe_ident, .Encore, move.id);
1614
+ try options.log.start(.{ foe_ident, .Encore, move.id });
1594
1615
  }
1595
1616
 
1596
- pub fn explode(battle: anytype, player: Player, state: *State, options: anytype) !void {
1597
- _ = .{ battle, player, state, options }; // TODO
1617
+ pub fn explode(battle: anytype, player: Player, _: *State, _: anytype) !void {
1618
+ var side = battle.side(player);
1619
+ var stored = side.stored();
1620
+
1621
+ stored.hp = 0;
1622
+ // Pokémon Showdown sets the status to 0 on faint(), and we need to do the same to be able
1623
+ // to correctly implement Pokémon Showdown's dumb speed-based switch mechanics
1624
+ if (!showdown) stored.status = 0;
1625
+ side.active.volatiles.LeechSeed = false;
1626
+ side.active.volatiles.DestinyBond = false;
1598
1627
  }
1599
1628
 
1600
1629
  pub fn falseSwipe(battle: anytype, player: Player, state: *State, _: anytype) !void {
@@ -1644,12 +1673,11 @@ pub const Effects = struct {
1644
1673
  var side = battle.side(player);
1645
1674
 
1646
1675
  if (side.active.volatiles.FocusEnergy) {
1647
- if (!showdown) try options.log.fail(battle.active(player), .None);
1648
- return;
1676
+ return if (!showdown) try options.log.fail(.{ battle.active(player), .None });
1649
1677
  }
1650
1678
 
1651
1679
  side.active.volatiles.FocusEnergy = true;
1652
- try options.log.start(battle.active(player), .FocusEnergy);
1680
+ try options.log.start(.{ battle.active(player), .FocusEnergy });
1653
1681
  }
1654
1682
 
1655
1683
  var SLOTS: [5]u4 = .{ 0, 0, 0, 0, 0 };
@@ -1659,7 +1687,7 @@ pub const Effects = struct {
1659
1687
 
1660
1688
  const foe = battle.foe(player);
1661
1689
 
1662
- if (state.first) return options.log.fail(battle.active(player), .None);
1690
+ if (state.first) return options.log.fail(.{ battle.active(player), .None });
1663
1691
 
1664
1692
  var n: u4 = 0;
1665
1693
  var i: u4 = 0;
@@ -1673,7 +1701,7 @@ pub const Effects = struct {
1673
1701
  }
1674
1702
  }
1675
1703
 
1676
- if (n == 0) return options.log.fail(battle.active(player), .None);
1704
+ if (n == 0) return options.log.fail(.{ battle.active(player), .None });
1677
1705
 
1678
1706
  const slot = try Rolls.forceSwitch(battle, player, SLOTS[0..i], n, options);
1679
1707
  try switchIn(battle, player.foe(), slot, .Drag, options);
@@ -1683,14 +1711,14 @@ pub const Effects = struct {
1683
1711
  var foe = battle.foe(player);
1684
1712
 
1685
1713
  if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
1686
- try options.log.lastmiss();
1687
- return options.log.miss(battle.active(player));
1714
+ try options.log.lastmiss(.{});
1715
+ return options.log.miss(.{battle.active(player)});
1688
1716
  } else if (foe.active.volatiles.Foresight) {
1689
- return options.log.fail(battle.active(player), .None);
1717
+ return options.log.fail(.{ battle.active(player), .None });
1690
1718
  }
1691
1719
 
1692
1720
  foe.active.volatiles.Foresight = true;
1693
- try options.log.start(battle.active(player.foe()), .FocusEnergy);
1721
+ try options.log.start(.{ battle.active(player.foe()), .FocusEnergy });
1694
1722
  }
1695
1723
 
1696
1724
  pub fn freezeChance(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1713,7 +1741,7 @@ pub const Effects = struct {
1713
1741
  if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1714
1742
 
1715
1743
  foe_stored.status = Status.init(.FRZ);
1716
- try options.log.status(foe_ident, foe_stored.status, .None);
1744
+ try options.log.status(.{ foe_ident, foe_stored.status, .None });
1717
1745
 
1718
1746
  // TODO: check for status berry
1719
1747
 
@@ -1721,8 +1749,26 @@ pub const Effects = struct {
1721
1749
  foe.active.volatiles.frozen = true;
1722
1750
  }
1723
1751
 
1724
- pub fn furyCutter(battle: anytype, player: Player, state: *State, options: anytype) !void {
1725
- _ = .{ battle, player, state, options }; // TODO
1752
+ // TODO FIXME showdown calls this before calcDamage (after checkHit) and modifies state.bp!
1753
+ pub fn furyCutter(battle: anytype, player: Player, state: *State, _: anytype) !void {
1754
+ var side = battle.side(player);
1755
+ var volatiles = &side.active.volatiles;
1756
+
1757
+ if (state.miss) {
1758
+ volatiles.fury_cutter = 0;
1759
+ return;
1760
+ }
1761
+
1762
+ assert(volatiles.fury_cutter >= 0 and volatiles.fury_cutter <= 5);
1763
+ if (volatiles.fury_cutter == 5) return;
1764
+ volatiles.fury_cutter += 1;
1765
+
1766
+ if (showdown) {
1767
+ state.bp *= 2;
1768
+ assert(state.bp <= 160);
1769
+ } else {
1770
+ state.damage *|= 2;
1771
+ }
1726
1772
  }
1727
1773
 
1728
1774
  pub fn futureSight(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1748,7 +1794,7 @@ pub const Effects = struct {
1748
1794
  inline for (STATS) |s| @field(foe.active.stats, s) = @field(stats, s);
1749
1795
  statusModify(foe.stored().status, &foe.active.stats);
1750
1796
 
1751
- try options.log.clearallboost();
1797
+ try options.log.clearallboost(.{});
1752
1798
  }
1753
1799
 
1754
1800
  pub fn heal(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1756,7 +1802,7 @@ pub const Effects = struct {
1756
1802
  var stored = side.stored();
1757
1803
  const ident = battle.active(player);
1758
1804
 
1759
- if (stored.stats.hp == stored.hp) return options.log.fail(ident, .None);
1805
+ if (stored.stats.hp == stored.hp) return options.log.fail(.{ ident, .None });
1760
1806
 
1761
1807
  const rest = state.move == .Rest;
1762
1808
  if (rest) {
@@ -1765,7 +1811,7 @@ pub const Effects = struct {
1765
1811
  side.active.volatiles.Toxic = false;
1766
1812
  stored.status = Status.slf(2);
1767
1813
  recomputeStats(battle, side);
1768
- try options.log.statusFrom(ident, stored.status, Move.Rest);
1814
+ try options.log.status(.{ ident, stored.status, .From, Move.Rest });
1769
1815
  stored.hp = stored.stats.hp;
1770
1816
  } else {
1771
1817
  stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
@@ -1777,7 +1823,7 @@ pub const Effects = struct {
1777
1823
  var side = battle.side(player);
1778
1824
  side.active.volatiles.Nightmare = false;
1779
1825
 
1780
- try options.log.cureteam(battle.active(player));
1826
+ try options.log.cureteam(.{battle.active(player)});
1781
1827
  for (&side.pokemon) |*p| p.status = 0;
1782
1828
 
1783
1829
  // We technically only need to recompute stats if the active Pokémon was burned/paralyzed,
@@ -1793,7 +1839,7 @@ pub const Effects = struct {
1793
1839
 
1794
1840
  pub fn hyperBeam(battle: anytype, player: Player, _: *State, options: anytype) !void {
1795
1841
  battle.side(player).active.volatiles.Recharging = true;
1796
- try options.log.mustrecharge(battle.active(player));
1842
+ try options.log.mustrecharge(.{battle.active(player)});
1797
1843
  }
1798
1844
 
1799
1845
  pub fn leechSeed(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1803,12 +1849,12 @@ pub const Effects = struct {
1803
1849
  // Invulnerability trumps type immunity on Pokémon Showdown
1804
1850
  const invulnerable = foe.active.volatiles.Underground or foe.active.volatiles.Flying;
1805
1851
  if (!invulnerable and foe.active.types.includes(.Grass)) {
1806
- return options.log.immune(battle.active(player.foe()), .None);
1852
+ return options.log.immune(.{ battle.active(player.foe()), .None });
1807
1853
  }
1808
1854
  try checkHit(battle, player, state, options);
1809
1855
  if (state.miss) {
1810
- try options.log.lastmiss();
1811
- return options.log.miss(battle.active(player));
1856
+ try options.log.lastmiss(.{});
1857
+ return options.log.miss(.{battle.active(player)});
1812
1858
  }
1813
1859
  if (foe.active.volatiles.LeechSeed) return;
1814
1860
  } else {
@@ -1816,14 +1862,14 @@ pub const Effects = struct {
1816
1862
  const miss =
1817
1863
  state.miss or foe.active.types.includes(.Grass) or foe.active.volatiles.LeechSeed;
1818
1864
  if (miss) {
1819
- try options.log.lastmiss();
1820
- return options.log.miss(battle.active(player));
1865
+ try options.log.lastmiss(.{});
1866
+ return options.log.miss(.{battle.active(player)});
1821
1867
  }
1822
1868
  }
1823
1869
 
1824
1870
  foe.active.volatiles.LeechSeed = true;
1825
1871
 
1826
- try options.log.start(battle.active(player.foe()), .LeechSeed);
1872
+ try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
1827
1873
  }
1828
1874
 
1829
1875
  pub fn lockOn(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1831,20 +1877,20 @@ pub const Effects = struct {
1831
1877
  const foe_ident = battle.active(player.foe());
1832
1878
 
1833
1879
  if (foe.active.volatiles.Substitute) {
1834
- return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
1880
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
1835
1881
  } else if (state.miss) {
1836
- return options.log.fail(foe_ident, .None);
1882
+ return options.log.fail(.{ foe_ident, .None });
1837
1883
  }
1838
1884
 
1839
1885
  foe.active.volatiles.LockOn = true;
1840
- try options.log.activate(battle.active(player), .LockOn);
1886
+ try options.log.activate(.{ battle.active(player), .LockOn });
1841
1887
  }
1842
1888
 
1843
1889
  const MAGNITUDE_POWER = [_]u8{ 10, 30, 50, 70, 90, 110, 150 };
1844
1890
 
1845
1891
  pub fn magnitude(battle: anytype, player: Player, state: *State, options: anytype) !void {
1846
1892
  const num = try Rolls.magnitude(battle, player, options);
1847
- try options.log.magnitude(battle.active(player), num);
1893
+ try options.log.activate(.{ battle.active(player), .Magnitude, num });
1848
1894
  state.bp = MAGNITUDE_POWER[num - 4];
1849
1895
  }
1850
1896
 
@@ -1852,11 +1898,11 @@ pub const Effects = struct {
1852
1898
  var foe = battle.foe(player);
1853
1899
 
1854
1900
  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));
1901
+ if (!showdown) return options.log.fail(.{ battle.active(player.foe()), .None });
1902
+ try options.log.lastmiss(.{});
1903
+ return options.log.miss(.{battle.active(player)});
1858
1904
  } else if (foe.active.volatiles.Trapped) {
1859
- return options.log.fail(battle.active(player.foe()), .None);
1905
+ return options.log.fail(.{ battle.active(player.foe()), .None });
1860
1906
  }
1861
1907
 
1862
1908
  foe.active.volatiles.Trapped = true;
@@ -1883,7 +1929,29 @@ pub const Effects = struct {
1883
1929
  }
1884
1930
 
1885
1931
  pub fn mimic(battle: anytype, player: Player, state: *State, options: anytype) !void {
1886
- _ = .{ battle, player, state, options }; // TODO
1932
+ var side = battle.side(player);
1933
+
1934
+ if (state.miss) {
1935
+ if (!showdown) return options.log.fail(.{ battle.active(player.foe()), .None });
1936
+ try options.log.lastmiss(.{});
1937
+ return options.log.miss(.{battle.active(player)});
1938
+ }
1939
+
1940
+ const move = battle.foe(player).lastMove(false);
1941
+ if (move == .None or move == .Struggle) {
1942
+ return options.log.fail(.{ battle.active(player.foe()), .None });
1943
+ }
1944
+
1945
+ var slot: u4 = 0;
1946
+ for (side.active.moves, 0..) |m, i| {
1947
+ if (m.id == move) return options.log.fail(.{ battle.active(player.foe()), .None });
1948
+ if (m.id == .Mimic) slot = @intCast(i + 1);
1949
+ }
1950
+ assert(slot != 0 and slot == state.mslot);
1951
+
1952
+ side.active.move(slot).id = move;
1953
+ side.active.move(slot).pp = 5;
1954
+ try options.log.start(.{ battle.active(player), .Mimic, move });
1887
1955
  }
1888
1956
 
1889
1957
  pub fn mirrorMove(
@@ -1900,11 +1968,11 @@ pub const Effects = struct {
1900
1968
 
1901
1969
  const last = foe.lastMove(false);
1902
1970
 
1903
- if (last == .None) return options.log.fail(battle.active(player), .None);
1971
+ if (last == .None) return options.log.fail(.{ battle.active(player), .None });
1904
1972
  const knows = knows: for (side.active.moves) |m| {
1905
1973
  if (last == m.id) break :knows true;
1906
1974
  } else false;
1907
- if (knows) return options.log.fail(battle.active(player), .None);
1975
+ if (knows) return options.log.fail(.{ battle.active(player), .None });
1908
1976
 
1909
1977
  // if (wPlayerCharging) ???
1910
1978
  // wPlayerCharging = 1
@@ -1918,17 +1986,17 @@ pub const Effects = struct {
1918
1986
  pub fn mist(battle: anytype, player: Player, _: *State, options: anytype) !void {
1919
1987
  var side = battle.side(player);
1920
1988
 
1921
- if (side.active.volatiles.Mist) return options.log.fail(battle.active(player), .None);
1989
+ if (side.active.volatiles.Mist) return options.log.fail(.{ battle.active(player), .None });
1922
1990
  side.active.volatiles.Mist = true;
1923
1991
 
1924
- try options.log.start(battle.active(player), .Mist);
1992
+ try options.log.start(.{ battle.active(player), .Mist });
1925
1993
  }
1926
1994
 
1927
1995
  pub fn weatherHeal(battle: anytype, player: Player, _: *State, options: anytype) !void {
1928
1996
  var stored = battle.side(player).stored();
1929
1997
  const ident = battle.active(player);
1930
1998
 
1931
- if (stored.hp == stored.stats.hp) return options.log.fail(ident, .None);
1999
+ if (stored.hp == stored.stats.hp) return options.log.fail(.{ ident, .None });
1932
2000
 
1933
2001
  stored.hp = switch (battle.field.weather) {
1934
2002
  .Sun => stored.stats.hp,
@@ -1936,7 +2004,7 @@ pub const Effects = struct {
1936
2004
  else => @min(stored.stats.hp, stored.hp + (stored.stats.hp / 4)),
1937
2005
  };
1938
2006
 
1939
- try options.log.heal(.{ ident, stored, Heal.None });
2007
+ try options.log.heal(.{ ident, stored, .None });
1940
2008
  }
1941
2009
 
1942
2010
  pub fn multiHit(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1949,17 +2017,17 @@ pub const Effects = struct {
1949
2017
  const foe_ident = battle.active(player.foe());
1950
2018
 
1951
2019
  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));
2020
+ if (!showdown) return options.log.fail(.{ battle.active(player.foe()), .None });
2021
+ try options.log.lastmiss(.{});
2022
+ return options.log.miss(.{battle.active(player)});
1955
2023
  } else if (foe_volatiles.Substitute) {
1956
- return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
2024
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
1957
2025
  } else if (!Status.is(foe.stored().status, .SLP) or foe_volatiles.Nightmare) {
1958
- return options.log.fail(foe_ident, .None);
2026
+ return options.log.fail(.{ foe_ident, .None });
1959
2027
  }
1960
2028
 
1961
2029
  foe_volatiles.Nightmare = true;
1962
- try options.log.start(battle.active(player.foe()), .Nightmare);
2030
+ try options.log.start(.{ battle.active(player.foe()), .Nightmare });
1963
2031
  }
1964
2032
 
1965
2033
  pub const ohko = struct {
@@ -1983,9 +2051,9 @@ pub const Effects = struct {
1983
2051
  const foe_ident = battle.active(player.foe());
1984
2052
 
1985
2053
  state.damage = 0;
1986
- if (state.immune()) return options.log.immune(foe_ident, .None);
2054
+ if (state.immune()) return options.log.immune(.{ foe_ident, .None });
1987
2055
  const delta = side.stored().level -| foe.stored().level;
1988
- if (delta == 0) return options.log.immune(foe_ident, .OHKO);
2056
+ if (delta == 0) return options.log.immune(.{ foe_ident, .OHKO });
1989
2057
 
1990
2058
  state.accuracy = 2 * delta +| Gen12.percent(30);
1991
2059
  try checkHit(battle, player, state, options);
@@ -2001,37 +2069,39 @@ pub const Effects = struct {
2001
2069
  const ident = battle.active(player);
2002
2070
  const foe_ident = battle.active(player.foe());
2003
2071
 
2004
- if (state.miss) return options.log.fail(foe_ident, .None);
2072
+ if (state.miss) return options.log.fail(.{ foe_ident, .None });
2005
2073
  if (foe.active.volatiles.Substitute) {
2006
- return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
2074
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
2007
2075
  }
2008
2076
 
2009
2077
  const avg = @max((foe.stored().hp + side.stored().hp) / 2, 1);
2010
2078
  foe.stored().hp -= avg;
2011
2079
  side.stored().hp = avg;
2012
2080
 
2013
- try options.log.sethp(foe_ident, foe.stored(), .Silent);
2014
- try options.log.sethp(ident, side.stored(), .None);
2081
+ try options.log.sethp(.{ foe_ident, foe.stored(), .Silent });
2082
+ try options.log.sethp(.{ ident, side.stored(), .None });
2015
2083
  }
2016
2084
 
2017
2085
  pub fn paralyze(battle: anytype, player: Player, state: *State, options: anytype) !void {
2018
2086
  var foe = battle.foe(player);
2019
2087
  const foe_ident = battle.active(player.foe());
2020
2088
 
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);
2089
+ if (Status.is(foe.stored().status, .PAR)) {
2090
+ return options.log.fail(.{ foe_ident, .Paralysis });
2091
+ }
2092
+ if (state.immune()) return options.log.immune(.{ foe_ident, .None });
2093
+ if (Status.any(foe.stored().status)) return options.log.fail(.{ foe_ident, .None });
2024
2094
  if (state.miss) {
2025
- try options.log.lastmiss();
2026
- return options.log.miss(battle.active(player));
2095
+ try options.log.lastmiss(.{});
2096
+ return options.log.miss(.{battle.active(player)});
2027
2097
  } else if (foe.active.volatiles.Substitute) {
2028
- return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
2098
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
2029
2099
  }
2030
2100
 
2031
2101
  foe.stored().status = Status.init(.PAR);
2032
2102
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2033
2103
 
2034
- try options.log.status(foe_ident, foe.stored().status, .None);
2104
+ try options.log.status(.{ foe_ident, foe.stored().status, .None });
2035
2105
  }
2036
2106
 
2037
2107
  pub fn paralyzeChance(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -2046,14 +2116,15 @@ pub const Effects = struct {
2046
2116
 
2047
2117
  foe_stored.status = Status.init(.PAR);
2048
2118
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2049
- try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
2119
+
2120
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
2050
2121
 
2051
2122
  // TODO: check for status berry
2052
2123
  }
2053
2124
 
2054
2125
  pub fn payDay(battle: anytype, player: Player, _: *State, options: anytype) !void {
2055
2126
  if (!showdown or !battle.foe(player).active.volatiles.Substitute) {
2056
- try options.log.fieldactivate(); // FIXME .PayDay
2127
+ try options.log.fieldactivate(.{}); // FIXME .PayDay
2057
2128
  }
2058
2129
  }
2059
2130
 
@@ -2062,17 +2133,17 @@ pub const Effects = struct {
2062
2133
  var foe = battle.foe(player);
2063
2134
 
2064
2135
  if (foe.active.volatiles.PerishSong) {
2065
- return options.log.fail(battle.active(player.foe()), .None);
2136
+ return options.log.fail(.{ battle.active(player.foe()), .None });
2066
2137
  } else if (!side.active.volatiles.PerishSong) {
2067
2138
  side.active.volatiles.PerishSong = true;
2068
2139
  side.active.volatiles.perish_song = 4;
2069
- try options.log.start(battle.active(player), .PerishSong3Silent);
2140
+ try options.log.start(.{ battle.active(player), .PerishSong3Silent });
2070
2141
  }
2071
2142
  foe.active.volatiles.PerishSong = true;
2072
2143
  foe.active.volatiles.perish_song = 4;
2073
- try options.log.start(battle.active(player.foe()), .PerishSong3Silent);
2144
+ try options.log.start(.{ battle.active(player.foe()), .PerishSong3Silent });
2074
2145
 
2075
- try options.log.fieldactivate(); // FIXME .PerishSong
2146
+ try options.log.fieldactivate(.{}); // FIXME .PerishSong
2076
2147
  }
2077
2148
 
2078
2149
  pub fn poison(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -2082,24 +2153,24 @@ pub const Effects = struct {
2082
2153
 
2083
2154
  const toxic = state.move == .Toxic;
2084
2155
  if (state.immune() or foe.active.types.includes(.Poison)) {
2085
- return options.log.immune(foe_ident, .None);
2156
+ return options.log.immune(.{ foe_ident, .None });
2086
2157
  } else if (Status.any(foe_stored.status)) {
2087
2158
  // Pokémon Showdown considers Toxic to be a status even in Generation II and so
2088
2159
  // will not include a fail reason for Toxic vs. Poison or vice-versa...
2089
- return options.log.fail(foe_ident, if (Status.is(foe_stored.status, .PSN))
2160
+ return options.log.fail(.{ foe_ident, if (Status.is(foe_stored.status, .PSN))
2090
2161
  if (!showdown)
2091
- .Poison
2162
+ Fail.Poison
2092
2163
  else if (toxic == (foe_stored.status == Status.TOX))
2093
- if (toxic) .Toxic else .Poison
2164
+ if (toxic) Fail.Toxic else Fail.Poison
2094
2165
  else
2095
- .None
2166
+ Fail.None
2096
2167
  else
2097
- .None);
2168
+ Fail.None });
2098
2169
  } else if (foe.active.volatiles.Substitute) {
2099
- return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
2170
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
2100
2171
  } else if (state.miss) {
2101
- try options.log.lastmiss();
2102
- return options.log.miss(battle.active(player));
2172
+ try options.log.lastmiss(.{});
2173
+ return options.log.miss(.{battle.active(player)});
2103
2174
  }
2104
2175
 
2105
2176
  foe_stored.status = Status.init(.PSN);
@@ -2109,7 +2180,7 @@ pub const Effects = struct {
2109
2180
  foe.active.volatiles.toxic = 0;
2110
2181
  }
2111
2182
 
2112
- try options.log.status(foe_ident, foe_stored.status, .None);
2183
+ try options.log.status(.{ foe_ident, foe_stored.status, .None });
2113
2184
 
2114
2185
  // TODO: check for status berry
2115
2186
  }
@@ -2125,7 +2196,8 @@ pub const Effects = struct {
2125
2196
  if (foe.conditions.Safeguard) return;
2126
2197
 
2127
2198
  foe_stored.status = Status.init(.PSN);
2128
- try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
2199
+
2200
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
2129
2201
 
2130
2202
  // TODO: check for status berry
2131
2203
  }
@@ -2140,9 +2212,7 @@ pub const Effects = struct {
2140
2212
 
2141
2213
  const failed = !state.first or volatiles.Substitute or
2142
2214
  !try Rolls.protect(battle, player, volatiles.protect, options);
2143
- if (failed) {
2144
- return options.log.fail(battle.active(player), .None);
2145
- }
2215
+ if (failed) return options.log.fail(.{ battle.active(player), .None });
2146
2216
 
2147
2217
  volatiles.protect = @max(8, volatiles.protect + 1);
2148
2218
  if (state.move == .Endure) {
@@ -2150,7 +2220,7 @@ pub const Effects = struct {
2150
2220
  } else {
2151
2221
  volatiles.Protect = true;
2152
2222
  }
2153
- try options.log.singleturn(battle.active(player), state.move);
2223
+ try options.log.singleturn(.{ battle.active(player), state.move });
2154
2224
  }
2155
2225
 
2156
2226
  pub fn psychUp(battle: anytype, player: Player, _: *State, options: anytype) !void {
@@ -2159,11 +2229,11 @@ pub const Effects = struct {
2159
2229
 
2160
2230
  // Pokémon Showdown should fail when the enemy has no boosts/unboosts but doesn't
2161
2231
  if (!showdown and @as(u32, @bitCast(foe.active.boosts)) == 0) {
2162
- return options.log.fail(battle.active(player), .None);
2232
+ return options.log.fail(.{ battle.active(player), .None });
2163
2233
  }
2164
2234
 
2165
2235
  side.active.boosts = foe.active.boosts;
2166
- try options.log.copyboost(battle.active(player), battle.active(player.foe()));
2236
+ try options.log.copyboost(.{ battle.active(player), battle.active(player.foe()) });
2167
2237
  }
2168
2238
 
2169
2239
  pub fn pursuit(battle: anytype, player: Player, state: *State, _: anytype) !void {
@@ -2174,15 +2244,14 @@ pub const Effects = struct {
2174
2244
  pub const rage = struct {
2175
2245
  pub fn start(battle: anytype, player: Player, _: *State, options: anytype) !void {
2176
2246
  battle.side(player).active.volatiles.Rage = true;
2177
- try options.log.singlemove(battle.active(player), Move.Rage);
2247
+ try options.log.singlemove(.{ battle.active(player), Move.Rage });
2178
2248
  }
2179
2249
 
2180
2250
  pub fn damage(battle: anytype, player: Player, state: *State, _: anytype) !void {
2181
- var side = battle.side(player);
2182
- var volatiles = &side.active.volatiles;
2251
+ const side = battle.side(player);
2183
2252
 
2184
- assert(volatiles.Rage);
2185
- state.damage *|= (volatiles.rage +| 1);
2253
+ assert(side.active.volatiles.Rage);
2254
+ state.damage *|= (side.active.volatiles.rage +| 1);
2186
2255
  }
2187
2256
  };
2188
2257
 
@@ -2194,18 +2263,18 @@ pub const Effects = struct {
2194
2263
  var side = battle.side(player);
2195
2264
 
2196
2265
  if (side.active.volatiles.LeechSeed) {
2197
- try options.log.end(battle.active(player), .LeechSeedFrom);
2266
+ try options.log.end(.{ battle.active(player), .LeechSeedFrom });
2198
2267
  side.active.volatiles.LeechSeed = false;
2199
2268
  }
2200
2269
 
2201
2270
  if (side.conditions.Spikes) {
2202
- try options.log.spikesend(battle.active(player));
2271
+ try options.log.sideend(.{ player, protocol.Side.Spikes, battle.active(player) });
2203
2272
  side.conditions.Spikes = false;
2204
2273
  }
2205
2274
 
2206
2275
  if (side.active.volatiles.bind.duration > 0) {
2207
2276
  const reason = @intFromEnum(End.Bind) + side.active.volatiles.bind.reason - 1;
2208
- try options.log.end(battle.active(player), @enumFromInt(reason));
2277
+ try options.log.end(.{ battle.active(player), @as(End, @enumFromInt(reason)) });
2209
2278
  side.active.volatiles.bind = .{};
2210
2279
  }
2211
2280
  }
@@ -2213,18 +2282,14 @@ pub const Effects = struct {
2213
2282
  pub fn recoil(battle: anytype, player: Player, state: *State, options: anytype) !void {
2214
2283
  var side = battle.side(player);
2215
2284
  var stored = side.stored();
2285
+ const ident = battle.active(player);
2216
2286
 
2217
2287
  assert(state.damage > 0);
2218
2288
 
2219
2289
  const damage: i16 = @intCast(@max(state.damage / 4, 1));
2220
2290
  stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
2221
2291
 
2222
- try options.log.damageOf(
2223
- battle.active(player),
2224
- stored,
2225
- .RecoilOf,
2226
- battle.active(player.foe()),
2227
- );
2292
+ try options.log.damage(.{ ident, stored, .RecoilOf, battle.active(player.foe()) });
2228
2293
  }
2229
2294
 
2230
2295
  pub fn reversal(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -2287,7 +2352,7 @@ pub const Effects = struct {
2287
2352
 
2288
2353
  pub fn sandstorm(battle: anytype, player: Player, _: *State, options: anytype) !void {
2289
2354
  if (battle.field.weather != .Sandstorm) return weather(battle, .Sandstorm, options);
2290
- return options.log.fail(battle.active(player), .None);
2355
+ return options.log.fail(.{ battle.active(player), .None });
2291
2356
  }
2292
2357
 
2293
2358
  pub fn safeguard(battle: anytype, player: Player, _: *State, options: anytype) !void {
@@ -2311,11 +2376,13 @@ pub const Effects = struct {
2311
2376
  ) !void {
2312
2377
  var side = battle.side(player);
2313
2378
 
2314
- if (@field(side.conditions, key)) return options.log.fail(battle.active(player), .None);
2379
+ if (@field(side.conditions, key)) {
2380
+ return options.log.fail(.{ battle.active(player), .None });
2381
+ }
2315
2382
 
2316
2383
  @field(side.conditions, key) = true;
2317
2384
  @field(side.conditions, value) = 5;
2318
- try options.log.sidestart(player, @field(protocol.Side, key));
2385
+ try options.log.sidestart(.{ player, @field(protocol.Side, key) });
2319
2386
  }
2320
2387
 
2321
2388
  pub fn sketch(battle: anytype, player: Player, _: *State, options: anytype) !void {
@@ -2326,9 +2393,9 @@ pub const Effects = struct {
2326
2393
 
2327
2394
  if (showdown) {
2328
2395
  // yes, actually "Splash". '|nothing' gets blindly mapped to '|-activate||move:Splash'
2329
- try options.log.activate(battle.active(player), .Splash);
2396
+ try options.log.activate(.{ battle.active(player), .Splash });
2330
2397
  } else {
2331
- try options.log.fail(battle.active(player), .None);
2398
+ try options.log.fail(.{ battle.active(player), .None });
2332
2399
  }
2333
2400
  }
2334
2401
 
@@ -2338,14 +2405,14 @@ pub const Effects = struct {
2338
2405
  const foe_ident = battle.active(player.foe());
2339
2406
 
2340
2407
  if (Status.any(foe_stored.status)) {
2341
- return options.log.fail(
2408
+ return options.log.fail(.{
2342
2409
  foe_ident,
2343
- if (Status.is(foe_stored.status, .SLP)) .Sleep else .None,
2344
- );
2410
+ if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
2411
+ });
2345
2412
  }
2346
2413
 
2347
2414
  if (foe.active.volatiles.Substitute) {
2348
- return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
2415
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
2349
2416
  }
2350
2417
 
2351
2418
  // Sleep Clause Mod
@@ -2356,22 +2423,21 @@ pub const Effects = struct {
2356
2423
  }
2357
2424
 
2358
2425
  foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2359
- try options.log.statusFrom(foe_ident, foe_stored.status, state.move);
2426
+ try options.log.status(.{ foe_ident, foe_stored.status, .From, state.move });
2360
2427
  }
2361
2428
 
2362
2429
  pub fn sleepTalk(battle: anytype, player: Player, state: *State, options: anytype) !void {
2363
2430
  const side = battle.side(player);
2364
2431
 
2365
2432
  if (!Status.is(side.stored().status, .SLP)) {
2366
- if (!showdown) try options.log.fail(battle.active(player), .None);
2367
- return;
2433
+ return if (!showdown) try options.log.fail(.{ battle.active(player), .None });
2368
2434
  }
2369
2435
 
2370
2436
  var n: u3 = 0;
2371
2437
  for (side.active.moves, 0..) |m, i| {
2372
2438
  if (state.mslot - 1 != i and Move.get(m.id).extra.sleep_talk) n += 1;
2373
2439
  }
2374
- if (n == 0) return options.log.fail(battle.active(player), .None);
2440
+ if (n == 0) return options.log.fail(.{ battle.active(player), .None });
2375
2441
 
2376
2442
  _ = try Rolls.sleepTalk(battle, player, n, state.mslot, options); // TODO
2377
2443
  }
@@ -2380,55 +2446,75 @@ pub const Effects = struct {
2380
2446
  if (Status.is(battle.side(player).stored().status, .SLP)) return true;
2381
2447
  state.damage = 0;
2382
2448
  state.miss = true; // TODO is this necessary?
2383
- if (!showdown) try options.log.fail(battle.active(player), .None);
2449
+ if (!showdown) try options.log.fail(.{ battle.active(player), .None });
2384
2450
  return false;
2385
2451
  }
2386
2452
 
2387
2453
  pub fn spikes(battle: anytype, player: Player, _: *State, options: anytype) !void {
2388
2454
  var foe = battle.foe(player);
2389
- if (foe.conditions.Spikes) return options.log.fail(battle.active(player), .None);
2455
+ if (foe.conditions.Spikes) return options.log.fail(.{ battle.active(player), .None });
2390
2456
 
2391
2457
  foe.conditions.Spikes = true;
2392
- try options.log.sidestart(player.foe(), .Spikes);
2458
+ try options.log.sidestart(.{ player.foe(), protocol.Side.Spikes });
2393
2459
  }
2394
2460
 
2395
2461
  pub fn spite(battle: anytype, player: Player, state: *State, options: anytype) !void {
2396
- _ = .{ battle, player, state, options }; // TODO
2397
- // var foe = battle.foe(player);
2462
+ var foe = battle.foe(player);
2398
2463
 
2399
- // if (state.miss) return options.log.fail(battle.active(player), .None);
2464
+ const last = foe.lastMove(false);
2465
+ if (state.miss or last == .None or last == .Struggle) {
2466
+ return options.log.fail(.{ battle.active(player.foe()), .None });
2467
+ }
2400
2468
 
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
- // }
2469
+ var slot: u4 = 0;
2470
+ for (foe.active.moves, 0..) |m, i| {
2471
+ if (m.id == last) slot = @intCast(i + 1);
2472
+ }
2473
+ // If the move was called via Metronome it might not exist
2474
+ if (slot == 0 or foe.active.move(slot).pp == 0) {
2475
+ return options.log.fail(.{ battle.active(player.foe()), .None });
2476
+ }
2477
+
2478
+ // Pokémon Showdown always deducts 4 instead of randomizing
2479
+ const pp = @min(
2480
+ foe.active.move(slot).pp,
2481
+ if (showdown) 4 else try Rolls.spite(battle, player, options),
2482
+ );
2483
+ foe.active.move(slot).pp -= pp;
2484
+ if (!foe.active.volatiles.Transform) {
2485
+ var move_slot = foe.stored().move(slot);
2486
+ assert(move_slot.pp > 0);
2487
+
2488
+ move_slot.pp = @as(u6, @intCast(move_slot.pp)) -% 1;
2489
+ assert(foe.active.move(slot).pp == foe.stored().move(slot).pp);
2490
+ }
2491
+
2492
+ try options.log.activate(.{ battle.active(player.foe()), .Spite, last, pp });
2407
2493
  }
2408
2494
 
2409
2495
  pub fn splash(battle: anytype, player: Player, _: *State, options: anytype) !void {
2410
- try options.log.activate(battle.active(player), .Splash);
2496
+ try options.log.activate(.{ battle.active(player), .Splash });
2411
2497
  }
2412
2498
 
2413
2499
  pub fn substitute(battle: anytype, player: Player, _: *State, options: anytype) !void {
2414
2500
  var side = battle.side(player);
2415
2501
  if (side.active.volatiles.Substitute) {
2416
- return options.log.fail(battle.active(player), .Substitute);
2502
+ return options.log.fail(.{ battle.active(player), .Substitute });
2417
2503
  }
2418
2504
 
2419
2505
  assert(side.stored().stats.hp <= 1023);
2420
2506
  const hp: u8 = @intCast(side.stored().stats.hp / 4);
2421
2507
 
2422
2508
  if (hp == 0 or side.stored().hp <= hp) {
2423
- return options.log.fail(battle.active(player), .Weak);
2509
+ return options.log.fail(.{ battle.active(player), .Weak });
2424
2510
  }
2425
2511
 
2426
2512
  side.stored().hp -= hp;
2427
2513
  side.active.volatiles.substitute = hp;
2428
2514
  side.active.volatiles.Substitute = true;
2429
2515
  side.active.volatiles.bind = .{};
2430
- try options.log.start(battle.active(player), .Substitute);
2431
- try options.log.damage(battle.active(player), side.stored(), .None);
2516
+ try options.log.start(.{ battle.active(player), .Substitute });
2517
+ try options.log.damage(.{ battle.active(player), side.stored(), .None });
2432
2518
  }
2433
2519
 
2434
2520
  pub fn sunnyDay(battle: anytype, _: Player, _: *State, options: anytype) !void {
@@ -2438,14 +2524,14 @@ pub const Effects = struct {
2438
2524
  fn weather(battle: anytype, w: Weather, options: anytype) !void {
2439
2525
  battle.field.weather = w;
2440
2526
  battle.field.weather_duration = 5;
2441
- try options.log.weather(w, .None);
2527
+ try options.log.weather(.{ w, protocol.Weather.None });
2442
2528
  }
2443
2529
 
2444
2530
  pub fn teleport(battle: anytype, player: Player, _: *State, options: anytype) !void {
2445
2531
  if (showdown) return;
2446
2532
 
2447
- try options.log.fail(battle.active(player), .None);
2448
- try options.log.laststill();
2533
+ try options.log.fail(.{ battle.active(player), .None });
2534
+ try options.log.laststill(.{});
2449
2535
  }
2450
2536
 
2451
2537
  pub fn thief(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -2460,11 +2546,11 @@ pub const Effects = struct {
2460
2546
  side.stored().item = foe.stored().item;
2461
2547
  foe.stored().item = .None;
2462
2548
 
2463
- try options.log.item(
2549
+ try options.log.item(.{
2464
2550
  battle.active(player),
2465
2551
  side.stored().item,
2466
2552
  battle.active(player.foe()),
2467
- );
2553
+ });
2468
2554
  }
2469
2555
 
2470
2556
  pub fn thrashing(battle: anytype, player: Player, _: *State, options: anytype) !void {
@@ -2486,11 +2572,11 @@ pub const Effects = struct {
2486
2572
  side.active.volatiles.dirty = false;
2487
2573
 
2488
2574
  if (foe.active.volatiles.Transform) {
2489
- return options.log.fail(battle.active(player), .None);
2575
+ return options.log.fail(.{ battle.active(player), .None });
2490
2576
  } 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));
2577
+ if (!showdown) return options.log.fail(.{ battle.active(player), .None });
2578
+ try options.log.lastmiss(.{});
2579
+ return options.log.miss(.{battle.active(player)});
2494
2580
  }
2495
2581
 
2496
2582
  side.active.volatiles.Transform = true;
@@ -2512,7 +2598,7 @@ pub const Effects = struct {
2512
2598
  side.active.moves[i].pp = if (m.id != .None) if (m.id == .Sketch) 1 else 5 else 0;
2513
2599
  }
2514
2600
 
2515
- try options.log.transform(battle.active(player), foe_ident);
2601
+ try options.log.transform(.{ battle.active(player), foe_ident });
2516
2602
  }
2517
2603
 
2518
2604
  pub fn triAttack(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -2537,17 +2623,17 @@ pub const Effects = struct {
2537
2623
  pub fn allStatUpChance(battle: anytype, player: Player, state: *State, options: anytype) !void {
2538
2624
  var side = battle.side(player);
2539
2625
 
2540
- var stats = &side.active.stats;
2541
- var boosts = &side.active.boosts;
2626
+ const stats = &side.active.stats;
2627
+ const boosts = &side.active.boosts;
2542
2628
 
2543
2629
  if (!state.proc) return;
2544
2630
  inline for (STATS, STAT_UP_REASONS) |s, r| {
2545
2631
  assert(@field(boosts, s) >= -6 and @field(boosts, s) <= 6);
2546
2632
  if (@field(boosts, s) == 6 or @field(stats, s) == MAX_STAT_VALUE) {
2547
- try options.log.boost(battle.active(player), r, 0);
2633
+ try options.log.boost(.{ battle.active(player), r, 0 });
2548
2634
  } else {
2549
2635
  try boostStat(battle, side, stats, boosts, s, 1);
2550
- try options.log.boost(battle.active(player), r, 1);
2636
+ try options.log.boost(.{ battle.active(player), r, 1 });
2551
2637
  }
2552
2638
  }
2553
2639
  }
@@ -2557,51 +2643,61 @@ pub const Effects = struct {
2557
2643
  var side = battle.side(player);
2558
2644
  const ident = battle.active(player);
2559
2645
 
2560
- if (state.miss) return log.fail(ident, .None);
2646
+ if (state.miss) return log.fail(.{ ident, .None });
2561
2647
 
2562
- var stats = &side.active.stats;
2648
+ const stats = &side.active.stats;
2563
2649
  var boosts = &side.active.boosts;
2564
2650
 
2565
2651
  const move = Move.get(state.move);
2566
2652
  switch (move.effect) {
2567
2653
  .AttackUp1, .AttackUp2, .Rage, .Swagger => {
2568
2654
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2569
- if (boosts.atk == 6 or stats.atk == MAX_STAT_VALUE) return log.fail(ident, .None);
2655
+ if (boosts.atk == 6 or stats.atk == MAX_STAT_VALUE) {
2656
+ return log.fail(.{ ident, .None });
2657
+ }
2570
2658
  const n: u2 = if (move.effect == .AttackUp2 or move.effect == .Swagger) 2 else 1;
2571
2659
  try boostStat(battle, side, stats, boosts, "atk", n);
2572
2660
  const reason = if (showdown and move.effect == .Rage) Boost.Rage else Boost.Attack;
2573
- try log.boost(ident, reason, n);
2661
+ try log.boost(.{ ident, reason, n });
2574
2662
  },
2575
2663
  .DefenseUp1, .DefenseUp2, .SkullBash => {
2576
2664
  assert(boosts.def >= -6 and boosts.def <= 6);
2577
- if (boosts.def == 6 or stats.def == MAX_STAT_VALUE) return log.fail(ident, .None);
2665
+ if (boosts.def == 6 or stats.def == MAX_STAT_VALUE) {
2666
+ return log.fail(.{ ident, .None });
2667
+ }
2578
2668
  const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
2579
2669
  try boostStat(battle, side, stats, boosts, "def", n);
2580
- try log.boost(ident, .Defense, n);
2670
+ try log.boost(.{ ident, .Defense, n });
2581
2671
  },
2582
2672
  .SpeedUp2 => {
2583
2673
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2584
- if (boosts.spe == 6 or stats.spe == MAX_STAT_VALUE) return log.fail(ident, .None);
2674
+ if (boosts.spe == 6 or stats.spe == MAX_STAT_VALUE) {
2675
+ return log.fail(.{ ident, .None });
2676
+ }
2585
2677
  try boostStat(battle, side, stats, boosts, "spe", 2);
2586
- try log.boost(ident, .Speed, 2);
2678
+ try log.boost(.{ ident, .Speed, 2 });
2587
2679
  },
2588
2680
  .SpAtkUp1 => {
2589
2681
  assert(boosts.spa >= -6 and boosts.spa <= 6);
2590
- if (boosts.spa == 6 or stats.spa == MAX_STAT_VALUE) return log.fail(ident, .None);
2682
+ if (boosts.spa == 6 or stats.spa == MAX_STAT_VALUE) {
2683
+ return log.fail(.{ ident, .None });
2684
+ }
2591
2685
  try boostStat(battle, side, stats, boosts, "spa", 1);
2592
- try log.boost(ident, .SpecialAttack, 1);
2686
+ try log.boost(.{ ident, .SpecialAttack, 1 });
2593
2687
  },
2594
2688
  .SpDefUp2 => {
2595
2689
  assert(boosts.spd >= -6 and boosts.spd <= 6);
2596
- if (boosts.spd == 6 or stats.spd == MAX_STAT_VALUE) return log.fail(ident, .None);
2690
+ if (boosts.spd == 6 or stats.spd == MAX_STAT_VALUE) {
2691
+ return log.fail(.{ ident, .None });
2692
+ }
2597
2693
  try boostStat(battle, side, stats, boosts, "spd", 2);
2598
- try log.boost(ident, .SpecialDefense, 2);
2694
+ try log.boost(.{ ident, .SpecialDefense, 2 });
2599
2695
  },
2600
2696
  .EvasionUp1 => {
2601
2697
  assert(boosts.evasion >= -6 and boosts.evasion <= 6);
2602
- if (boosts.evasion == 6) return log.fail(ident, .None);
2698
+ if (boosts.evasion == 6) return log.fail(.{ ident, .None });
2603
2699
  boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
2604
- try log.boost(ident, .Evasion, 1);
2700
+ try log.boost(.{ ident, .Evasion, 1 });
2605
2701
  if (state.move == .Minimize) side.active.volatiles.minimized = true;
2606
2702
  },
2607
2703
  else => unreachable,
@@ -2614,79 +2710,83 @@ pub const Effects = struct {
2614
2710
  const foe_ident = battle.active(player.foe());
2615
2711
 
2616
2712
  if (foe.active.volatiles.Mist) {
2617
- try log.activate(foe_ident, .Mist);
2618
- return log.fail(foe_ident, .None);
2713
+ try log.activate(.{ foe_ident, .Mist });
2714
+ return log.fail(.{ foe_ident, .None });
2619
2715
  }
2620
2716
 
2621
2717
  const fail = foe.active.volatiles.Substitute or state.miss or !state.proc;
2622
2718
 
2623
- var stats = &foe.active.stats;
2719
+ const stats = &foe.active.stats;
2624
2720
  var boosts = &foe.active.boosts;
2625
2721
 
2626
2722
  const move = Move.get(state.move);
2723
+ const none: Fail = .None;
2627
2724
  switch (move.effect) {
2628
2725
  .AttackDown1, .AttackDown2, .AttackDownChance => {
2629
2726
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2630
2727
  if (fail or boosts.atk == -6) {
2631
- return if (move.effect != .AttackDownChance) try log.fail(foe_ident, .None);
2728
+ return if (move.effect != .AttackDownChance) try log.fail(.{ foe_ident, none });
2632
2729
  } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2633
- try options.log.lastmiss();
2634
- return options.log.miss(battle.active(player));
2730
+ try options.log.lastmiss(.{});
2731
+ return options.log.miss(.{battle.active(player)});
2635
2732
  } else if (stats.atk == 1) {
2636
- return if (move.effect != .AttackDownChance) try log.fail(foe_ident, .None);
2733
+ return if (move.effect != .AttackDownChance) try log.fail(.{ foe_ident, none });
2637
2734
  }
2638
2735
  const n: u2 = if (move.effect == .AttackDown2) 2 else 1;
2639
2736
  try unboostStat(battle, foe, stats, boosts, "atk", n);
2640
- try log.boost(foe_ident, .Attack, -@as(i8, n));
2737
+ try log.boost(.{ foe_ident, .Attack, -@as(i8, n) });
2641
2738
  },
2642
2739
  .DefenseDown1, .DefenseDown2, .DefenseDownChance => {
2643
2740
  assert(boosts.def >= -6 and boosts.def <= 6);
2644
2741
  if (fail or boosts.def == -6) {
2645
- return if (move.effect != .DefenseDownChance) try log.fail(foe_ident, .None);
2742
+ if (move.effect != .DefenseDownChance) try log.fail(.{ foe_ident, none });
2743
+ return;
2646
2744
  } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2647
- try options.log.lastmiss();
2648
- return options.log.miss(battle.active(player));
2745
+ try options.log.lastmiss(.{});
2746
+ return options.log.miss(.{battle.active(player)});
2649
2747
  } else if (stats.def == 1) {
2650
- return if (move.effect != .DefenseDownChance) try log.fail(foe_ident, .None);
2748
+ if (move.effect != .DefenseDownChance) try log.fail(.{ foe_ident, none });
2749
+ return;
2651
2750
  }
2652
2751
  const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
2653
2752
  try unboostStat(battle, foe, stats, boosts, "def", n);
2654
- try log.boost(foe_ident, .Defense, -@as(i8, n));
2753
+ try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
2655
2754
  },
2656
2755
  .SpeedDown1, .SpeedDown2, .SpeedDownChance => {
2657
2756
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2658
2757
  if (fail or boosts.spe == -6) {
2659
- return if (move.effect != .SpeedDownChance) try log.fail(foe_ident, .None);
2758
+ return if (move.effect != .SpeedDownChance) try log.fail(.{ foe_ident, none });
2660
2759
  } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2661
- try options.log.lastmiss();
2662
- return options.log.miss(battle.active(player));
2760
+ try options.log.lastmiss(.{});
2761
+ return options.log.miss(.{battle.active(player)});
2663
2762
  } else if (stats.spe == 1) {
2664
- return if (move.effect != .SpeedDownChance) try log.fail(foe_ident, .None);
2763
+ return if (move.effect != .SpeedDownChance) try log.fail(.{ foe_ident, none });
2665
2764
  }
2666
2765
  const n: u2 = if (move.effect == .SpeedDown2) 2 else 1;
2667
2766
  try unboostStat(battle, foe, stats, boosts, "spe", n);
2668
- try log.boost(foe_ident, .Speed, -@as(i8, n));
2767
+ try log.boost(.{ foe_ident, .Speed, -@as(i8, n) });
2669
2768
  },
2670
2769
  .SpDefDownChance => {
2671
2770
  assert(boosts.spd >= -6 and boosts.spd <= 6);
2672
2771
  if (fail or boosts.spd == -6) return;
2673
2772
  if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2674
- try options.log.lastmiss();
2675
- return options.log.miss(battle.active(player));
2773
+ try options.log.lastmiss(.{});
2774
+ return options.log.miss(.{battle.active(player)});
2676
2775
  } else if (stats.spd == 1) return;
2677
2776
  try unboostStat(battle, foe, stats, boosts, "spd", 1);
2678
- try log.boost(foe_ident, .SpecialDefense, -1);
2777
+ try log.boost(.{ foe_ident, .SpecialDefense, -1 });
2679
2778
  },
2680
2779
  .AccuracyDown1, .AccuracyDownChance => {
2681
2780
  assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
2682
2781
  if (fail or boosts.accuracy == -6) {
2683
- return if (move.effect != .AccuracyDownChance) try log.fail(foe_ident, .None);
2782
+ if (move.effect != .AccuracyDownChance) try log.fail(.{ foe_ident, none });
2783
+ return;
2684
2784
  } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2685
- try options.log.lastmiss();
2686
- return options.log.miss(battle.active(player));
2785
+ try options.log.lastmiss(.{});
2786
+ return options.log.miss(.{battle.active(player)});
2687
2787
  }
2688
2788
  boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
2689
- try log.boost(foe_ident, .Accuracy, -1);
2789
+ try log.boost(.{ foe_ident, .Accuracy, -1 });
2690
2790
  },
2691
2791
  else => unreachable,
2692
2792
  }
@@ -2700,9 +2800,9 @@ fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
2700
2800
  }
2701
2801
 
2702
2802
  fn recomputeStats(battle: anytype, side: *Side) void {
2703
- var stats = unmodifiedStats(battle, side);
2803
+ const stats = unmodifiedStats(battle, side);
2704
2804
  inline for (STATS) |s| {
2705
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, @field(side.active.boosts, s)) + 6))];
2805
+ const mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, @field(side.active.boosts, s)) + 6))];
2706
2806
  const val = @field(stats, s) * mod[0] / mod[1];
2707
2807
  @field(side.active.stats, s) = @max(1, @min(MAX_STAT_VALUE, val));
2708
2808
  }
@@ -2725,7 +2825,7 @@ fn boostStat(
2725
2825
  n: u2,
2726
2826
  ) !void {
2727
2827
  @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))];
2828
+ const mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, @field(boosts, s)) + 6))];
2729
2829
  const stat = @field(unmodifiedStats(battle, side), s);
2730
2830
  @field(stats, s) = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2731
2831
  if (comptime std.mem.eql(u8, s, "atk")) {
@@ -2748,7 +2848,7 @@ fn unboostStat(
2748
2848
  n: u2,
2749
2849
  ) !void {
2750
2850
  @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))];
2851
+ const mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, @field(boosts, s)) + 6))];
2752
2852
  const stat = @field(unmodifiedStats(battle, side), s);
2753
2853
  @field(stats, s) = @max(1, stat * mod[0] / mod[1]);
2754
2854
  if (comptime std.mem.eql(u8, s, "atk")) {
@@ -2995,13 +3095,12 @@ pub const Rolls = struct {
2995
3095
  return hits;
2996
3096
  }
2997
3097
 
2998
- fn spite(battle: anytype, player: Player, options: anytype) !u8 {
2999
- const pp = if (options.calc.overridden(player, .spite)) |val|
3098
+ fn spite(battle: anytype, player: Player, options: anytype) !u3 {
3099
+ assert(!showdown);
3100
+ const pp: u3 = if (options.calc.overridden(player, .spite)) |val|
3000
3101
  val + 1
3001
- else if (showdown)
3002
- battle.rng.range(u8, 2, 6)
3003
3102
  else
3004
- (battle.rng.next() & 3) + 2;
3103
+ @intCast((battle.rng.next() & 3) + 2);
3005
3104
 
3006
3105
  assert(pp >= 2 and pp <= 5);
3007
3106
  try options.chance.spite(player, pp);
@@ -3226,7 +3325,7 @@ pub const Rolls = struct {
3226
3325
 
3227
3326
  const move: Move = overridden orelse move: {
3228
3327
  if (showdown) {
3229
- var r = battle.rng.range(u8, 0, @intCast(Move.METRONOME.len - n));
3328
+ var r = battle.rng.range(u8, 0, @as(u8, @intCast(Move.METRONOME.len - n)));
3230
3329
  for (moves) |m| {
3231
3330
  if (m.id == .None) break;
3232
3331
  if (@intFromEnum(m.id) > @intFromEnum(Move.METRONOME[r])) break;
@@ -3449,7 +3548,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3449
3548
  .Move => {
3450
3549
  const side = battle.side(player);
3451
3550
 
3452
- var active = &side.active;
3551
+ const active = &side.active;
3453
3552
  const stored = side.stored();
3454
3553
 
3455
3554
  // While players are not given any input options on the cartridge in these cases,