@pkmn/engine 0.1.0-dev.1220fbc5 → 0.1.0-dev.12e7292c

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,16 +1274,12 @@ 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
  }
1261
1281
 
1262
1282
  pub const Effects = struct {
1263
- pub fn alwaysHit(battle: anytype, player: Player, state: *State, options: anytype) !void {
1264
- _ = .{ battle, player, state, options }; // TODO
1265
- }
1266
-
1267
1283
  pub fn attract(battle: anytype, player: Player, _: *State, options: anytype) !void {
1268
1284
  const side = battle.side(player);
1269
1285
  var foe = battle.foe(player);
@@ -1271,16 +1287,14 @@ pub const Effects = struct {
1271
1287
  const gender: u8 = @intFromEnum(side.stored().dvs.gender);
1272
1288
  const foe_gender: u8 = @intFromEnum(foe.stored().dvs.gender);
1273
1289
  if (gender + foe_gender != 1) {
1274
- try options.log.immune(battle.active(player.foe()), .None);
1275
- return;
1290
+ return options.log.immune(.{ battle.active(player.foe()), .None });
1276
1291
  } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
1277
- try options.log.lastmiss();
1278
- try options.log.miss(battle.active(player));
1279
- return;
1292
+ try options.log.lastmiss(.{});
1293
+ return options.log.miss(.{battle.active(player)});
1280
1294
  }
1281
1295
 
1282
1296
  foe.active.volatiles.Attract = true;
1283
- try options.log.start(battle.active(player.foe()), .Attract);
1297
+ try options.log.start(.{ battle.active(player.foe()), .Attract });
1284
1298
  }
1285
1299
 
1286
1300
  pub fn batonPass(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1293,6 +1307,13 @@ pub const Effects = struct {
1293
1307
 
1294
1308
  pub fn bellyDrum(battle: anytype, player: Player, state: *State, options: anytype) !void {
1295
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
+ //
1296
1317
  }
1297
1318
 
1298
1319
  pub const bide = struct {
@@ -1312,10 +1333,10 @@ pub const Effects = struct {
1312
1333
  // }
1313
1334
  try options.chance.attacking(player, volatiles.attacks);
1314
1335
 
1315
- if (volatiles.attacks != 0) return options.log.activate(ident, .Bide);
1336
+ if (volatiles.attacks != 0) return options.log.activate(.{ ident, .Bide });
1316
1337
 
1317
1338
  volatiles.Bide = false;
1318
- try options.log.end(ident, .Bide);
1339
+ try options.log.end(.{ ident, .Bide });
1319
1340
 
1320
1341
  state.damage = volatiles.bide *% 2;
1321
1342
  volatiles.bide = 0;
@@ -1329,7 +1350,16 @@ pub const Effects = struct {
1329
1350
  };
1330
1351
 
1331
1352
  pub fn binding(battle: anytype, player: Player, state: *State, options: anytype) !void {
1332
- _ = .{ 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
+ };
1333
1363
  }
1334
1364
 
1335
1365
  pub fn burnChance(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1339,12 +1369,12 @@ pub const Effects = struct {
1339
1369
  if (foe.active.volatiles.Substitute) return;
1340
1370
  if (Status.any(foe_stored.status)) {
1341
1371
  if (Status.is(foe_stored.status, .FRZ)) {
1342
- assert(Move.get(state.move).type == .Fire);
1343
- try options.log.curestatus(
1372
+ assert(Move.get(state.move).type == .Fire or state.move == .TriAttack);
1373
+ try options.log.curestatus(.{
1344
1374
  battle.active(player.foe()),
1345
1375
  foe_stored.status,
1346
1376
  .Message,
1347
- );
1377
+ });
1348
1378
  foe_stored.status = 0;
1349
1379
  }
1350
1380
  return;
@@ -1353,11 +1383,14 @@ pub const Effects = struct {
1353
1383
  if (foe.conditions.Safeguard) return;
1354
1384
  // Nothing is immune to Fire-type, but Ghosts are immune to burnChance called by Tri Attack
1355
1385
  if (state.immune()) return;
1356
- if (foe.active.types.includes(Move.get(state.move).type)) return;
1386
+ if (foe.active.types.includes(Move.get(state.move).type) and state.move != .TriAttack) {
1387
+ return;
1388
+ }
1357
1389
 
1358
1390
  foe_stored.status = Status.init(.BRN);
1359
1391
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
1360
- 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 });
1361
1394
 
1362
1395
  // TODO: check for status berry
1363
1396
  }
@@ -1377,10 +1410,7 @@ pub const Effects = struct {
1377
1410
  for (side.active.moves) |m| {
1378
1411
  if (convertible(side.active, m.id)) n += 1;
1379
1412
  }
1380
- if (n == 0) {
1381
- try options.log.fail(battle.active(player), .None);
1382
- return;
1383
- }
1413
+ if (n == 0) return options.log.fail(.{ battle.active(player), .None });
1384
1414
 
1385
1415
  side.active.types.type1 = try Rolls.conversion(battle, player, n, options);
1386
1416
  side.active.types.type2 = side.active.types.type1;
@@ -1390,26 +1420,87 @@ pub const Effects = struct {
1390
1420
  var side = battle.side(player);
1391
1421
 
1392
1422
  const last = battle.foe(player).lastMove(false);
1393
- if (last == .None) {
1394
- try options.log.fail(battle.active(player), .None);
1395
- return;
1396
- }
1423
+ if (last == .None) return options.log.fail(.{ battle.active(player), .None });
1397
1424
  const move = Move.get(last);
1398
- if (move.type == .@"???") {
1399
- try options.log.fail(battle.active(player), .None);
1400
- return;
1401
- }
1425
+ if (move.type == .@"???") return options.log.fail(.{ battle.active(player), .None });
1402
1426
 
1403
1427
  side.active.types.type1 = try Rolls.conversion2(battle, player, move.type, options);
1404
1428
  side.active.types.type2 = side.active.types.type1;
1405
1429
  }
1406
1430
 
1407
- pub fn counter(battle: anytype, player: Player, state: *State, options: anytype) !void {
1408
- _ = .{ 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;
1409
1456
  }
1410
1457
 
1411
1458
  pub fn curse(battle: anytype, player: Player, state: *State, options: anytype) !void {
1412
- _ = .{ 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
+ }
1413
1504
  }
1414
1505
 
1415
1506
  pub fn defenseCurl(battle: anytype, player: Player, _: *State, _: anytype) !void {
@@ -1419,14 +1510,13 @@ pub const Effects = struct {
1419
1510
  pub fn defrost(battle: anytype, player: Player, _: *State, options: anytype) !void {
1420
1511
  var side = battle.side(player);
1421
1512
  assert(Status.is(side.stored().status, .FRZ));
1422
-
1423
- try options.log.curestatus(battle.active(player), side.stored().status, .Message);
1513
+ try options.log.curestatus(.{ battle.active(player), side.stored().status, .Message });
1424
1514
  side.stored().status = 0;
1425
1515
  }
1426
1516
 
1427
1517
  pub fn destinyBond(battle: anytype, player: Player, _: *State, options: anytype) !void {
1428
1518
  battle.side(player).active.volatiles.DestinyBond = true;
1429
- try options.log.singlemove(battle.active(player), Move.DestinyBond);
1519
+ try options.log.singlemove(.{ battle.active(player), Move.DestinyBond });
1430
1520
  }
1431
1521
 
1432
1522
  pub fn disable(battle: anytype, player: Player, _: *State, options: anytype) !void {
@@ -1434,16 +1524,10 @@ pub const Effects = struct {
1434
1524
  var volatiles = &foe.active.volatiles;
1435
1525
  const foe_ident = battle.active(player.foe());
1436
1526
 
1437
- if (volatiles.disable.move != 0) {
1438
- try options.log.fail(foe_ident, .None);
1439
- return;
1440
- }
1527
+ if (volatiles.disable.move != 0) return options.log.fail(.{ foe_ident, .None });
1441
1528
 
1442
1529
  const last = battle.foe(player).lastMove(false);
1443
- if (last == .None or last == .Struggle) {
1444
- try options.log.fail(foe_ident, .None);
1445
- return;
1446
- }
1530
+ if (last == .None or last == .Struggle) return options.log.fail(.{ foe_ident, .None });
1447
1531
 
1448
1532
  var slot: u3 = 0;
1449
1533
  for (foe.active.moves) |m| {
@@ -1452,15 +1536,12 @@ pub const Effects = struct {
1452
1536
  if (m.id == last) break;
1453
1537
  }
1454
1538
  const move = foe.active.move(slot);
1455
- if (move.pp == 0) {
1456
- try options.log.fail(foe_ident, .None);
1457
- return;
1458
- }
1539
+ if (move.pp == 0) return options.log.fail(.{ foe_ident, .None });
1459
1540
 
1460
1541
  volatiles.disable.move = slot;
1461
1542
  volatiles.disable.duration = Rolls.disableDuration(battle, player, options);
1462
1543
 
1463
- try options.log.startEffect(foe_ident, .Disable, move.id);
1544
+ try options.log.start(.{ foe_ident, .Disable, move.id });
1464
1545
  }
1465
1546
 
1466
1547
  pub const double = struct {
@@ -1479,13 +1560,14 @@ pub const Effects = struct {
1479
1560
 
1480
1561
  pub fn drainHP(battle: anytype, player: Player, state: *State, options: anytype) !void {
1481
1562
  var stored = battle.side(player).stored();
1563
+ const ident = battle.active(player);
1482
1564
 
1483
1565
  assert(state.damage != 0);
1484
1566
 
1485
1567
  if (stored.hp == stored.stats.hp) return;
1486
1568
  stored.hp = @min(stored.stats.hp, stored.hp + @max(state.damage / 2, 1));
1487
1569
 
1488
- try options.log.drain(battle.active(player), stored, battle.active(player.foe()));
1570
+ try options.log.heal(.{ ident, stored, .Drain, battle.active(player.foe()) });
1489
1571
  }
1490
1572
 
1491
1573
  pub fn encore(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1497,10 +1579,7 @@ pub const Effects = struct {
1497
1579
  const failed = volatiles.Encore or
1498
1580
  (last == .None or last == .Struggle or
1499
1581
  last == .Encore or .last == .MirrorMove);
1500
- if (failed) {
1501
- try options.log.fail(foe_ident, .None);
1502
- return;
1503
- }
1582
+ if (failed) return options.log.fail(.{ foe_ident, .None });
1504
1583
 
1505
1584
  var slot: u3 = 0;
1506
1585
  for (foe.active.moves) |m| {
@@ -1509,10 +1588,7 @@ pub const Effects = struct {
1509
1588
  if (m.id == last) break;
1510
1589
  }
1511
1590
  const move = foe.active.move(slot);
1512
- if (move.pp == 0) {
1513
- try options.log.fail(foe_ident, .None);
1514
- return;
1515
- }
1591
+ if (move.pp == 0) return options.log.fail(.{ foe_ident, .None });
1516
1592
 
1517
1593
  volatiles.Encore = true;
1518
1594
  volatiles.encore = Rolls.encoreDuration(battle, player, options);
@@ -1520,7 +1596,7 @@ pub const Effects = struct {
1520
1596
  // TODO ???
1521
1597
  _ = state;
1522
1598
 
1523
- try options.log.startEffect(foe_ident, .Encore, move.id);
1599
+ try options.log.start(.{ foe_ident, .Encore, move.id });
1524
1600
  }
1525
1601
 
1526
1602
  pub fn explode(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1552,10 +1628,6 @@ pub const Effects = struct {
1552
1628
  };
1553
1629
  }
1554
1630
 
1555
- pub fn flameWheel(battle: anytype, player: Player, state: *State, options: anytype) !void {
1556
- _ = .{ battle, player, state, options }; // TODO
1557
- }
1558
-
1559
1631
  pub fn flinchChance(battle: anytype, player: Player, state: *State, _: anytype) !void {
1560
1632
  const foe = battle.foe(player);
1561
1633
  const status = foe.stored().status;
@@ -1578,12 +1650,11 @@ pub const Effects = struct {
1578
1650
  var side = battle.side(player);
1579
1651
 
1580
1652
  if (side.active.volatiles.FocusEnergy) {
1581
- if (!showdown) try options.log.fail(battle.active(player), .None);
1582
- return;
1653
+ return if (!showdown) try options.log.fail(.{ battle.active(player), .None });
1583
1654
  }
1584
1655
 
1585
1656
  side.active.volatiles.FocusEnergy = true;
1586
- try options.log.start(battle.active(player), .FocusEnergy);
1657
+ try options.log.start(.{ battle.active(player), .FocusEnergy });
1587
1658
  }
1588
1659
 
1589
1660
  var SLOTS: [5]u4 = .{ 0, 0, 0, 0, 0 };
@@ -1593,7 +1664,7 @@ pub const Effects = struct {
1593
1664
 
1594
1665
  const foe = battle.foe(player);
1595
1666
 
1596
- if (state.first) return options.log.fail(battle.active(player), .None);
1667
+ if (state.first) return options.log.fail(.{ battle.active(player), .None });
1597
1668
 
1598
1669
  var n: u4 = 0;
1599
1670
  var i: u4 = 0;
@@ -1607,10 +1678,7 @@ pub const Effects = struct {
1607
1678
  }
1608
1679
  }
1609
1680
 
1610
- if (n == 0) {
1611
- try options.log.fail(battle.active(player), .None);
1612
- return;
1613
- }
1681
+ if (n == 0) return options.log.fail(.{ battle.active(player), .None });
1614
1682
 
1615
1683
  const slot = try Rolls.forceSwitch(battle, player, SLOTS[0..i], n, options);
1616
1684
  try switchIn(battle, player.foe(), slot, .Drag, options);
@@ -1620,16 +1688,14 @@ pub const Effects = struct {
1620
1688
  var foe = battle.foe(player);
1621
1689
 
1622
1690
  if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
1623
- try options.log.lastmiss();
1624
- try options.log.miss(battle.active(player));
1625
- return;
1691
+ try options.log.lastmiss(.{});
1692
+ return options.log.miss(.{battle.active(player)});
1626
1693
  } else if (foe.active.volatiles.Foresight) {
1627
- try options.log.fail(battle.active(player), .None);
1628
- return;
1694
+ return options.log.fail(.{ battle.active(player), .None });
1629
1695
  }
1630
1696
 
1631
1697
  foe.active.volatiles.Foresight = true;
1632
- try options.log.start(battle.active(player.foe()), .FocusEnergy);
1698
+ try options.log.start(.{ battle.active(player.foe()), .FocusEnergy });
1633
1699
  }
1634
1700
 
1635
1701
  pub fn freezeChance(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1641,15 +1707,18 @@ pub const Effects = struct {
1641
1707
  if (Status.any(foe_stored.status)) return;
1642
1708
 
1643
1709
  // Nothing is immune to Ice-type, but Ghosts are immune to freezeChance called by Tri Attack
1710
+ if (state.immune()) return;
1644
1711
  if (battle.field.weather == .Sun) return;
1645
- if (foe.active.types.includes(Move.get(state.move).type)) return;
1712
+ if (foe.active.types.includes(Move.get(state.move).type) and state.move != .TriAttack) {
1713
+ return;
1714
+ }
1646
1715
  if (!state.proc) return;
1647
1716
  if (foe.conditions.Safeguard) return;
1648
1717
  // Freeze Clause Mod
1649
1718
  if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1650
1719
 
1651
1720
  foe_stored.status = Status.init(.FRZ);
1652
- try options.log.status(foe_ident, foe_stored.status, .None);
1721
+ try options.log.status(.{ foe_ident, foe_stored.status, .None });
1653
1722
 
1654
1723
  // TODO: check for status berry
1655
1724
 
@@ -1671,46 +1740,131 @@ pub const Effects = struct {
1671
1740
  state.bp = ((if (frustration) 255 - val else val) * 10) / 25;
1672
1741
  }
1673
1742
 
1674
- pub fn haze(battle: anytype, player: Player, state: *State, options: anytype) !void {
1675
- _ = .{ 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(.{});
1676
1757
  }
1677
1758
 
1678
1759
  pub fn heal(battle: anytype, player: Player, state: *State, options: anytype) !void {
1679
- _ = .{ battle, player, state, options }; // TODO
1680
- }
1760
+ var side = battle.side(player);
1761
+ var stored = side.stored();
1762
+ const ident = battle.active(player);
1681
1763
 
1682
- pub fn healBell(battle: anytype, player: Player, state: *State, options: anytype) !void {
1683
- _ = .{ battle, player, state, options }; // TODO
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 });
1684
1779
  }
1685
1780
 
1686
- pub fn hiddenPower(battle: anytype, player: Player, state: *State, options: anytype) !void {
1687
- _ = .{ 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);
1688
1791
  }
1689
1792
 
1690
- pub fn hyperBeam(battle: anytype, player: Player, state: *State, options: anytype) !void {
1691
- _ = .{ 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;
1692
1797
  }
1693
1798
 
1694
- pub fn jumpKick(battle: anytype, player: Player, state: *State, options: anytype) !void {
1695
- _ = .{ 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)});
1696
1802
  }
1697
1803
 
1698
1804
  pub fn leechSeed(battle: anytype, player: Player, state: *State, options: anytype) !void {
1699
- _ = .{ 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 });
1700
1832
  }
1701
1833
 
1702
1834
  pub fn lockOn(battle: anytype, player: Player, state: *State, options: anytype) !void {
1703
- _ = .{ 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 });
1704
1846
  }
1705
1847
 
1706
1848
  const MAGNITUDE_POWER = [_]u8{ 10, 30, 50, 70, 90, 110, 150 };
1707
1849
 
1708
1850
  pub fn magnitude(battle: anytype, player: Player, state: *State, options: anytype) !void {
1709
- _ = .{ 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];
1710
1854
  }
1711
1855
 
1712
- pub fn meanLook(battle: anytype, player: Player, state: *State, options: anytype) !void {
1713
- _ = .{ 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;
1714
1868
  }
1715
1869
 
1716
1870
  pub fn metronome(
@@ -1730,17 +1884,13 @@ pub const Effects = struct {
1730
1884
  // FIXME
1731
1885
  state.move = try Rolls.metronome(battle, player, options);
1732
1886
  state.from = .Metronome;
1733
- return try generated.runMove(battle, player, state, options);
1887
+ return generated.runMove(battle, player, state, options);
1734
1888
  }
1735
1889
 
1736
1890
  pub fn mimic(battle: anytype, player: Player, state: *State, options: anytype) !void {
1737
1891
  _ = .{ battle, player, state, options }; // TODO
1738
1892
  }
1739
1893
 
1740
- pub fn mirrorCoat(battle: anytype, player: Player, state: *State, options: anytype) !void {
1741
- _ = .{ battle, player, state, options }; // TODO
1742
- }
1743
-
1744
1894
  pub fn mirrorMove(
1745
1895
  battle: anytype,
1746
1896
  player: Player,
@@ -1755,11 +1905,11 @@ pub const Effects = struct {
1755
1905
 
1756
1906
  const last = foe.lastMove(false);
1757
1907
 
1758
- if (last == .None) return options.log.fail(battle.active(player), .None);
1908
+ if (last == .None) return options.log.fail(.{ battle.active(player), .None });
1759
1909
  const knows = knows: for (side.active.moves) |m| {
1760
1910
  if (last == m.id) break :knows true;
1761
1911
  } else false;
1762
- if (knows) return options.log.fail(battle.active(player), .None);
1912
+ if (knows) return options.log.fail(.{ battle.active(player), .None });
1763
1913
 
1764
1914
  // if (wPlayerCharging) ???
1765
1915
  // wPlayerCharging = 1
@@ -1767,23 +1917,23 @@ pub const Effects = struct {
1767
1917
  // FIXME
1768
1918
  state.move = last;
1769
1919
  state.from = .MirrorMove;
1770
- return try generated.runMove(battle, player, state, options);
1920
+ return generated.runMove(battle, player, state, options);
1771
1921
  }
1772
1922
 
1773
1923
  pub fn mist(battle: anytype, player: Player, _: *State, options: anytype) !void {
1774
1924
  var side = battle.side(player);
1775
1925
 
1776
- 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 });
1777
1927
  side.active.volatiles.Mist = true;
1778
1928
 
1779
- try options.log.start(battle.active(player), .Mist);
1929
+ try options.log.start(.{ battle.active(player), .Mist });
1780
1930
  }
1781
1931
 
1782
1932
  pub fn weatherHeal(battle: anytype, player: Player, _: *State, options: anytype) !void {
1783
1933
  var stored = battle.side(player).stored();
1784
1934
  const ident = battle.active(player);
1785
1935
 
1786
- 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 });
1787
1937
 
1788
1938
  stored.hp = switch (battle.field.weather) {
1789
1939
  .Sun => stored.stats.hp,
@@ -1791,7 +1941,7 @@ pub const Effects = struct {
1791
1941
  else => @min(stored.stats.hp, stored.hp + (stored.stats.hp / 4)),
1792
1942
  };
1793
1943
 
1794
- try options.log.heal(ident, stored, Heal.None);
1944
+ try options.log.heal(.{ ident, stored, .None });
1795
1945
  }
1796
1946
 
1797
1947
  pub fn multiHit(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1804,17 +1954,17 @@ pub const Effects = struct {
1804
1954
  const foe_ident = battle.active(player.foe());
1805
1955
 
1806
1956
  if (foe_volatiles.Flying or foe_volatiles.Underground) {
1807
- if (!showdown) return options.log.fail(battle.active(player.foe()), .None);
1808
- try options.log.lastmiss();
1809
- 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)});
1810
1960
  } else if (foe_volatiles.Substitute) {
1811
- return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
1961
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
1812
1962
  } else if (!Status.is(foe.stored().status, .SLP) or foe_volatiles.Nightmare) {
1813
- return options.log.fail(foe_ident, .None);
1963
+ return options.log.fail(.{ foe_ident, .None });
1814
1964
  }
1815
1965
 
1816
1966
  foe_volatiles.Nightmare = true;
1817
- try options.log.start(battle.active(player.foe()), .Nightmare);
1967
+ try options.log.start(.{ battle.active(player.foe()), .Nightmare });
1818
1968
  }
1819
1969
 
1820
1970
  pub const ohko = struct {
@@ -1838,9 +1988,9 @@ pub const Effects = struct {
1838
1988
  const foe_ident = battle.active(player.foe());
1839
1989
 
1840
1990
  state.damage = 0;
1841
- if (state.immune()) return options.log.immune(foe_ident, .None);
1991
+ if (state.immune()) return options.log.immune(.{ foe_ident, .None });
1842
1992
  const delta = side.stored().level -| foe.stored().level;
1843
- if (delta == 0) return options.log.immune(foe_ident, .OHKO);
1993
+ if (delta == 0) return options.log.immune(.{ foe_ident, .OHKO });
1844
1994
 
1845
1995
  state.accuracy = 2 * delta +| Gen12.percent(30);
1846
1996
  try checkHit(battle, player, state, options);
@@ -1856,21 +2006,39 @@ pub const Effects = struct {
1856
2006
  const ident = battle.active(player);
1857
2007
  const foe_ident = battle.active(player.foe());
1858
2008
 
1859
- if (state.miss) return options.log.fail(foe_ident, .None);
2009
+ if (state.miss) return options.log.fail(.{ foe_ident, .None });
1860
2010
  if (foe.active.volatiles.Substitute) {
1861
- return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
2011
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
1862
2012
  }
1863
2013
 
1864
2014
  const avg = @max((foe.stored().hp + side.stored().hp) / 2, 1);
1865
2015
  foe.stored().hp -= avg;
1866
2016
  side.stored().hp = avg;
1867
2017
 
1868
- try options.log.sethp(foe_ident, foe.stored(), .Silent);
1869
- 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 });
1870
2020
  }
1871
2021
 
1872
2022
  pub fn paralyze(battle: anytype, player: Player, state: *State, options: anytype) !void {
1873
- _ = .{ battle, player, state, options }; // TODO
2023
+ var foe = battle.foe(player);
2024
+ const foe_ident = battle.active(player.foe());
2025
+
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 });
2031
+ if (state.miss) {
2032
+ try options.log.lastmiss(.{});
2033
+ return options.log.miss(.{battle.active(player)});
2034
+ } else if (foe.active.volatiles.Substitute) {
2035
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
2036
+ }
2037
+
2038
+ foe.stored().status = Status.init(.PAR);
2039
+ foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2040
+
2041
+ try options.log.status(.{ foe_ident, foe.stored().status, .None });
1874
2042
  }
1875
2043
 
1876
2044
  pub fn paralyzeChance(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1885,14 +2053,15 @@ pub const Effects = struct {
1885
2053
 
1886
2054
  foe_stored.status = Status.init(.PAR);
1887
2055
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
1888
- 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 });
1889
2058
 
1890
2059
  // TODO: check for status berry
1891
2060
  }
1892
2061
 
1893
2062
  pub fn payDay(battle: anytype, player: Player, _: *State, options: anytype) !void {
1894
2063
  if (!showdown or !battle.foe(player).active.volatiles.Substitute) {
1895
- try options.log.fieldactivate(); // FIXME .PayDay
2064
+ try options.log.fieldactivate(.{}); // FIXME .PayDay
1896
2065
  }
1897
2066
  }
1898
2067
 
@@ -1901,17 +2070,17 @@ pub const Effects = struct {
1901
2070
  var foe = battle.foe(player);
1902
2071
 
1903
2072
  if (foe.active.volatiles.PerishSong) {
1904
- return options.log.fail(battle.active(player.foe()), .None);
2073
+ return options.log.fail(.{ battle.active(player.foe()), .None });
1905
2074
  } else if (!side.active.volatiles.PerishSong) {
1906
2075
  side.active.volatiles.PerishSong = true;
1907
2076
  side.active.volatiles.perish_song = 4;
1908
- try options.log.start(battle.active(player), .PerishSong3Silent);
2077
+ try options.log.start(.{ battle.active(player), .PerishSong3Silent });
1909
2078
  }
1910
2079
  foe.active.volatiles.PerishSong = true;
1911
2080
  foe.active.volatiles.perish_song = 4;
1912
- try options.log.start(battle.active(player.foe()), .PerishSong3Silent);
2081
+ try options.log.start(.{ battle.active(player.foe()), .PerishSong3Silent });
1913
2082
 
1914
- try options.log.fieldactivate(); // FIXME .PerishSong
2083
+ try options.log.fieldactivate(.{}); // FIXME .PerishSong
1915
2084
  }
1916
2085
 
1917
2086
  pub fn poison(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1921,24 +2090,24 @@ pub const Effects = struct {
1921
2090
 
1922
2091
  const toxic = state.move == .Toxic;
1923
2092
  if (state.immune() or foe.active.types.includes(.Poison)) {
1924
- return options.log.immune(foe_ident, .None);
2093
+ return options.log.immune(.{ foe_ident, .None });
1925
2094
  } else if (Status.any(foe_stored.status)) {
1926
2095
  // Pokémon Showdown considers Toxic to be a status even in Generation II and so
1927
2096
  // will not include a fail reason for Toxic vs. Poison or vice-versa...
1928
- 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))
1929
2098
  if (!showdown)
1930
- .Poison
2099
+ Fail.Poison
1931
2100
  else if (toxic == (foe_stored.status == Status.TOX))
1932
- if (toxic) .Toxic else .Poison
2101
+ if (toxic) Fail.Toxic else Fail.Poison
1933
2102
  else
1934
- .None
2103
+ Fail.None
1935
2104
  else
1936
- .None);
2105
+ Fail.None });
1937
2106
  } else if (foe.active.volatiles.Substitute) {
1938
- return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
2107
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
1939
2108
  } else if (state.miss) {
1940
- try options.log.lastmiss();
1941
- try options.log.miss(battle.active(player));
2109
+ try options.log.lastmiss(.{});
2110
+ return options.log.miss(.{battle.active(player)});
1942
2111
  }
1943
2112
 
1944
2113
  foe_stored.status = Status.init(.PSN);
@@ -1948,7 +2117,7 @@ pub const Effects = struct {
1948
2117
  foe.active.volatiles.toxic = 0;
1949
2118
  }
1950
2119
 
1951
- try options.log.status(foe_ident, foe_stored.status, .None);
2120
+ try options.log.status(.{ foe_ident, foe_stored.status, .None });
1952
2121
 
1953
2122
  // TODO: check for status berry
1954
2123
  }
@@ -1964,7 +2133,8 @@ pub const Effects = struct {
1964
2133
  if (foe.conditions.Safeguard) return;
1965
2134
 
1966
2135
  foe_stored.status = Status.init(.PSN);
1967
- 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 });
1968
2138
 
1969
2139
  // TODO: check for status berry
1970
2140
  }
@@ -1979,10 +2149,7 @@ pub const Effects = struct {
1979
2149
 
1980
2150
  const failed = !state.first or volatiles.Substitute or
1981
2151
  !try Rolls.protect(battle, player, volatiles.protect, options);
1982
- if (failed) {
1983
- try options.log.fail(battle.active(player), .None);
1984
- return;
1985
- }
2152
+ if (failed) return options.log.fail(.{ battle.active(player), .None });
1986
2153
 
1987
2154
  volatiles.protect = @max(8, volatiles.protect + 1);
1988
2155
  if (state.move == .Endure) {
@@ -1990,21 +2157,20 @@ pub const Effects = struct {
1990
2157
  } else {
1991
2158
  volatiles.Protect = true;
1992
2159
  }
1993
- try options.log.singleturn(battle.active(player), state.move);
2160
+ try options.log.singleturn(.{ battle.active(player), state.move });
1994
2161
  }
1995
2162
 
1996
2163
  pub fn psychUp(battle: anytype, player: Player, _: *State, options: anytype) !void {
1997
- _ = .{ battle, player, options }; // TODO
1998
- // var side = battle.side(player);
1999
- // const foe = battle.foe(player);
2164
+ var side = battle.side(player);
2165
+ const foe = battle.foe(player);
2000
2166
 
2001
- // // Pokémon Showdown should fail when the enemy has no boosts/unboosts but doesn't
2002
- // if (!showdown and @as(u32, @bitCast(foe.active.boosts)) == 0) {
2003
- // return options.log.fail(battle.active(player), .None);
2004
- // }
2167
+ // Pokémon Showdown should fail when the enemy has no boosts/unboosts but doesn't
2168
+ if (!showdown and @as(u32, @bitCast(foe.active.boosts)) == 0) {
2169
+ return options.log.fail(.{ battle.active(player), .None });
2170
+ }
2005
2171
 
2006
- // side.active.boosts = foe.active.boosts;
2007
- // try options.log.copyboost(battle.active(player), battle.active(player.foe()));
2172
+ side.active.boosts = foe.active.boosts;
2173
+ try options.log.copyboost(.{ battle.active(player), battle.active(player.foe()) });
2008
2174
  }
2009
2175
 
2010
2176
  pub fn pursuit(battle: anytype, player: Player, state: *State, _: anytype) !void {
@@ -2015,7 +2181,7 @@ pub const Effects = struct {
2015
2181
  pub const rage = struct {
2016
2182
  pub fn start(battle: anytype, player: Player, _: *State, options: anytype) !void {
2017
2183
  battle.side(player).active.volatiles.Rage = true;
2018
- try options.log.singlemove(battle.active(player), Move.Rage);
2184
+ try options.log.singlemove(.{ battle.active(player), Move.Rage });
2019
2185
  }
2020
2186
 
2021
2187
  pub fn damage(battle: anytype, player: Player, state: *State, _: anytype) !void {
@@ -2031,16 +2197,37 @@ pub const Effects = struct {
2031
2197
  return weather(battle, .Rain, options);
2032
2198
  }
2033
2199
 
2034
- pub fn rapidSpin(battle: anytype, player: Player, state: *State, options: anytype) !void {
2035
- _ = .{ battle, player, state, options }; // TODO
2036
- }
2200
+ pub fn rapidSpin(battle: anytype, player: Player, _: *State, options: anytype) !void {
2201
+ var side = battle.side(player);
2037
2202
 
2038
- pub fn razorWind(battle: anytype, player: Player, state: *State, options: anytype) !void {
2039
- _ = .{ battle, player, state, options }; // TODO
2203
+ if (side.active.volatiles.LeechSeed) {
2204
+ try options.log.end(.{ battle.active(player), .LeechSeedFrom });
2205
+ side.active.volatiles.LeechSeed = false;
2206
+ }
2207
+
2208
+ if (side.conditions.Spikes) {
2209
+ try options.log.sideend(.{ player, protocol.Side.Spikes, battle.active(player) });
2210
+ side.conditions.Spikes = false;
2211
+ }
2212
+
2213
+ if (side.active.volatiles.bind.duration > 0) {
2214
+ const reason = @intFromEnum(End.Bind) + side.active.volatiles.bind.reason - 1;
2215
+ try options.log.end(.{ battle.active(player), @as(End, @enumFromInt(reason)) });
2216
+ side.active.volatiles.bind = .{};
2217
+ }
2040
2218
  }
2041
2219
 
2042
2220
  pub fn recoil(battle: anytype, player: Player, state: *State, options: anytype) !void {
2043
- _ = .{ battle, player, state, options }; // TODO
2221
+ var side = battle.side(player);
2222
+ var stored = side.stored();
2223
+ const ident = battle.active(player);
2224
+
2225
+ assert(state.damage > 0);
2226
+
2227
+ const damage: i16 = @intCast(@max(state.damage / 4, 1));
2228
+ stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
2229
+
2230
+ try options.log.damage(.{ ident, stored, .RecoilOf, battle.active(player.foe()) });
2044
2231
  }
2045
2232
 
2046
2233
  pub fn reversal(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -2068,25 +2255,72 @@ pub const Effects = struct {
2068
2255
  try calcDamage(battle, player, state, options);
2069
2256
  }
2070
2257
 
2071
- pub fn rollout(battle: anytype, player: Player, state: *State, options: anytype) !void {
2072
- _ = .{ battle, player, state, options }; // TODO
2258
+ pub const rollout = struct {
2259
+ pub fn check(battle: anytype, player: Player, _: *State, _: anytype) !void {
2260
+ var side = battle.side(player);
2261
+
2262
+ if (!side.active.volatiles.Rollout) {
2263
+ side.active.volatiles.attacks = 0;
2264
+ } else {
2265
+ // TODO SkipToBattleCommand(doturn_command)
2266
+ }
2267
+ }
2268
+
2269
+ pub fn power(battle: anytype, player: Player, state: *State, _: anytype) !void {
2270
+ var side = battle.side(player);
2271
+
2272
+ if (Status.is(side.stored().status, .SLP)) return;
2273
+
2274
+ if (side.active.volatiles.attacks > 0) {
2275
+ // TODO set wSomeoneIsRampaging
2276
+ }
2277
+
2278
+ if (state.miss) {
2279
+ side.active.volatiles.Rollout = false;
2280
+ return;
2281
+ }
2282
+
2283
+ assert(side.active.volatiles.attacks < 5);
2284
+ side.active.volatiles.attacks += 1;
2285
+ side.active.volatiles.Rollout = side.active.volatiles.attacks != 5;
2286
+
2287
+ if (side.active.volatiles.DefenseCurl) state.damage *|= 2;
2288
+ }
2289
+ };
2290
+
2291
+ pub fn sandstorm(battle: anytype, player: Player, _: *State, options: anytype) !void {
2292
+ if (battle.field.weather != .Sandstorm) return weather(battle, .Sandstorm, options);
2293
+ return options.log.fail(.{ battle.active(player), .None });
2073
2294
  }
2074
2295
 
2075
- pub fn sacredFire(battle: anytype, player: Player, state: *State, options: anytype) !void {
2076
- _ = .{ battle, player, state, options }; // TODO
2296
+ pub fn safeguard(battle: anytype, player: Player, _: *State, options: anytype) !void {
2297
+ return screens(battle, player, "Safeguard", "safeguard", options);
2077
2298
  }
2078
2299
 
2079
- pub fn safeguard(battle: anytype, player: Player, state: *State, options: anytype) !void {
2080
- _ = .{ battle, player, state, options }; // TODO
2300
+ pub fn reflect(battle: anytype, player: Player, _: *State, options: anytype) !void {
2301
+ return screens(battle, player, "Reflect", "reflect", options);
2081
2302
  }
2082
2303
 
2083
- pub fn sandstorm(battle: anytype, player: Player, _: *State, options: anytype) !void {
2084
- if (battle.field.weather != .Sandstorm) return weather(battle, .Sandstorm, options);
2085
- return options.log.fail(battle.active(player), .None);
2304
+ pub fn lightScreen(battle: anytype, player: Player, _: *State, options: anytype) !void {
2305
+ return screens(battle, player, "LightScreen", "light_screen", options);
2086
2306
  }
2087
2307
 
2088
- pub fn screens(battle: anytype, player: Player, state: *State, options: anytype) !void {
2089
- _ = .{ battle, player, state, options }; // TODO
2308
+ fn screens(
2309
+ battle: anytype,
2310
+ player: Player,
2311
+ comptime key: []const u8,
2312
+ comptime value: []const u8,
2313
+ options: anytype,
2314
+ ) !void {
2315
+ var side = battle.side(player);
2316
+
2317
+ if (@field(side.conditions, key)) {
2318
+ return options.log.fail(.{ battle.active(player), .None });
2319
+ }
2320
+
2321
+ @field(side.conditions, key) = true;
2322
+ @field(side.conditions, value) = 5;
2323
+ try options.log.sidestart(.{ player, @field(protocol.Side, key) });
2090
2324
  }
2091
2325
 
2092
2326
  pub fn sketch(battle: anytype, player: Player, _: *State, options: anytype) !void {
@@ -2097,34 +2331,26 @@ pub const Effects = struct {
2097
2331
 
2098
2332
  if (showdown) {
2099
2333
  // yes, actually "Splash". '|nothing' gets blindly mapped to '|-activate||move:Splash'
2100
- try options.log.activate(battle.active(player), .Splash);
2334
+ try options.log.activate(.{ battle.active(player), .Splash });
2101
2335
  } else {
2102
- try options.log.fail(battle.active(player), .None);
2336
+ try options.log.fail(.{ battle.active(player), .None });
2103
2337
  }
2104
2338
  }
2105
2339
 
2106
- pub fn skullBash(battle: anytype, player: Player, state: *State, options: anytype) !void {
2107
- _ = .{ battle, player, state, options }; // TODO
2108
- }
2109
-
2110
- pub fn skyAttack(battle: anytype, player: Player, state: *State, options: anytype) !void {
2111
- _ = .{ battle, player, state, options }; // TODO
2112
- }
2113
-
2114
2340
  pub fn sleep(battle: anytype, player: Player, state: *State, options: anytype) !void {
2115
2341
  var foe = battle.foe(player);
2116
2342
  var foe_stored = foe.stored();
2117
2343
  const foe_ident = battle.active(player.foe());
2118
2344
 
2119
2345
  if (Status.any(foe_stored.status)) {
2120
- return options.log.fail(
2346
+ return options.log.fail(.{
2121
2347
  foe_ident,
2122
- if (Status.is(foe_stored.status, .SLP)) .Sleep else .None,
2123
- );
2348
+ if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
2349
+ });
2124
2350
  }
2125
2351
 
2126
2352
  if (foe.active.volatiles.Substitute) {
2127
- return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
2353
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
2128
2354
  }
2129
2355
 
2130
2356
  // Sleep Clause Mod
@@ -2135,25 +2361,21 @@ pub const Effects = struct {
2135
2361
  }
2136
2362
 
2137
2363
  foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2138
- try options.log.statusFrom(foe_ident, foe_stored.status, state.move);
2364
+ try options.log.status(.{ foe_ident, foe_stored.status, .From, state.move });
2139
2365
  }
2140
2366
 
2141
2367
  pub fn sleepTalk(battle: anytype, player: Player, state: *State, options: anytype) !void {
2142
2368
  const side = battle.side(player);
2143
2369
 
2144
2370
  if (!Status.is(side.stored().status, .SLP)) {
2145
- if (!showdown) try options.log.fail(battle.active(player), .None);
2146
- return;
2371
+ return if (!showdown) try options.log.fail(.{ battle.active(player), .None });
2147
2372
  }
2148
2373
 
2149
2374
  var n: u3 = 0;
2150
2375
  for (side.active.moves, 0..) |m, i| {
2151
2376
  if (state.mslot - 1 != i and Move.get(m.id).extra.sleep_talk) n += 1;
2152
2377
  }
2153
- if (n == 0) {
2154
- try options.log.fail(battle.active(player), .None);
2155
- return;
2156
- }
2378
+ if (n == 0) return options.log.fail(.{ battle.active(player), .None });
2157
2379
 
2158
2380
  _ = try Rolls.sleepTalk(battle, player, n, state.mslot, options); // TODO
2159
2381
  }
@@ -2162,28 +2384,55 @@ pub const Effects = struct {
2162
2384
  if (Status.is(battle.side(player).stored().status, .SLP)) return true;
2163
2385
  state.damage = 0;
2164
2386
  state.miss = true; // TODO is this necessary?
2165
- if (!showdown) try options.log.fail(battle.active(player), .None);
2387
+ if (!showdown) try options.log.fail(.{ battle.active(player), .None });
2166
2388
  return false;
2167
2389
  }
2168
2390
 
2169
- pub fn spikes(battle: anytype, player: Player, state: *State, options: anytype) !void {
2170
- _ = .{ battle, player, state, options }; // TODO
2391
+ pub fn spikes(battle: anytype, player: Player, _: *State, options: anytype) !void {
2392
+ var foe = battle.foe(player);
2393
+ if (foe.conditions.Spikes) return options.log.fail(.{ battle.active(player), .None });
2394
+
2395
+ foe.conditions.Spikes = true;
2396
+ try options.log.sidestart(.{ player.foe(), protocol.Side.Spikes });
2171
2397
  }
2172
2398
 
2173
2399
  pub fn spite(battle: anytype, player: Player, state: *State, options: anytype) !void {
2174
2400
  _ = .{ battle, player, state, options }; // TODO
2401
+ // var foe = battle.foe(player);
2402
+
2403
+ // if (state.miss) return options.log.fail(.{ battle.active(player), .None });
2404
+
2405
+ // const last = foe.lastMove(false);
2406
+ // if (showdown) {
2407
+ // const roll = try Rolls.spite(battle, player, options);
2408
+ // } else {
2409
+ // if (last != .None) {}
2410
+ // }
2175
2411
  }
2176
2412
 
2177
2413
  pub fn splash(battle: anytype, player: Player, _: *State, options: anytype) !void {
2178
- try options.log.activate(battle.active(player), .Splash);
2414
+ try options.log.activate(.{ battle.active(player), .Splash });
2179
2415
  }
2180
2416
 
2181
- pub fn stomp(battle: anytype, player: Player, state: *State, options: anytype) !void {
2182
- _ = .{ battle, player, state, options }; // TODO
2183
- }
2417
+ pub fn substitute(battle: anytype, player: Player, _: *State, options: anytype) !void {
2418
+ var side = battle.side(player);
2419
+ if (side.active.volatiles.Substitute) {
2420
+ return options.log.fail(.{ battle.active(player), .Substitute });
2421
+ }
2184
2422
 
2185
- pub fn substitute(battle: anytype, player: Player, state: *State, options: anytype) !void {
2186
- _ = .{ battle, player, state, options }; // TODO
2423
+ assert(side.stored().stats.hp <= 1023);
2424
+ const hp: u8 = @intCast(side.stored().stats.hp / 4);
2425
+
2426
+ if (hp == 0 or side.stored().hp <= hp) {
2427
+ return options.log.fail(.{ battle.active(player), .Weak });
2428
+ }
2429
+
2430
+ side.stored().hp -= hp;
2431
+ side.active.volatiles.substitute = hp;
2432
+ side.active.volatiles.Substitute = true;
2433
+ side.active.volatiles.bind = .{};
2434
+ try options.log.start(.{ battle.active(player), .Substitute });
2435
+ try options.log.damage(.{ battle.active(player), side.stored(), .None });
2187
2436
  }
2188
2437
 
2189
2438
  pub fn sunnyDay(battle: anytype, _: Player, _: *State, options: anytype) !void {
@@ -2193,18 +2442,14 @@ pub const Effects = struct {
2193
2442
  fn weather(battle: anytype, w: Weather, options: anytype) !void {
2194
2443
  battle.field.weather = w;
2195
2444
  battle.field.weather_duration = 5;
2196
- try options.log.weather(w, .None);
2197
- }
2198
-
2199
- pub fn swagger(battle: anytype, player: Player, state: *State, options: anytype) !void {
2200
- _ = .{ battle, player, state, options }; // TODO
2445
+ try options.log.weather(.{ w, protocol.Weather.None });
2201
2446
  }
2202
2447
 
2203
2448
  pub fn teleport(battle: anytype, player: Player, _: *State, options: anytype) !void {
2204
2449
  if (showdown) return;
2205
2450
 
2206
- try options.log.fail(battle.active(player), .None);
2207
- try options.log.laststill();
2451
+ try options.log.fail(.{ battle.active(player), .None });
2452
+ try options.log.laststill(.{});
2208
2453
  }
2209
2454
 
2210
2455
  pub fn thief(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -2219,15 +2464,21 @@ pub const Effects = struct {
2219
2464
  side.stored().item = foe.stored().item;
2220
2465
  foe.stored().item = .None;
2221
2466
 
2222
- try options.log.item(
2467
+ try options.log.item(.{
2223
2468
  battle.active(player),
2224
2469
  side.stored().item,
2225
2470
  battle.active(player.foe()),
2226
- );
2471
+ });
2227
2472
  }
2228
2473
 
2229
- pub fn thrashing(battle: anytype, player: Player, state: *State, options: anytype) !void {
2230
- _ = .{ battle, player, state, options }; // TODO
2474
+ pub fn thrashing(battle: anytype, player: Player, _: *State, options: anytype) !void {
2475
+ var side = battle.side(player);
2476
+ if (Status.is(side.stored().status, .SLP)) return;
2477
+
2478
+ assert(!side.active.volatiles.Thrashing);
2479
+ side.active.volatiles.Thrashing = true;
2480
+ side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
2481
+ // TODO wSomeoneIsRampaging = true;
2231
2482
  }
2232
2483
 
2233
2484
  pub fn transform(battle: anytype, player: Player, _: *State, options: anytype) !void {
@@ -2239,11 +2490,11 @@ pub const Effects = struct {
2239
2490
  side.active.volatiles.dirty = false;
2240
2491
 
2241
2492
  if (foe.active.volatiles.Transform) {
2242
- return options.log.fail(battle.active(player), .None);
2493
+ return options.log.fail(.{ battle.active(player), .None });
2243
2494
  } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2244
- if (!showdown) return options.log.fail(battle.active(player), .None);
2245
- try options.log.lastmiss();
2246
- 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)});
2247
2498
  }
2248
2499
 
2249
2500
  side.active.volatiles.Transform = true;
@@ -2265,7 +2516,7 @@ pub const Effects = struct {
2265
2516
  side.active.moves[i].pp = if (m.id != .None) if (m.id == .Sketch) 1 else 5 else 0;
2266
2517
  }
2267
2518
 
2268
- try options.log.transform(battle.active(player), foe_ident);
2519
+ try options.log.transform(.{ battle.active(player), foe_ident });
2269
2520
  }
2270
2521
 
2271
2522
  pub fn triAttack(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -2284,8 +2535,7 @@ pub const Effects = struct {
2284
2535
  _ = .{ battle, player, state, options }; // TODO
2285
2536
  }
2286
2537
 
2287
- const ALL_STAT_UP_STATS = [_][]const u8{ "atk", "def", "spe", "spa", "spd" };
2288
- const ALL_STAT_UP_REASONS =
2538
+ const STAT_UP_REASONS =
2289
2539
  [_]Boost{ .Attack, .Defense, .Speed, .SpecialAttack, .SpecialDefense };
2290
2540
 
2291
2541
  pub fn allStatUpChance(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -2295,20 +2545,15 @@ pub const Effects = struct {
2295
2545
  var boosts = &side.active.boosts;
2296
2546
 
2297
2547
  if (!state.proc) return;
2298
- inline for (ALL_STAT_UP_STATS, ALL_STAT_UP_REASONS) |s, r| {
2548
+ inline for (STATS, STAT_UP_REASONS) |s, r| {
2299
2549
  assert(@field(boosts, s) >= -6 and @field(boosts, s) <= 6);
2300
2550
  if (@field(boosts, s) == 6 or @field(stats, s) == MAX_STAT_VALUE) {
2301
- try options.log.boost(battle.active(player), r, 0);
2551
+ try options.log.boost(.{ battle.active(player), r, 0 });
2302
2552
  } else {
2303
- @field(boosts, s) = @intCast(@min(6, @as(i8, @field(boosts, s)) + 1));
2304
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, @field(boosts, s)) + 6))];
2305
- const stat = @field(unmodifiedStats(battle, side), s);
2306
- @field(stats, s) = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2307
- 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 });
2308
2555
  }
2309
2556
  }
2310
-
2311
- statusModify(side.stored().status, stats);
2312
2557
  }
2313
2558
 
2314
2559
  pub fn boost(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -2316,72 +2561,65 @@ pub const Effects = struct {
2316
2561
  var side = battle.side(player);
2317
2562
  const ident = battle.active(player);
2318
2563
 
2319
- if (state.miss) return log.fail(ident, .None);
2564
+ if (state.miss) return log.fail(.{ ident, .None });
2320
2565
 
2321
2566
  var stats = &side.active.stats;
2322
2567
  var boosts = &side.active.boosts;
2323
2568
 
2324
2569
  const move = Move.get(state.move);
2325
2570
  switch (move.effect) {
2326
- .AttackUp1, .AttackUp2, .Rage => {
2571
+ .AttackUp1, .AttackUp2, .Rage, .Swagger => {
2327
2572
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2328
- if (boosts.atk == 6 or stats.atk == MAX_STAT_VALUE) return log.fail(ident, .None);
2329
- const n: u2 = if (move.effect == .AttackUp2) 2 else 1;
2330
- boosts.atk = @as(i4, @intCast(@min(6, @as(i8, boosts.atk) + n)));
2573
+ if (boosts.atk == 6 or stats.atk == MAX_STAT_VALUE) {
2574
+ return log.fail(.{ ident, .None });
2575
+ }
2576
+ const n: u2 = if (move.effect == .AttackUp2 or move.effect == .Swagger) 2 else 1;
2577
+ try boostStat(battle, side, stats, boosts, "atk", n);
2331
2578
  const reason = if (showdown and move.effect == .Rage) Boost.Rage else Boost.Attack;
2332
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2333
- const stat = unmodifiedStats(battle, side).atk;
2334
- stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2335
- try log.boost(ident, reason, n);
2579
+ try log.boost(.{ ident, reason, n });
2336
2580
  },
2337
- .DefenseUp1, .DefenseUp2 => {
2581
+ .DefenseUp1, .DefenseUp2, .SkullBash => {
2338
2582
  assert(boosts.def >= -6 and boosts.def <= 6);
2339
- 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
+ }
2340
2586
  const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
2341
- boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
2342
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2343
- const stat = unmodifiedStats(battle, side).def;
2344
- stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2345
- try log.boost(ident, .Defense, n);
2587
+ try boostStat(battle, side, stats, boosts, "def", n);
2588
+ try log.boost(.{ ident, .Defense, n });
2346
2589
  },
2347
2590
  .SpeedUp2 => {
2348
2591
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2349
- if (boosts.spe == 6 or stats.spe == MAX_STAT_VALUE) return log.fail(ident, .None);
2350
- boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
2351
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2352
- const stat = unmodifiedStats(battle, side).spe;
2353
- stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2354
- 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 });
2355
2597
  },
2356
2598
  .SpAtkUp1 => {
2357
2599
  assert(boosts.spa >= -6 and boosts.spa <= 6);
2358
- if (boosts.spa == 6 or stats.spa == MAX_STAT_VALUE) return log.fail(ident, .None);
2359
- boosts.spa = @intCast(@min(6, @as(i8, boosts.spa) + 1));
2360
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.spa) + 6))];
2361
- const stat = unmodifiedStats(battle, side).spa;
2362
- stats.spa = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2363
- 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 });
2364
2605
  },
2365
2606
  .SpDefUp2 => {
2366
2607
  assert(boosts.spd >= -6 and boosts.spd <= 6);
2367
- if (boosts.spd == 6 or stats.spd == MAX_STAT_VALUE) return log.fail(ident, .None);
2368
- boosts.spd = @intCast(@min(6, @as(i8, boosts.spd) + 2));
2369
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.spd) + 6))];
2370
- const stat = unmodifiedStats(battle, side).spd;
2371
- stats.spd = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2372
- 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 });
2373
2613
  },
2374
2614
  .EvasionUp1 => {
2375
2615
  assert(boosts.evasion >= -6 and boosts.evasion <= 6);
2376
- if (boosts.evasion == 6) return log.fail(ident, .None);
2616
+ if (boosts.evasion == 6) return log.fail(.{ ident, .None });
2377
2617
  boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
2378
- try log.boost(ident, .Evasion, 1);
2618
+ try log.boost(.{ ident, .Evasion, 1 });
2379
2619
  if (state.move == .Minimize) side.active.volatiles.minimized = true;
2380
2620
  },
2381
2621
  else => unreachable,
2382
2622
  }
2383
-
2384
- statusModify(side.stored().status, stats);
2385
2623
  }
2386
2624
 
2387
2625
  pub fn unboost(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -2390,8 +2628,8 @@ pub const Effects = struct {
2390
2628
  const foe_ident = battle.active(player.foe());
2391
2629
 
2392
2630
  if (foe.active.volatiles.Mist) {
2393
- try log.activate(foe_ident, .Mist);
2394
- return log.fail(foe_ident, .None);
2631
+ try log.activate(.{ foe_ident, .Mist });
2632
+ return log.fail(.{ foe_ident, .None });
2395
2633
  }
2396
2634
 
2397
2635
  const fail = foe.active.volatiles.Substitute or state.miss or !state.proc;
@@ -2400,92 +2638,76 @@ pub const Effects = struct {
2400
2638
  var boosts = &foe.active.boosts;
2401
2639
 
2402
2640
  const move = Move.get(state.move);
2641
+ const none: Fail = .None;
2403
2642
  switch (move.effect) {
2404
2643
  .AttackDown1, .AttackDown2, .AttackDownChance => {
2405
2644
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2406
2645
  if (fail or boosts.atk == -6) {
2407
- return if (move.effect != .AttackDownChance) try log.fail(foe_ident, .None);
2646
+ return if (move.effect != .AttackDownChance) try log.fail(.{ foe_ident, none });
2408
2647
  } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2409
- try options.log.lastmiss();
2410
- try options.log.miss(battle.active(player));
2411
- return;
2648
+ try options.log.lastmiss(.{});
2649
+ return options.log.miss(.{battle.active(player)});
2412
2650
  } else if (stats.atk == 1) {
2413
- return if (move.effect != .AttackDownChance) try log.fail(foe_ident, .None);
2651
+ return if (move.effect != .AttackDownChance) try log.fail(.{ foe_ident, none });
2414
2652
  }
2415
2653
  const n: u2 = if (move.effect == .AttackDown2) 2 else 1;
2416
- boosts.atk = @intCast(@max(-6, @as(i8, boosts.atk) - n));
2417
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2418
- const stat = unmodifiedStats(battle, foe).atk;
2419
- stats.atk = @max(1, stat * mod[0] / mod[1]);
2420
- 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) });
2421
2656
  },
2422
2657
  .DefenseDown1, .DefenseDown2, .DefenseDownChance => {
2423
2658
  assert(boosts.def >= -6 and boosts.def <= 6);
2424
2659
  if (fail or boosts.def == -6) {
2425
- return if (move.effect != .DefenseDownChance) try log.fail(foe_ident, .None);
2426
- } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2427
- try options.log.lastmiss();
2428
- try options.log.miss(battle.active(player));
2660
+ if (move.effect != .DefenseDownChance) try log.fail(.{ foe_ident, none });
2429
2661
  return;
2662
+ } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2663
+ try options.log.lastmiss(.{});
2664
+ return options.log.miss(.{battle.active(player)});
2430
2665
  } else if (stats.def == 1) {
2431
- return if (move.effect != .DefenseDownChance) try log.fail(foe_ident, .None);
2666
+ if (move.effect != .DefenseDownChance) try log.fail(.{ foe_ident, none });
2667
+ return;
2432
2668
  }
2433
2669
  const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
2434
- boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
2435
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2436
- const stat = unmodifiedStats(battle, foe).def;
2437
- stats.def = @max(1, stat * mod[0] / mod[1]);
2438
- 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) });
2439
2672
  },
2440
2673
  .SpeedDown1, .SpeedDown2, .SpeedDownChance => {
2441
2674
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2442
2675
  if (fail or boosts.spe == -6) {
2443
- return if (move.effect != .SpeedDownChance) try log.fail(foe_ident, .None);
2676
+ return if (move.effect != .SpeedDownChance) try log.fail(.{ foe_ident, none });
2444
2677
  } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2445
- try options.log.lastmiss();
2446
- try options.log.miss(battle.active(player));
2447
- return;
2678
+ try options.log.lastmiss(.{});
2679
+ return options.log.miss(.{battle.active(player)});
2448
2680
  } else if (stats.spe == 1) {
2449
- return if (move.effect != .SpeedDownChance) try log.fail(foe_ident, .None);
2681
+ return if (move.effect != .SpeedDownChance) try log.fail(.{ foe_ident, none });
2450
2682
  }
2451
2683
  const n: u2 = if (move.effect == .SpeedDown2) 2 else 1;
2452
- boosts.spe = @intCast(@max(-6, @as(i8, boosts.spe) - 1));
2453
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2454
- const stat = unmodifiedStats(battle, foe).spe;
2455
- stats.spe = @max(1, stat * mod[0] / mod[1]);
2456
- try log.boost(foe_ident, .Speed, -@as(i8, n));
2457
- assert(boosts.spe >= -6);
2684
+ try unboostStat(battle, foe, stats, boosts, "spe", n);
2685
+ try log.boost(.{ foe_ident, .Speed, -@as(i8, n) });
2458
2686
  },
2459
2687
  .SpDefDownChance => {
2460
2688
  assert(boosts.spd >= -6 and boosts.spd <= 6);
2461
2689
  if (fail or boosts.spd == -6) return;
2462
2690
  if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2463
- try options.log.lastmiss();
2464
- try options.log.miss(battle.active(player));
2465
- return;
2691
+ try options.log.lastmiss(.{});
2692
+ return options.log.miss(.{battle.active(player)});
2466
2693
  } else if (stats.spd == 1) return;
2467
- boosts.spd = @intCast(@max(-6, @as(i8, boosts.spd) - 1));
2468
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.spd) + 6))];
2469
- const stat = unmodifiedStats(battle, foe).spd;
2470
- stats.spd = @max(1, stat * mod[0] / mod[1]);
2471
- try log.boost(foe_ident, .SpecialDefense, -1);
2694
+ try unboostStat(battle, foe, stats, boosts, "spd", 1);
2695
+ try log.boost(.{ foe_ident, .SpecialDefense, -1 });
2472
2696
  },
2473
2697
  .AccuracyDown1, .AccuracyDownChance => {
2474
2698
  assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
2475
2699
  if (fail or boosts.accuracy == -6) {
2476
- return if (move.effect != .AccuracyDownChance) try log.fail(foe_ident, .None);
2477
- } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2478
- try options.log.lastmiss();
2479
- try options.log.miss(battle.active(player));
2700
+ if (move.effect != .AccuracyDownChance) try log.fail(.{ foe_ident, none });
2480
2701
  return;
2702
+ } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2703
+ try options.log.lastmiss(.{});
2704
+ return options.log.miss(.{battle.active(player)});
2481
2705
  }
2482
2706
  boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
2483
- try log.boost(foe_ident, .Accuracy, -1);
2707
+ try log.boost(.{ foe_ident, .Accuracy, -1 });
2484
2708
  },
2485
2709
  else => unreachable,
2486
2710
  }
2487
-
2488
- statusModify(foe.stored().status, stats);
2489
2711
  }
2490
2712
  };
2491
2713
 
@@ -2495,6 +2717,15 @@ fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
2495
2717
  return &battle.side(id.player).pokemon[id.id - 1].stats;
2496
2718
  }
2497
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
+
2498
2729
  fn statusModify(status: u8, stats: *Stats(u16)) void {
2499
2730
  if (Status.is(status, .PAR)) {
2500
2731
  stats.spe = @max(stats.spe / 4, 1);
@@ -2503,6 +2734,52 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
2503
2734
  }
2504
2735
  }
2505
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
+
2506
2783
  fn isForced(active: anytype) bool {
2507
2784
  return active.volatiles.Recharging or active.volatiles.Rage or
2508
2785
  active.volatiles.Thrashing or active.volatiles.Charging or
@@ -2692,7 +2969,7 @@ pub const Rolls = struct {
2692
2969
  }
2693
2970
 
2694
2971
  fn magnitude(battle: anytype, player: Player, options: anytype) !u8 {
2695
- const num = if (options.calc.overridden(player, .magnitude)) |val|
2972
+ const num: u8 = if (options.calc.overridden(player, .magnitude)) |val|
2696
2973
  val + 3
2697
2974
  else if (showdown) num: {
2698
2975
  const r = battle.rng.range(u8, 0, 100);
@@ -2967,7 +3244,7 @@ pub const Rolls = struct {
2967
3244
 
2968
3245
  const move: Move = overridden orelse move: {
2969
3246
  if (showdown) {
2970
- 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)));
2971
3248
  for (moves) |m| {
2972
3249
  if (m.id == .None) break;
2973
3250
  if (@intFromEnum(m.id) > @intFromEnum(Move.METRONOME[r])) break;
@@ -3210,7 +3487,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3210
3487
  }
3211
3488
 
3212
3489
  var slot: u4 = 2;
3213
- while (!active.volatiles.trapped and slot <= 6) : (slot += 1) {
3490
+ while (!active.volatiles.Trapped and slot <= 6) : (slot += 1) {
3214
3491
  const id = side.order[slot - 1];
3215
3492
  if (id == 0 or side.pokemon[id - 1].hp == 0) continue;
3216
3493
  out[n] = .{ .type = .Switch, .data = slot };