@pkmn/engine 0.1.0-dev.9f75c7ae → 0.1.0-dev.a0167a9d

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.
Files changed (42) hide show
  1. package/LICENSE +1 -3
  2. package/README.md +44 -19
  3. package/build/pkg/addon.js +6 -7
  4. package/build/pkg/addon.js.map +1 -1
  5. package/build/pkg/data.d.ts +4 -4
  6. package/build/pkg/data.js.map +1 -1
  7. package/build/pkg/gen1.js.map +1 -1
  8. package/build/pkg/index.d.ts +2 -1
  9. package/build/pkg/index.js.map +1 -1
  10. package/build/pkg/protocol.js +5 -5
  11. package/build/pkg/protocol.js.map +1 -1
  12. package/build/test/display.js +2 -2
  13. package/build/test/display.js.map +1 -1
  14. package/build/tools/debug.js +5 -4
  15. package/build/tools/debug.js.map +1 -1
  16. package/build.zig +82 -90
  17. package/package.json +31 -23
  18. package/src/bin/install-pkmn-engine +26 -18
  19. package/src/lib/common/data.zig +4 -4
  20. package/src/lib/common/debug.zig +2 -2
  21. package/src/lib/common/optional.zig +1 -1
  22. package/src/lib/common/options.zig +2 -0
  23. package/src/lib/common/protocol.zig +393 -396
  24. package/src/lib/common/rational.zig +109 -15
  25. package/src/lib/common/rng.zig +11 -11
  26. package/src/lib/gen1/calc.zig +269 -97
  27. package/src/lib/gen1/chance.zig +126 -160
  28. package/src/lib/gen1/data/species.zig +1 -1
  29. package/src/lib/gen1/data.zig +21 -21
  30. package/src/lib/gen1/helpers.zig +191 -206
  31. package/src/lib/gen1/mechanics.zig +308 -285
  32. package/src/lib/gen2/chance.zig +211 -121
  33. package/src/lib/gen2/data.zig +13 -11
  34. package/src/lib/gen2/generated.zig +15 -24
  35. package/src/lib/gen2/helpers.zig +37 -7
  36. package/src/lib/gen2/mechanics.zig +855 -413
  37. package/src/lib/{binding/node.zig → node.zig} +9 -12
  38. package/src/lib/pkmn.zig +27 -12
  39. package/src/lib/{binding/wasm.zig → wasm.zig} +2 -1
  40. package/src/test/display/pkmn.html.tmpl +5 -5
  41. package/src/lib/gen1/README.md +0 -894
  42. package/src/lib/gen2/README.md +0 -330
@@ -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 {
@@ -136,11 +140,11 @@ fn start(battle: anytype, options: anytype) !Result {
136
140
  const p1 = battle.side(.P1);
137
141
  const p2 = battle.side(.P2);
138
142
 
139
- var p1_slot = findFirstAlive(p1);
143
+ const p1_slot = findFirstAlive(p1);
140
144
  assert(!showdown or p1_slot == 1);
141
145
  if (p1_slot == 0) return if (findFirstAlive(p2) == 0) Result.Tie else Result.Lose;
142
146
 
143
- var p2_slot = findFirstAlive(p2);
147
+ const p2_slot = findFirstAlive(p2);
144
148
  assert(!showdown or p2_slot == 1);
145
149
  if (p2_slot == 0) return Result.Win;
146
150
 
@@ -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
 
@@ -399,14 +403,22 @@ fn executeMove(
399
403
 
400
404
  assert(choice.type == .Move);
401
405
  var mslot: u4 = @intCast(choice.data);
406
+ _ = &mslot;
402
407
  var move = Move.Pound; // TODO
408
+ _ = &move;
403
409
 
404
410
  if (!try beforeMove(battle, player, move, options)) return null;
405
411
 
406
412
  // FIXME: need damage from doTurn
407
413
  const m = Move.get(move);
408
- var state: State =
409
- .{ .move = move, .mslot = mslot, .first = first, .accuracy = m.accuracy, .bp = m.bp };
414
+ var state: State = .{
415
+ .move = move,
416
+ .mslot = mslot,
417
+ .first = first,
418
+ .accuracy = m.accuracy,
419
+ .bp = m.bp,
420
+ .type = m.type,
421
+ };
410
422
  try generated.runMove(battle, player, &state, options);
411
423
  return null;
412
424
  }
@@ -423,7 +435,7 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
423
435
  if (volatiles.Recharging) {
424
436
  volatiles.Recharging = false;
425
437
  resetCant(volatiles);
426
- try log.cant(ident, .Recharge);
438
+ try log.cant(.{ ident, .Recharge });
427
439
  return false;
428
440
  }
429
441
 
@@ -442,12 +454,12 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
442
454
  }
443
455
 
444
456
  if (duration == 0) {
445
- try log.curestatus(ident, before, .Message);
457
+ try log.curestatus(.{ ident, before, .Message });
446
458
  stored.status = 0; // clears EXT if present
447
459
  resetCant(volatiles);
448
460
  volatiles.Nightmare = false;
449
461
  } else {
450
- try log.cant(ident, .Sleep);
462
+ try log.cant(.{ ident, .Sleep });
451
463
  if (move == .Snore or move == .SleepTalk) break :slp;
452
464
  resetCant(volatiles);
453
465
  return false;
@@ -456,7 +468,7 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
456
468
 
457
469
  if (Status.is(stored.status, .FRZ) and !(move == .FlameWheel or move == .SacredFire)) {
458
470
  resetCant(volatiles);
459
- try log.cant(ident, .Freeze);
471
+ try log.cant(.{ ident, .Freeze });
460
472
  return false;
461
473
  }
462
474
 
@@ -466,7 +478,7 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
466
478
  // if (!showdown) volatiles.Flinch = false;
467
479
  volatiles.Flinch = false;
468
480
  resetCant(volatiles);
469
- try log.cant(ident, .Flinch);
481
+ try log.cant(.{ ident, .Flinch });
470
482
  return false;
471
483
  }
472
484
 
@@ -480,7 +492,7 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
480
492
 
481
493
  if (volatiles.disable.duration == 0) {
482
494
  volatiles.disable.move = 0;
483
- try log.end(ident, .Disable);
495
+ try log.end(.{ ident, .Disable });
484
496
  }
485
497
  }
486
498
 
@@ -496,9 +508,9 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
496
508
 
497
509
  if (volatiles.confusion == 0) {
498
510
  volatiles.Confusion = false;
499
- try log.end(ident, .Confusion);
511
+ try log.end(.{ ident, .Confusion });
500
512
  } else {
501
- try log.activate(ident, .Confusion);
513
+ try log.activate(.{ ident, .Confusion });
502
514
 
503
515
  if (try Rolls.confused(battle, player, options)) {
504
516
  volatiles.BeatUp = false;
@@ -513,11 +525,11 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
513
525
  }
514
526
 
515
527
  if (volatiles.Attract) {
516
- try log.activate(ident, .Attract);
528
+ try log.activate(.{ ident, .Attract });
517
529
 
518
530
  if (try Rolls.attract(battle, player, options)) {
519
531
  resetCant(volatiles);
520
- try log.cant(ident, .Attract);
532
+ try log.cant(.{ ident, .Attract });
521
533
  return false;
522
534
  }
523
535
  }
@@ -528,14 +540,14 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
528
540
  if (m != .None and m == move) {
529
541
  side.active.volatiles.Charging = false;
530
542
  resetCant(volatiles);
531
- try options.log.disabled(ident, move);
543
+ try options.log.cant(.{ ident, .Disable, move });
532
544
  return false;
533
545
  }
534
546
  }
535
547
 
536
548
  if (Status.is(stored.status, .PAR) and try Rolls.paralyzed(battle, player, options)) {
537
549
  resetCant(volatiles);
538
- try log.cant(ident, .Paralysis);
550
+ try log.cant(.{ ident, .Paralysis });
539
551
  return false;
540
552
  }
541
553
 
@@ -555,11 +567,11 @@ fn resetCant(volatiles: *align(8) Volatiles) void {
555
567
 
556
568
  pub fn canMove(battle: anytype, player: Player, state: *State, options: anytype) !bool {
557
569
  var side = battle.side(player);
558
- var volatiles = &side.active.volatiles;
570
+ const volatiles = &side.active.volatiles;
559
571
 
560
572
  // FIXME: pass in target to avoid lookup?
561
573
  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);
574
+ try options.log.move(.{ battle.active(player), state.move, battle.active(target), state.from });
563
575
 
564
576
  const charging = false; // TODO charging uses doUse
565
577
  const skip_pp = charging or state.move == .Struggle or
@@ -638,7 +650,7 @@ pub fn afterMove(
638
650
 
639
651
  pub fn checkCriticalHit(battle: anytype, player: Player, state: *State, options: anytype) !void {
640
652
  const move = Move.get(state.move);
641
- if (move.bp == 0) return;
653
+ if (state.bp == 0) return;
642
654
 
643
655
  const side = battle.side(player);
644
656
  const pokemon = side.stored();
@@ -676,31 +688,30 @@ pub fn adjustDamage(battle: anytype, player: Player, state: *State, _: anytype)
676
688
  const side = battle.side(player);
677
689
  const foe = battle.foe(player);
678
690
  const types = foe.active.types;
679
- const move = Move.get(state.move);
680
691
 
681
692
  var d = state.damage;
682
- if ((move.type == .Water and battle.field.weather == .Rain) or
683
- (move.type == .Fire and battle.field.weather == .Sun))
693
+ if ((state.type == .Water and battle.field.weather == .Rain) or
694
+ (state.type == .Fire and battle.field.weather == .Sun))
684
695
  {
685
696
  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))
697
+ } else if ((state.type == .Water and battle.field.weather == .Sun) or
698
+ ((state.type == .Fire or state.move == .SolarBeam) and battle.field.weather == .Rain))
688
699
  {
689
700
  d = d *% @intFromEnum(Effectiveness.Resisted) / 10;
690
701
  }
691
702
 
692
- if (side.active.types.includes(move.type)) d +%= d / 2;
703
+ if (side.active.types.includes(state.type)) d +%= d / 2;
693
704
 
694
705
  const neutral = @intFromEnum(Effectiveness.Neutral);
695
- const foresight = foe.active.volatiles.Foresight and FORESIGHT.includes(move.type);
706
+ const foresight = foe.active.volatiles.Foresight and FORESIGHT.includes(state.type);
696
707
  const eff1: u16 = if (foresight and types.type1 == .Ghost)
697
708
  neutral
698
709
  else
699
- @intFromEnum(move.type.effectiveness(types.type1));
710
+ @intFromEnum(state.type.effectiveness(types.type1));
700
711
  const eff2: u16 = if (foresight and types.type2 == .Ghost)
701
712
  neutral
702
713
  else
703
- @intFromEnum(move.type.effectiveness(types.type2));
714
+ @intFromEnum(state.type.effectiveness(types.type2));
704
715
 
705
716
  // Type effectiveness matchup precedence only matters with (NVE, SE)
706
717
  if (!showdown and (eff1 + eff2) == Effectiveness.mismatch and
@@ -840,13 +851,13 @@ fn checkFaint(
840
851
  if (foe_fainted) try faint(battle, player.foe(), !more, options);
841
852
 
842
853
  if (player_out and foe_out) {
843
- try options.log.tie();
854
+ try options.log.tie(.{});
844
855
  return Result.Tie;
845
856
  } else if (player_out) {
846
- try options.log.win(player.foe());
857
+ try options.log.win(.{player.foe()});
847
858
  return if (player == .P1) Result.Lose else Result.Win;
848
859
  } else if (foe_out) {
849
- try options.log.win(player);
860
+ try options.log.win(.{player});
850
861
  return if (player == .P1) Result.Win else Result.Lose;
851
862
  }
852
863
 
@@ -876,19 +887,19 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !void {
876
887
  side.stored().stats.spe;
877
888
  }
878
889
 
879
- try options.log.faint(battle.active(player), done);
890
+ try options.log.faint(.{ battle.active(player), done });
880
891
  options.calc.capped(player);
881
892
  }
882
893
 
883
894
  // TODO: see if we can move *before* applyDamage and include failuretext logic
884
895
  pub fn reportOutcome(battle: anytype, player: Player, state: *State, options: anytype) !void {
885
896
  const foe_ident = battle.active(player.foe());
886
- if (state.crit) try options.log.crit(foe_ident);
897
+ if (state.crit) try options.log.crit(.{foe_ident});
887
898
  if (!state.immune()) {
888
899
  if (state.effectiveness > Effectiveness.neutral) {
889
- try options.log.supereffective(foe_ident);
900
+ try options.log.supereffective(.{foe_ident});
890
901
  } else if (state.effectiveness < Effectiveness.neutral) {
891
- try options.log.resisted(foe_ident);
902
+ try options.log.resisted(.{foe_ident});
892
903
  }
893
904
  }
894
905
  }
@@ -921,8 +932,8 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !bool {
921
932
  }
922
933
 
923
934
  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);
935
+ // Pokémon Showdown uses damage [of] here but its not relevant in Generation II
936
+ try options.log.damage(.{ ident, stored, if (brn) Damage.Burn else Damage.Poison });
926
937
  if (stored.hp == 0) return true;
927
938
  }
928
939
 
@@ -938,27 +949,27 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !bool {
938
949
 
939
950
  const damage = @max(stored.stats.hp / 8, 1);
940
951
  stored.hp -|= damage;
941
- // As above, Pokémon Showdown uses damageOf but its not relevant
942
- try options.log.damage(ident, stored, .LeechSeed);
952
+ // As above, Pokémon Showdown uses damage [of] but its not relevant
953
+ try options.log.damage(.{ ident, stored, .LeechSeed });
943
954
 
944
955
  const before = foe_stored.hp;
945
956
  foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
946
957
  // 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);
958
+ if (foe_stored.hp > before) try options.log.heal(.{ foe_ident, foe_stored, .Silent });
948
959
  if (stored.hp == 0) return true;
949
960
  }
950
961
 
951
962
  if (volatiles.Nightmare) {
952
963
  stored.hp -= @max(stored.stats.hp / 4, 1);
953
964
  // ibid
954
- try options.log.damage(ident, stored, .Nightmare);
965
+ try options.log.damage(.{ ident, stored, .Nightmare });
955
966
  if (stored.hp == 0) return true;
956
967
  }
957
968
 
958
969
  if (volatiles.Curse) {
959
970
  stored.hp -= @max(stored.stats.hp / 4, 1);
960
971
  // ibid
961
- try options.log.damage(ident, stored, .Curse);
972
+ try options.log.damage(.{ ident, stored, .Curse });
962
973
  if (stored.hp == 0) return true;
963
974
  }
964
975
 
@@ -979,7 +990,7 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
979
990
  future_sight.count -= 1;
980
991
  assert(future_sight.count != 0);
981
992
  if (future_sight.count == 1) {
982
- try options.log.end(battle.active(player), .FutureSight);
993
+ try options.log.end(.{ battle.active(player), .FutureSight });
983
994
  // TODO doMove
984
995
  }
985
996
  }
@@ -988,21 +999,24 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
988
999
 
989
1000
  // Weather
990
1001
  if (battle.field.weather != .None) {
991
- assert(battle.field.weather_duration > 0);
992
1002
  battle.field.weather_duration -= 1;
993
1003
 
994
1004
  if (battle.field.weather_duration == 0) {
995
1005
  battle.field.weather = .None;
996
- try options.log.weather(battle.field.weather, .None);
1006
+ try options.log.weather(.{ battle.field.weather, protocol.Weather.None });
997
1007
  } else {
998
- try options.log.weather(battle.field.weather, .Upkeep);
1008
+ try options.log.weather(.{ battle.field.weather, protocol.Weather.Upkeep });
999
1009
  if (battle.field.weather == .Sandstorm) {
1000
1010
  inline for (players) |player| {
1001
1011
  var side = battle.side(player);
1002
1012
  const active = side.active;
1003
1013
  if (!(active.volatiles.Underground or active.types.sandstormImmune())) {
1004
1014
  side.stored().hp -|= @max(side.stored().stats.hp / 8, 1);
1005
- try options.log.damage(battle.active(player), side.stored(), .Sandstorm);
1015
+ try options.log.damage(.{
1016
+ battle.active(player),
1017
+ side.stored(),
1018
+ .Sandstorm,
1019
+ });
1006
1020
  }
1007
1021
  }
1008
1022
  if (try checkFaint(battle, .P1, options)) |r| return r;
@@ -1014,19 +1028,24 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1014
1028
  inline for (players) |player| {
1015
1029
  var side = battle.side(player);
1016
1030
  var volatiles = &side.active.volatiles;
1031
+ const ident = battle.active(player);
1017
1032
 
1018
1033
  if (volatiles.bind.duration > 0 and !volatiles.Substitute) {
1019
1034
  volatiles.bind.duration -= 1;
1020
1035
  if (volatiles.bind.duration == 0) {
1021
1036
  const reason = @intFromEnum(End.Bind) + volatiles.bind.reason - 1;
1022
- try options.log.end(battle.active(player), @enumFromInt(reason));
1037
+ try options.log.end(.{ ident, @as(End, @enumFromInt(reason)) });
1023
1038
  } else {
1024
1039
  var reason = @intFromEnum(Activate.Bind) + volatiles.bind.reason - 1;
1025
- try options.log.activate(battle.active(player), @enumFromInt(reason));
1040
+ try options.log.activate(.{ ident, @as(Activate, @enumFromInt(reason)) });
1026
1041
 
1027
1042
  reason = @intFromEnum(Damage.Bind) + volatiles.bind.reason - 1;
1028
1043
  side.stored().hp -|= @max(side.stored().stats.hp / 16, 1);
1029
- try options.log.damage(battle.active(player), side.stored(), @enumFromInt(reason));
1044
+ try options.log.damage(.{
1045
+ ident,
1046
+ side.stored(),
1047
+ @as(Damage, @enumFromInt(reason)),
1048
+ });
1030
1049
  }
1031
1050
  }
1032
1051
  }
@@ -1038,12 +1057,11 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1038
1057
  var volatiles = &side.active.volatiles;
1039
1058
 
1040
1059
  if (volatiles.PerishSong) {
1041
- assert(volatiles.perish_song > 0);
1042
1060
  volatiles.perish_song -= 1;
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,13 +1078,15 @@ 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
 
1067
1087
  // Mystery Berry
1068
1088
  inline for (players) |player| {
1069
- var stored = battle.side(player).stored();
1089
+ const stored = battle.side(player).stored();
1070
1090
 
1071
1091
  if (stored.item == .MysteryBerry) {
1072
1092
  // TODO stored and active moves, transform, etc
@@ -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,29 +1159,29 @@ 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?
1146
1166
  // side.active.volatiles.Toxic = false;
1147
- // side.active.volatiles.NightMare = false;
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
  }
@@ -1173,12 +1193,11 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1173
1193
  var volatiles = &side.active.volatiles;
1174
1194
 
1175
1195
  if (volatiles.Encore) {
1176
- assert(volatiles.encore > 0);
1177
1196
  volatiles.encore -= 1;
1178
1197
 
1179
1198
  if (volatiles.encore == 0 or side.active.move(mslot).pp == 0) {
1180
1199
  volatiles.Encore = false;
1181
- try options.log.end(battle.active(player), .Encore);
1200
+ try options.log.end(.{ battle.active(player), .Encore });
1182
1201
  }
1183
1202
  }
1184
1203
  }
@@ -1195,12 +1214,11 @@ fn updateSideCondition(
1195
1214
  ) !void {
1196
1215
  var side = battle.side(player);
1197
1216
  if (@field(side.conditions, key)) {
1198
- assert(@field(side.conditions, value) > 0);
1199
1217
  @field(side.conditions, value) -= 1;
1200
1218
 
1201
1219
  if (@field(side.conditions, value) == 0) {
1202
1220
  @field(side.conditions, key) = false;
1203
- try options.log.sideend(player, @field(protocol.Side, key));
1221
+ try options.log.sideend(.{ player, @field(protocol.Side, key) });
1204
1222
  }
1205
1223
  }
1206
1224
  }
@@ -1209,13 +1227,13 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
1209
1227
  battle.turn += 1;
1210
1228
 
1211
1229
  if (showdown and battle.turn >= 1000) {
1212
- try options.log.tie();
1230
+ try options.log.tie(.{});
1213
1231
  return Result.Tie;
1214
1232
  } else if (battle.turn >= 65535) {
1215
1233
  return Result.Error;
1216
1234
  }
1217
1235
 
1218
- try options.log.turn(battle.turn);
1236
+ try options.log.turn(.{battle.turn});
1219
1237
 
1220
1238
  return Result.Default;
1221
1239
  }
@@ -1231,7 +1249,7 @@ pub fn buildRage(battle: anytype, player: Player, state: *State, options: anytyp
1231
1249
  }
1232
1250
  } else {
1233
1251
  foe.active.volatiles.rage +|= 1;
1234
- try options.log.activate(battle.active(player), .Rage);
1252
+ try options.log.activate(.{ battle.active(player), .Rage });
1235
1253
  }
1236
1254
  }
1237
1255
  }
@@ -1254,16 +1272,12 @@ pub fn destinyBond(battle: anytype, player: Player, _: *State, options: anytype)
1254
1272
 
1255
1273
  if (foe.active.volatiles.DestinyBond) {
1256
1274
  battle.side(player).stored().hp = 0;
1257
- try options.log.activate(battle.active(player.foe()), .DestinyBond);
1275
+ try options.log.activate(.{ battle.active(player.foe()), .DestinyBond });
1258
1276
  }
1259
1277
  return true;
1260
1278
  }
1261
1279
 
1262
1280
  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
1281
  pub fn attract(battle: anytype, player: Player, _: *State, options: anytype) !void {
1268
1282
  const side = battle.side(player);
1269
1283
  var foe = battle.foe(player);
@@ -1271,16 +1285,14 @@ pub const Effects = struct {
1271
1285
  const gender: u8 = @intFromEnum(side.stored().dvs.gender);
1272
1286
  const foe_gender: u8 = @intFromEnum(foe.stored().dvs.gender);
1273
1287
  if (gender + foe_gender != 1) {
1274
- try options.log.immune(battle.active(player.foe()), .None);
1275
- return;
1288
+ return options.log.immune(.{ battle.active(player.foe()), .None });
1276
1289
  } 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;
1290
+ try options.log.lastmiss(.{});
1291
+ return options.log.miss(.{battle.active(player)});
1280
1292
  }
1281
1293
 
1282
1294
  foe.active.volatiles.Attract = true;
1283
- try options.log.start(battle.active(player.foe()), .Attract);
1295
+ try options.log.start(.{ battle.active(player.foe()), .Attract });
1284
1296
  }
1285
1297
 
1286
1298
  pub fn batonPass(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1292,7 +1304,32 @@ pub const Effects = struct {
1292
1304
  }
1293
1305
 
1294
1306
  pub fn bellyDrum(battle: anytype, player: Player, state: *State, options: anytype) !void {
1295
- _ = .{ battle, player, state, options }; // TODO
1307
+ assert(!state.miss);
1308
+ var side = battle.side(player);
1309
+
1310
+ const ident = battle.active(player);
1311
+
1312
+ assert(side.active.boosts.atk >= -6 and side.active.boosts.atk <= 6);
1313
+ if (side.active.boosts.atk == 6 or side.active.boosts.atk == MAX_STAT_VALUE) {
1314
+ return options.log.fail(.{ ident, .None });
1315
+ }
1316
+
1317
+ const hp = @max(side.stored().stats.hp / 2, 1);
1318
+ if (side.stored().hp <= hp) {
1319
+ try boostStat(battle, side, &side.active.stats, &side.active.boosts, "atk", 2);
1320
+ try options.log.boost(.{ ident, .Attack, @as(i8, 2), .Silent });
1321
+ return options.log.fail(.{ ident, .None });
1322
+ } else {
1323
+ side.stored().hp -= hp;
1324
+ try options.log.damage(.{ ident, side.stored(), .None });
1325
+
1326
+ const before = side.active.boosts.atk;
1327
+ var i: usize = 0;
1328
+ while (i < 6 and side.active.stats.atk < MAX_STAT_VALUE) : (i += 1) {
1329
+ try boostStat(battle, side, &side.active.stats, &side.active.boosts, "atk", 2);
1330
+ }
1331
+ try options.log.boost(.{ ident, .Attack, @as(i8, side.active.boosts.atk - before) });
1332
+ }
1296
1333
  }
1297
1334
 
1298
1335
  pub const bide = struct {
@@ -1304,7 +1341,6 @@ pub const Effects = struct {
1304
1341
 
1305
1342
  if (!volatiles.Bide) return;
1306
1343
 
1307
- assert(volatiles.attacks > 0);
1308
1344
  // if (options.calc.modify(player, .attacking)) |extend| {
1309
1345
  // if (!extend) volatiles.attacks = 0;
1310
1346
  // } else {
@@ -1312,10 +1348,10 @@ pub const Effects = struct {
1312
1348
  // }
1313
1349
  try options.chance.attacking(player, volatiles.attacks);
1314
1350
 
1315
- if (volatiles.attacks != 0) return try options.log.activate(ident, .Bide);
1351
+ if (volatiles.attacks != 0) return options.log.activate(.{ ident, .Bide });
1316
1352
 
1317
1353
  volatiles.Bide = false;
1318
- try options.log.end(ident, .Bide);
1354
+ try options.log.end(.{ ident, .Bide });
1319
1355
 
1320
1356
  state.damage = volatiles.bide *% 2;
1321
1357
  volatiles.bide = 0;
@@ -1329,7 +1365,16 @@ pub const Effects = struct {
1329
1365
  };
1330
1366
 
1331
1367
  pub fn binding(battle: anytype, player: Player, state: *State, options: anytype) !void {
1332
- _ = .{ battle, player, state, options }; // TODO
1368
+ var foe = battle.foe(player);
1369
+
1370
+ if (state.miss) return;
1371
+ if (foe.active.volatiles.bind.duration > 0) return;
1372
+ if (foe.active.volatiles.Substitute) return;
1373
+
1374
+ foe.active.volatiles.bind = .{
1375
+ .duration = Rolls.bindingDuration(battle, player, options),
1376
+ .reason = Move.get(state.move).extra.protocol,
1377
+ };
1333
1378
  }
1334
1379
 
1335
1380
  pub fn burnChance(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1339,12 +1384,12 @@ pub const Effects = struct {
1339
1384
  if (foe.active.volatiles.Substitute) return;
1340
1385
  if (Status.any(foe_stored.status)) {
1341
1386
  if (Status.is(foe_stored.status, .FRZ)) {
1342
- assert(Move.get(state.move).type == .Fire);
1343
- try options.log.curestatus(
1387
+ assert(Move.get(state.move).type == .Fire or state.move == .TriAttack);
1388
+ try options.log.curestatus(.{
1344
1389
  battle.active(player.foe()),
1345
1390
  foe_stored.status,
1346
1391
  .Message,
1347
- );
1392
+ });
1348
1393
  foe_stored.status = 0;
1349
1394
  }
1350
1395
  return;
@@ -1353,11 +1398,14 @@ pub const Effects = struct {
1353
1398
  if (foe.conditions.Safeguard) return;
1354
1399
  // Nothing is immune to Fire-type, but Ghosts are immune to burnChance called by Tri Attack
1355
1400
  if (state.immune()) return;
1356
- if (foe.active.types.includes(Move.get(state.move).type)) return;
1401
+ if (foe.active.types.includes(Move.get(state.move).type) and state.move != .TriAttack) {
1402
+ return;
1403
+ }
1357
1404
 
1358
1405
  foe_stored.status = Status.init(.BRN);
1359
1406
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
1360
- try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
1407
+
1408
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
1361
1409
 
1362
1410
  // TODO: check for status berry
1363
1411
  }
@@ -1377,10 +1425,7 @@ pub const Effects = struct {
1377
1425
  for (side.active.moves) |m| {
1378
1426
  if (convertible(side.active, m.id)) n += 1;
1379
1427
  }
1380
- if (n == 0) {
1381
- try options.log.fail(battle.active(player), .None);
1382
- return;
1383
- }
1428
+ if (n == 0) return options.log.fail(.{ battle.active(player), .None });
1384
1429
 
1385
1430
  side.active.types.type1 = try Rolls.conversion(battle, player, n, options);
1386
1431
  side.active.types.type2 = side.active.types.type1;
@@ -1390,26 +1435,87 @@ pub const Effects = struct {
1390
1435
  var side = battle.side(player);
1391
1436
 
1392
1437
  const last = battle.foe(player).lastMove(false);
1393
- if (last == .None) {
1394
- try options.log.fail(battle.active(player), .None);
1395
- return;
1396
- }
1438
+ if (last == .None) return options.log.fail(.{ battle.active(player), .None });
1397
1439
  const move = Move.get(last);
1398
- if (move.type == .@"???") {
1399
- try options.log.fail(battle.active(player), .None);
1400
- return;
1401
- }
1440
+ if (move.type == .@"???") return options.log.fail(.{ battle.active(player), .None });
1402
1441
 
1403
1442
  side.active.types.type1 = try Rolls.conversion2(battle, player, move.type, options);
1404
1443
  side.active.types.type2 = side.active.types.type1;
1405
1444
  }
1406
1445
 
1407
- pub fn counter(battle: anytype, player: Player, state: *State, options: anytype) !void {
1408
- _ = .{ battle, player, state, options }; // TODO
1446
+ pub fn counter(battle: anytype, player: Player, state: *State, _: anytype) !void {
1447
+ assert(state.move == .Counter or state.move == .MirrorCoat);
1448
+
1449
+ const foe = battle.foe(player);
1450
+
1451
+ state.miss = true;
1452
+ const last = foe.lastMove(false);
1453
+
1454
+ if (last == .None) return;
1455
+ if (last == state.move) return;
1456
+
1457
+ const immune = state.immune();
1458
+ state.effectiveness = Effectiveness.neutral;
1459
+ if (immune) {
1460
+ state.damage = 0;
1461
+ return;
1462
+ }
1463
+
1464
+ const move = Move.get(last);
1465
+ if (state.bp == 0) return;
1466
+ if (move.type.special() != (last == .MirrorCoat)) return;
1467
+ if (state.damage == 0) return;
1468
+
1469
+ state.damage *|= 2;
1470
+ state.miss = false;
1409
1471
  }
1410
1472
 
1411
1473
  pub fn curse(battle: anytype, player: Player, state: *State, options: anytype) !void {
1412
- _ = .{ battle, player, state, options }; // TODO
1474
+ var side = battle.side(player);
1475
+ var foe = battle.foe(player);
1476
+
1477
+ const ident = battle.active(player);
1478
+ const foe_ident = battle.active(player.foe());
1479
+
1480
+ if (side.active.types.includes(.Ghost)) {
1481
+ if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
1482
+ try options.log.lastmiss(.{});
1483
+ return options.log.miss(.{battle.active(player)});
1484
+ } else if (foe.active.volatiles.Substitute) {
1485
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
1486
+ } else if (foe.active.volatiles.Curse) {
1487
+ return options.log.fail(.{ foe_ident, .None });
1488
+ }
1489
+
1490
+ foe.active.volatiles.Curse = true;
1491
+ try options.log.start(.{ foe_ident, .Curse });
1492
+
1493
+ side.stored().hp -= @max(side.stored().stats.hp / 2, 1);
1494
+ try options.log.damage(.{ ident, side.stored(), .Curse });
1495
+ } else {
1496
+ const stats = &side.active.stats;
1497
+ const boosts = &side.active.boosts;
1498
+
1499
+ const max_atk = boosts.atk == 6 or stats.atk == MAX_STAT_VALUE;
1500
+ const max_def = boosts.def == 6 or stats.def == MAX_STAT_VALUE;
1501
+ if (max_atk and max_def) return options.log.fail(.{ ident, .None });
1502
+
1503
+ if (!(boosts.spe == -6 or stats.spe == 1)) {
1504
+ assert(boosts.spe >= -6 and boosts.spe <= 6);
1505
+ try unboostStat(battle, side, stats, boosts, "spe", 1);
1506
+ try options.log.boost(.{ ident, .Speed, @as(i8, -1) });
1507
+ }
1508
+ if (!max_atk) {
1509
+ assert(boosts.atk >= -6 and boosts.atk <= 6);
1510
+ try boostStat(battle, side, stats, boosts, "atk", 1);
1511
+ try options.log.boost(.{ ident, .Attack, @as(i8, 1) });
1512
+ }
1513
+ if (!max_def) {
1514
+ assert(boosts.def >= -6 and boosts.def <= 6);
1515
+ try boostStat(battle, side, stats, boosts, "def", 1);
1516
+ try options.log.boost(.{ ident, .Defense, @as(i8, 1) });
1517
+ }
1518
+ }
1413
1519
  }
1414
1520
 
1415
1521
  pub fn defenseCurl(battle: anytype, player: Player, _: *State, _: anytype) !void {
@@ -1419,14 +1525,13 @@ pub const Effects = struct {
1419
1525
  pub fn defrost(battle: anytype, player: Player, _: *State, options: anytype) !void {
1420
1526
  var side = battle.side(player);
1421
1527
  assert(Status.is(side.stored().status, .FRZ));
1422
-
1423
- try options.log.curestatus(battle.active(player), side.stored().status, .Message);
1528
+ try options.log.curestatus(.{ battle.active(player), side.stored().status, .Message });
1424
1529
  side.stored().status = 0;
1425
1530
  }
1426
1531
 
1427
1532
  pub fn destinyBond(battle: anytype, player: Player, _: *State, options: anytype) !void {
1428
1533
  battle.side(player).active.volatiles.DestinyBond = true;
1429
- try options.log.singlemove(battle.active(player), Move.DestinyBond);
1534
+ try options.log.singlemove(.{ battle.active(player), Move.DestinyBond });
1430
1535
  }
1431
1536
 
1432
1537
  pub fn disable(battle: anytype, player: Player, _: *State, options: anytype) !void {
@@ -1434,16 +1539,10 @@ pub const Effects = struct {
1434
1539
  var volatiles = &foe.active.volatiles;
1435
1540
  const foe_ident = battle.active(player.foe());
1436
1541
 
1437
- if (volatiles.disable.move != 0) {
1438
- try options.log.fail(foe_ident, .None);
1439
- return;
1440
- }
1542
+ if (volatiles.disable.move != 0) return options.log.fail(.{ foe_ident, .None });
1441
1543
 
1442
1544
  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
- }
1545
+ if (last == .None or last == .Struggle) return options.log.fail(.{ foe_ident, .None });
1447
1546
 
1448
1547
  var slot: u3 = 0;
1449
1548
  for (foe.active.moves) |m| {
@@ -1452,15 +1551,12 @@ pub const Effects = struct {
1452
1551
  if (m.id == last) break;
1453
1552
  }
1454
1553
  const move = foe.active.move(slot);
1455
- if (move.pp == 0) {
1456
- try options.log.fail(foe_ident, .None);
1457
- return;
1458
- }
1554
+ if (move.pp == 0) return options.log.fail(.{ foe_ident, .None });
1459
1555
 
1460
1556
  volatiles.disable.move = slot;
1461
1557
  volatiles.disable.duration = Rolls.disableDuration(battle, player, options);
1462
1558
 
1463
- try options.log.startEffect(foe_ident, .Disable, move.id);
1559
+ try options.log.start(.{ foe_ident, .Disable, move.id });
1464
1560
  }
1465
1561
 
1466
1562
  pub const double = struct {
@@ -1479,13 +1575,14 @@ pub const Effects = struct {
1479
1575
 
1480
1576
  pub fn drainHP(battle: anytype, player: Player, state: *State, options: anytype) !void {
1481
1577
  var stored = battle.side(player).stored();
1578
+ const ident = battle.active(player);
1482
1579
 
1483
1580
  assert(state.damage != 0);
1484
1581
 
1485
1582
  if (stored.hp == stored.stats.hp) return;
1486
1583
  stored.hp = @min(stored.stats.hp, stored.hp + @max(state.damage / 2, 1));
1487
1584
 
1488
- try options.log.drain(battle.active(player), stored, battle.active(player.foe()));
1585
+ try options.log.heal(.{ ident, stored, .Drain, battle.active(player.foe()) });
1489
1586
  }
1490
1587
 
1491
1588
  pub fn encore(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1497,10 +1594,7 @@ pub const Effects = struct {
1497
1594
  const failed = volatiles.Encore or
1498
1595
  (last == .None or last == .Struggle or
1499
1596
  last == .Encore or .last == .MirrorMove);
1500
- if (failed) {
1501
- try options.log.fail(foe_ident, .None);
1502
- return;
1503
- }
1597
+ if (failed) return options.log.fail(.{ foe_ident, .None });
1504
1598
 
1505
1599
  var slot: u3 = 0;
1506
1600
  for (foe.active.moves) |m| {
@@ -1509,10 +1603,7 @@ pub const Effects = struct {
1509
1603
  if (m.id == last) break;
1510
1604
  }
1511
1605
  const move = foe.active.move(slot);
1512
- if (move.pp == 0) {
1513
- try options.log.fail(foe_ident, .None);
1514
- return;
1515
- }
1606
+ if (move.pp == 0) return options.log.fail(.{ foe_ident, .None });
1516
1607
 
1517
1608
  volatiles.Encore = true;
1518
1609
  volatiles.encore = Rolls.encoreDuration(battle, player, options);
@@ -1520,11 +1611,19 @@ pub const Effects = struct {
1520
1611
  // TODO ???
1521
1612
  _ = state;
1522
1613
 
1523
- try options.log.startEffect(foe_ident, .Encore, move.id);
1614
+ try options.log.start(.{ foe_ident, .Encore, move.id });
1524
1615
  }
1525
1616
 
1526
- pub fn explode(battle: anytype, player: Player, state: *State, options: anytype) !void {
1527
- _ = .{ battle, player, state, options }; // TODO
1617
+ pub fn explode(battle: anytype, player: Player, _: *State, _: anytype) !void {
1618
+ var side = battle.side(player);
1619
+ var stored = side.stored();
1620
+
1621
+ stored.hp = 0;
1622
+ // Pokémon Showdown sets the status to 0 on faint(), and we need to do the same to be able
1623
+ // to correctly implement Pokémon Showdown's dumb speed-based switch mechanics
1624
+ if (!showdown) stored.status = 0;
1625
+ side.active.volatiles.LeechSeed = false;
1626
+ side.active.volatiles.DestinyBond = false;
1528
1627
  }
1529
1628
 
1530
1629
  pub fn falseSwipe(battle: anytype, player: Player, state: *State, _: anytype) !void {
@@ -1552,10 +1651,6 @@ pub const Effects = struct {
1552
1651
  };
1553
1652
  }
1554
1653
 
1555
- pub fn flameWheel(battle: anytype, player: Player, state: *State, options: anytype) !void {
1556
- _ = .{ battle, player, state, options }; // TODO
1557
- }
1558
-
1559
1654
  pub fn flinchChance(battle: anytype, player: Player, state: *State, _: anytype) !void {
1560
1655
  const foe = battle.foe(player);
1561
1656
  const status = foe.stored().status;
@@ -1578,12 +1673,11 @@ pub const Effects = struct {
1578
1673
  var side = battle.side(player);
1579
1674
 
1580
1675
  if (side.active.volatiles.FocusEnergy) {
1581
- if (!showdown) try options.log.fail(battle.active(player), .None);
1582
- return;
1676
+ return if (!showdown) try options.log.fail(.{ battle.active(player), .None });
1583
1677
  }
1584
1678
 
1585
1679
  side.active.volatiles.FocusEnergy = true;
1586
- try options.log.start(battle.active(player), .FocusEnergy);
1680
+ try options.log.start(.{ battle.active(player), .FocusEnergy });
1587
1681
  }
1588
1682
 
1589
1683
  var SLOTS: [5]u4 = .{ 0, 0, 0, 0, 0 };
@@ -1593,7 +1687,7 @@ pub const Effects = struct {
1593
1687
 
1594
1688
  const foe = battle.foe(player);
1595
1689
 
1596
- if (state.first) return try options.log.fail(battle.active(player), .None);
1690
+ if (state.first) return options.log.fail(.{ battle.active(player), .None });
1597
1691
 
1598
1692
  var n: u4 = 0;
1599
1693
  var i: u4 = 0;
@@ -1607,10 +1701,7 @@ pub const Effects = struct {
1607
1701
  }
1608
1702
  }
1609
1703
 
1610
- if (n == 0) {
1611
- try options.log.fail(battle.active(player), .None);
1612
- return;
1613
- }
1704
+ if (n == 0) return options.log.fail(.{ battle.active(player), .None });
1614
1705
 
1615
1706
  const slot = try Rolls.forceSwitch(battle, player, SLOTS[0..i], n, options);
1616
1707
  try switchIn(battle, player.foe(), slot, .Drag, options);
@@ -1620,16 +1711,14 @@ pub const Effects = struct {
1620
1711
  var foe = battle.foe(player);
1621
1712
 
1622
1713
  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;
1714
+ try options.log.lastmiss(.{});
1715
+ return options.log.miss(.{battle.active(player)});
1626
1716
  } else if (foe.active.volatiles.Foresight) {
1627
- try options.log.fail(battle.active(player), .None);
1628
- return;
1717
+ return options.log.fail(.{ battle.active(player), .None });
1629
1718
  }
1630
1719
 
1631
1720
  foe.active.volatiles.Foresight = true;
1632
- try options.log.start(battle.active(player.foe()), .FocusEnergy);
1721
+ try options.log.start(.{ battle.active(player.foe()), .FocusEnergy });
1633
1722
  }
1634
1723
 
1635
1724
  pub fn freezeChance(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1641,15 +1730,18 @@ pub const Effects = struct {
1641
1730
  if (Status.any(foe_stored.status)) return;
1642
1731
 
1643
1732
  // Nothing is immune to Ice-type, but Ghosts are immune to freezeChance called by Tri Attack
1733
+ if (state.immune()) return;
1644
1734
  if (battle.field.weather == .Sun) return;
1645
- if (foe.active.types.includes(Move.get(state.move).type)) return;
1735
+ if (foe.active.types.includes(Move.get(state.move).type) and state.move != .TriAttack) {
1736
+ return;
1737
+ }
1646
1738
  if (!state.proc) return;
1647
1739
  if (foe.conditions.Safeguard) return;
1648
1740
  // Freeze Clause Mod
1649
1741
  if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1650
1742
 
1651
1743
  foe_stored.status = Status.init(.FRZ);
1652
- try options.log.status(foe_ident, foe_stored.status, .None);
1744
+ try options.log.status(.{ foe_ident, foe_stored.status, .None });
1653
1745
 
1654
1746
  // TODO: check for status berry
1655
1747
 
@@ -1657,8 +1749,26 @@ pub const Effects = struct {
1657
1749
  foe.active.volatiles.frozen = true;
1658
1750
  }
1659
1751
 
1660
- pub fn furyCutter(battle: anytype, player: Player, state: *State, options: anytype) !void {
1661
- _ = .{ battle, player, state, options }; // TODO
1752
+ // TODO FIXME showdown calls this before calcDamage (after checkHit) and modifies state.bp!
1753
+ pub fn furyCutter(battle: anytype, player: Player, state: *State, _: anytype) !void {
1754
+ var side = battle.side(player);
1755
+ var volatiles = &side.active.volatiles;
1756
+
1757
+ if (state.miss) {
1758
+ volatiles.fury_cutter = 0;
1759
+ return;
1760
+ }
1761
+
1762
+ assert(volatiles.fury_cutter >= 0 and volatiles.fury_cutter <= 5);
1763
+ if (volatiles.fury_cutter == 5) return;
1764
+ volatiles.fury_cutter += 1;
1765
+
1766
+ if (showdown) {
1767
+ state.bp *= 2;
1768
+ assert(state.bp <= 160);
1769
+ } else {
1770
+ state.damage *|= 2;
1771
+ }
1662
1772
  }
1663
1773
 
1664
1774
  pub fn futureSight(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1671,73 +1781,222 @@ pub const Effects = struct {
1671
1781
  state.bp = ((if (frustration) 255 - val else val) * 10) / 25;
1672
1782
  }
1673
1783
 
1674
- pub fn haze(battle: anytype, player: Player, state: *State, options: anytype) !void {
1675
- _ = .{ battle, player, state, options }; // TODO
1784
+ pub fn haze(battle: anytype, player: Player, _: *State, options: anytype) !void {
1785
+ var side = battle.side(player);
1786
+ var stats = unmodifiedStats(battle, side);
1787
+ side.active.boosts = .{};
1788
+ inline for (STATS) |s| @field(side.active.stats, s) = @field(stats, s);
1789
+ statusModify(side.stored().status, &side.active.stats);
1790
+
1791
+ var foe = battle.foe(player);
1792
+ stats = unmodifiedStats(battle, foe);
1793
+ foe.active.boosts = .{};
1794
+ inline for (STATS) |s| @field(foe.active.stats, s) = @field(stats, s);
1795
+ statusModify(foe.stored().status, &foe.active.stats);
1796
+
1797
+ try options.log.clearallboost(.{});
1676
1798
  }
1677
1799
 
1678
1800
  pub fn heal(battle: anytype, player: Player, state: *State, options: anytype) !void {
1679
- _ = .{ battle, player, state, options }; // TODO
1680
- }
1801
+ var side = battle.side(player);
1802
+ var stored = side.stored();
1803
+ const ident = battle.active(player);
1681
1804
 
1682
- pub fn healBell(battle: anytype, player: Player, state: *State, options: anytype) !void {
1683
- _ = .{ battle, player, state, options }; // TODO
1805
+ if (stored.stats.hp == stored.hp) return options.log.fail(.{ ident, .None });
1806
+
1807
+ const rest = state.move == .Rest;
1808
+ if (rest) {
1809
+ // Adding the sleep status runs the sleep condition handler to roll duration
1810
+ if (showdown) battle.rng.advance(1);
1811
+ side.active.volatiles.Toxic = false;
1812
+ stored.status = Status.slf(2);
1813
+ recomputeStats(battle, side);
1814
+ try options.log.status(.{ ident, stored.status, .From, Move.Rest });
1815
+ stored.hp = stored.stats.hp;
1816
+ } else {
1817
+ stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
1818
+ }
1819
+ try options.log.heal(.{ ident, stored, if (rest) Heal.Silent else Heal.None });
1684
1820
  }
1685
1821
 
1686
- pub fn hiddenPower(battle: anytype, player: Player, state: *State, options: anytype) !void {
1687
- _ = .{ battle, player, state, options }; // TODO
1822
+ pub fn healBell(battle: anytype, player: Player, _: *State, options: anytype) !void {
1823
+ var side = battle.side(player);
1824
+ side.active.volatiles.Nightmare = false;
1825
+
1826
+ try options.log.cureteam(.{battle.active(player)});
1827
+ for (&side.pokemon) |*p| p.status = 0;
1828
+
1829
+ // We technically only need to recompute stats if the active Pokémon was burned/paralyzed,
1830
+ // but adding a branch to check for that is probably more expensive than just recomputing
1831
+ recomputeStats(battle, side);
1688
1832
  }
1689
1833
 
1690
- pub fn hyperBeam(battle: anytype, player: Player, state: *State, options: anytype) !void {
1691
- _ = .{ battle, player, state, options }; // TODO
1834
+ pub fn hiddenPower(battle: anytype, player: Player, state: *State, _: anytype) !void {
1835
+ const side = battle.side(player);
1836
+ state.bp = side.stored().dvs.power();
1837
+ state.type = side.stored().dvs.type;
1692
1838
  }
1693
1839
 
1694
- pub fn jumpKick(battle: anytype, player: Player, state: *State, options: anytype) !void {
1695
- _ = .{ battle, player, state, options }; // TODO
1840
+ pub fn hyperBeam(battle: anytype, player: Player, _: *State, options: anytype) !void {
1841
+ battle.side(player).active.volatiles.Recharging = true;
1842
+ try options.log.mustrecharge(.{battle.active(player)});
1696
1843
  }
1697
1844
 
1698
1845
  pub fn leechSeed(battle: anytype, player: Player, state: *State, options: anytype) !void {
1699
- _ = .{ battle, player, state, options }; // TODO
1846
+ var foe = battle.foe(player);
1847
+
1848
+ if (showdown) {
1849
+ // Invulnerability trumps type immunity on Pokémon Showdown
1850
+ const invulnerable = foe.active.volatiles.Underground or foe.active.volatiles.Flying;
1851
+ if (!invulnerable and foe.active.types.includes(.Grass)) {
1852
+ return options.log.immune(.{ battle.active(player.foe()), .None });
1853
+ }
1854
+ try checkHit(battle, player, state, options);
1855
+ if (state.miss) {
1856
+ try options.log.lastmiss(.{});
1857
+ return options.log.miss(.{battle.active(player)});
1858
+ }
1859
+ if (foe.active.volatiles.LeechSeed) return;
1860
+ } else {
1861
+ try checkHit(battle, player, state, options);
1862
+ const miss =
1863
+ state.miss or foe.active.types.includes(.Grass) or foe.active.volatiles.LeechSeed;
1864
+ if (miss) {
1865
+ try options.log.lastmiss(.{});
1866
+ return options.log.miss(.{battle.active(player)});
1867
+ }
1868
+ }
1869
+
1870
+ foe.active.volatiles.LeechSeed = true;
1871
+
1872
+ try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
1700
1873
  }
1701
1874
 
1702
1875
  pub fn lockOn(battle: anytype, player: Player, state: *State, options: anytype) !void {
1703
- _ = .{ battle, player, state, options }; // TODO
1876
+ var foe = battle.foe(player);
1877
+ const foe_ident = battle.active(player.foe());
1878
+
1879
+ if (foe.active.volatiles.Substitute) {
1880
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
1881
+ } else if (state.miss) {
1882
+ return options.log.fail(.{ foe_ident, .None });
1883
+ }
1884
+
1885
+ foe.active.volatiles.LockOn = true;
1886
+ try options.log.activate(.{ battle.active(player), .LockOn });
1704
1887
  }
1705
1888
 
1706
1889
  const MAGNITUDE_POWER = [_]u8{ 10, 30, 50, 70, 90, 110, 150 };
1707
1890
 
1708
1891
  pub fn magnitude(battle: anytype, player: Player, state: *State, options: anytype) !void {
1709
- _ = .{ battle, player, state, options }; // TODO
1892
+ const num = try Rolls.magnitude(battle, player, options);
1893
+ try options.log.activate(.{ battle.active(player), .Magnitude, num });
1894
+ state.bp = MAGNITUDE_POWER[num - 4];
1710
1895
  }
1711
1896
 
1712
- pub fn meanLook(battle: anytype, player: Player, state: *State, options: anytype) !void {
1713
- _ = .{ battle, player, state, options }; // TODO
1897
+ pub fn meanLook(battle: anytype, player: Player, _: *State, options: anytype) !void {
1898
+ var foe = battle.foe(player);
1899
+
1900
+ if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
1901
+ if (!showdown) return options.log.fail(.{ battle.active(player.foe()), .None });
1902
+ try options.log.lastmiss(.{});
1903
+ return options.log.miss(.{battle.active(player)});
1904
+ } else if (foe.active.volatiles.Trapped) {
1905
+ return options.log.fail(.{ battle.active(player.foe()), .None });
1906
+ }
1907
+
1908
+ foe.active.volatiles.Trapped = true;
1714
1909
  }
1715
1910
 
1716
- pub fn metronome(battle: anytype, player: Player, state: *State, options: anytype) !void {
1717
- _ = .{ battle, player, state, options }; // TODO
1911
+ pub fn metronome(
1912
+ battle: anytype,
1913
+ player: Player,
1914
+ state: *State,
1915
+ options: anytype,
1916
+ ) anyerror!void {
1917
+ var side = battle.side(player);
1918
+
1919
+ side.last_used_move = .None;
1920
+ side.active.volatiles.dirty = false;
1921
+
1922
+ // if (wPlayerCharging) ???
1923
+ // wPlayerCharging = 1
1924
+
1925
+ // FIXME
1926
+ state.move = try Rolls.metronome(battle, player, options);
1927
+ state.from = .Metronome;
1928
+ return generated.runMove(battle, player, state, options);
1718
1929
  }
1719
1930
 
1720
1931
  pub fn mimic(battle: anytype, player: Player, state: *State, options: anytype) !void {
1721
- _ = .{ battle, player, state, options }; // TODO
1722
- }
1932
+ var side = battle.side(player);
1723
1933
 
1724
- pub fn mirrorCoat(battle: anytype, player: Player, state: *State, options: anytype) !void {
1725
- _ = .{ battle, player, state, options }; // TODO
1934
+ if (state.miss) {
1935
+ if (!showdown) return options.log.fail(.{ battle.active(player.foe()), .None });
1936
+ try options.log.lastmiss(.{});
1937
+ return options.log.miss(.{battle.active(player)});
1938
+ }
1939
+
1940
+ const move = battle.foe(player).lastMove(false);
1941
+ if (move == .None or move == .Struggle) {
1942
+ return options.log.fail(.{ battle.active(player.foe()), .None });
1943
+ }
1944
+
1945
+ var slot: u4 = 0;
1946
+ for (side.active.moves, 0..) |m, i| {
1947
+ if (m.id == move) return options.log.fail(.{ battle.active(player.foe()), .None });
1948
+ if (m.id == .Mimic) slot = @intCast(i + 1);
1949
+ }
1950
+ assert(slot != 0 and slot == state.mslot);
1951
+
1952
+ side.active.move(slot).id = move;
1953
+ side.active.move(slot).pp = 5;
1954
+ try options.log.start(.{ battle.active(player), .Mimic, move });
1726
1955
  }
1727
1956
 
1728
- pub fn mirrorMove(battle: anytype, player: Player, state: *State, options: anytype) !void {
1729
- _ = .{ battle, player, state, options }; // TODO
1957
+ pub fn mirrorMove(
1958
+ battle: anytype,
1959
+ player: Player,
1960
+ state: *State,
1961
+ options: anytype,
1962
+ ) anyerror!void {
1963
+ var side = battle.side(player);
1964
+ const foe = battle.foe(player);
1965
+
1966
+ side.last_used_move = .None;
1967
+ side.active.volatiles.dirty = false;
1968
+
1969
+ const last = foe.lastMove(false);
1970
+
1971
+ if (last == .None) return options.log.fail(.{ battle.active(player), .None });
1972
+ const knows = knows: for (side.active.moves) |m| {
1973
+ if (last == m.id) break :knows true;
1974
+ } else false;
1975
+ if (knows) return options.log.fail(.{ battle.active(player), .None });
1976
+
1977
+ // if (wPlayerCharging) ???
1978
+ // wPlayerCharging = 1
1979
+
1980
+ // FIXME
1981
+ state.move = last;
1982
+ state.from = .MirrorMove;
1983
+ return generated.runMove(battle, player, state, options);
1730
1984
  }
1731
1985
 
1732
- pub fn mist(battle: anytype, player: Player, state: *State, options: anytype) !void {
1733
- _ = .{ battle, player, state, options }; // TODO
1986
+ pub fn mist(battle: anytype, player: Player, _: *State, options: anytype) !void {
1987
+ var side = battle.side(player);
1988
+
1989
+ if (side.active.volatiles.Mist) return options.log.fail(.{ battle.active(player), .None });
1990
+ side.active.volatiles.Mist = true;
1991
+
1992
+ try options.log.start(.{ battle.active(player), .Mist });
1734
1993
  }
1735
1994
 
1736
1995
  pub fn weatherHeal(battle: anytype, player: Player, _: *State, options: anytype) !void {
1737
1996
  var stored = battle.side(player).stored();
1738
1997
  const ident = battle.active(player);
1739
1998
 
1740
- if (stored.hp == stored.stats.hp) return try options.log.fail(ident, .None);
1999
+ if (stored.hp == stored.stats.hp) return options.log.fail(.{ ident, .None });
1741
2000
 
1742
2001
  stored.hp = switch (battle.field.weather) {
1743
2002
  .Sun => stored.stats.hp,
@@ -1745,7 +2004,7 @@ pub const Effects = struct {
1745
2004
  else => @min(stored.stats.hp, stored.hp + (stored.stats.hp / 4)),
1746
2005
  };
1747
2006
 
1748
- try options.log.heal(ident, stored, Heal.None);
2007
+ try options.log.heal(.{ ident, stored, .None });
1749
2008
  }
1750
2009
 
1751
2010
  pub fn multiHit(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1753,7 +2012,22 @@ pub const Effects = struct {
1753
2012
  }
1754
2013
 
1755
2014
  pub fn nightmare(battle: anytype, player: Player, state: *State, options: anytype) !void {
1756
- _ = .{ battle, player, state, options }; // TODO
2015
+ var foe = battle.foe(player);
2016
+ var foe_volatiles = &foe.active.volatiles;
2017
+ const foe_ident = battle.active(player.foe());
2018
+
2019
+ if (foe_volatiles.Flying or foe_volatiles.Underground) {
2020
+ if (!showdown) return options.log.fail(.{ battle.active(player.foe()), .None });
2021
+ try options.log.lastmiss(.{});
2022
+ return options.log.miss(.{battle.active(player)});
2023
+ } else if (foe_volatiles.Substitute) {
2024
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
2025
+ } else if (!Status.is(foe.stored().status, .SLP) or foe_volatiles.Nightmare) {
2026
+ return options.log.fail(.{ foe_ident, .None });
2027
+ }
2028
+
2029
+ foe_volatiles.Nightmare = true;
2030
+ try options.log.start(.{ battle.active(player.foe()), .Nightmare });
1757
2031
  }
1758
2032
 
1759
2033
  pub const ohko = struct {
@@ -1777,9 +2051,9 @@ pub const Effects = struct {
1777
2051
  const foe_ident = battle.active(player.foe());
1778
2052
 
1779
2053
  state.damage = 0;
1780
- if (state.immune()) return try options.log.immune(foe_ident, .None);
2054
+ if (state.immune()) return options.log.immune(.{ foe_ident, .None });
1781
2055
  const delta = side.stored().level -| foe.stored().level;
1782
- if (delta == 0) return try options.log.immune(foe_ident, .OHKO);
2056
+ if (delta == 0) return options.log.immune(.{ foe_ident, .OHKO });
1783
2057
 
1784
2058
  state.accuracy = 2 * delta +| Gen12.percent(30);
1785
2059
  try checkHit(battle, player, state, options);
@@ -1789,11 +2063,45 @@ pub const Effects = struct {
1789
2063
  };
1790
2064
 
1791
2065
  pub fn painSplit(battle: anytype, player: Player, state: *State, options: anytype) !void {
1792
- _ = .{ battle, player, state, options }; // TODO
2066
+ var side = battle.side(player);
2067
+ var foe = battle.foe(player);
2068
+
2069
+ const ident = battle.active(player);
2070
+ const foe_ident = battle.active(player.foe());
2071
+
2072
+ if (state.miss) return options.log.fail(.{ foe_ident, .None });
2073
+ if (foe.active.volatiles.Substitute) {
2074
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
2075
+ }
2076
+
2077
+ const avg = @max((foe.stored().hp + side.stored().hp) / 2, 1);
2078
+ foe.stored().hp -= avg;
2079
+ side.stored().hp = avg;
2080
+
2081
+ try options.log.sethp(.{ foe_ident, foe.stored(), .Silent });
2082
+ try options.log.sethp(.{ ident, side.stored(), .None });
1793
2083
  }
1794
2084
 
1795
2085
  pub fn paralyze(battle: anytype, player: Player, state: *State, options: anytype) !void {
1796
- _ = .{ battle, player, state, options }; // TODO
2086
+ var foe = battle.foe(player);
2087
+ const foe_ident = battle.active(player.foe());
2088
+
2089
+ if (Status.is(foe.stored().status, .PAR)) {
2090
+ return options.log.fail(.{ foe_ident, .Paralysis });
2091
+ }
2092
+ if (state.immune()) return options.log.immune(.{ foe_ident, .None });
2093
+ if (Status.any(foe.stored().status)) return options.log.fail(.{ foe_ident, .None });
2094
+ if (state.miss) {
2095
+ try options.log.lastmiss(.{});
2096
+ return options.log.miss(.{battle.active(player)});
2097
+ } else if (foe.active.volatiles.Substitute) {
2098
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
2099
+ }
2100
+
2101
+ foe.stored().status = Status.init(.PAR);
2102
+ foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2103
+
2104
+ try options.log.status(.{ foe_ident, foe.stored().status, .None });
1797
2105
  }
1798
2106
 
1799
2107
  pub fn paralyzeChance(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1808,17 +2116,34 @@ pub const Effects = struct {
1808
2116
 
1809
2117
  foe_stored.status = Status.init(.PAR);
1810
2118
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
1811
- try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
2119
+
2120
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
1812
2121
 
1813
2122
  // TODO: check for status berry
1814
2123
  }
1815
2124
 
1816
- pub fn payDay(battle: anytype, player: Player, state: *State, options: anytype) !void {
1817
- _ = .{ battle, player, state, options }; // TODO
2125
+ pub fn payDay(battle: anytype, player: Player, _: *State, options: anytype) !void {
2126
+ if (!showdown or !battle.foe(player).active.volatiles.Substitute) {
2127
+ try options.log.fieldactivate(.{}); // FIXME .PayDay
2128
+ }
1818
2129
  }
1819
2130
 
1820
- pub fn perishSong(battle: anytype, player: Player, state: *State, options: anytype) !void {
1821
- _ = .{ battle, player, state, options }; // TODO
2131
+ pub fn perishSong(battle: anytype, player: Player, _: *State, options: anytype) !void {
2132
+ var side = battle.side(player);
2133
+ var foe = battle.foe(player);
2134
+
2135
+ if (foe.active.volatiles.PerishSong) {
2136
+ return options.log.fail(.{ battle.active(player.foe()), .None });
2137
+ } else if (!side.active.volatiles.PerishSong) {
2138
+ side.active.volatiles.PerishSong = true;
2139
+ side.active.volatiles.perish_song = 4;
2140
+ try options.log.start(.{ battle.active(player), .PerishSong3Silent });
2141
+ }
2142
+ foe.active.volatiles.PerishSong = true;
2143
+ foe.active.volatiles.perish_song = 4;
2144
+ try options.log.start(.{ battle.active(player.foe()), .PerishSong3Silent });
2145
+
2146
+ try options.log.fieldactivate(.{}); // FIXME .PerishSong
1822
2147
  }
1823
2148
 
1824
2149
  pub fn poison(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1828,24 +2153,24 @@ pub const Effects = struct {
1828
2153
 
1829
2154
  const toxic = state.move == .Toxic;
1830
2155
  if (state.immune() or foe.active.types.includes(.Poison)) {
1831
- return options.log.immune(foe_ident, .None);
2156
+ return options.log.immune(.{ foe_ident, .None });
1832
2157
  } else if (Status.any(foe_stored.status)) {
1833
2158
  // Pokémon Showdown considers Toxic to be a status even in Generation II and so
1834
2159
  // will not include a fail reason for Toxic vs. Poison or vice-versa...
1835
- return options.log.fail(foe_ident, if (Status.is(foe_stored.status, .PSN))
2160
+ return options.log.fail(.{ foe_ident, if (Status.is(foe_stored.status, .PSN))
1836
2161
  if (!showdown)
1837
- .Poison
2162
+ Fail.Poison
1838
2163
  else if (toxic == (foe_stored.status == Status.TOX))
1839
- if (toxic) .Toxic else .Poison
2164
+ if (toxic) Fail.Toxic else Fail.Poison
1840
2165
  else
1841
- .None
2166
+ Fail.None
1842
2167
  else
1843
- .None);
2168
+ Fail.None });
1844
2169
  } else if (foe.active.volatiles.Substitute) {
1845
- return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
2170
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
1846
2171
  } else if (state.miss) {
1847
- try options.log.lastmiss();
1848
- try options.log.miss(battle.active(player));
2172
+ try options.log.lastmiss(.{});
2173
+ return options.log.miss(.{battle.active(player)});
1849
2174
  }
1850
2175
 
1851
2176
  foe_stored.status = Status.init(.PSN);
@@ -1855,7 +2180,7 @@ pub const Effects = struct {
1855
2180
  foe.active.volatiles.toxic = 0;
1856
2181
  }
1857
2182
 
1858
- try options.log.status(foe_ident, foe_stored.status, .None);
2183
+ try options.log.status(.{ foe_ident, foe_stored.status, .None });
1859
2184
 
1860
2185
  // TODO: check for status berry
1861
2186
  }
@@ -1871,7 +2196,8 @@ pub const Effects = struct {
1871
2196
  if (foe.conditions.Safeguard) return;
1872
2197
 
1873
2198
  foe_stored.status = Status.init(.PSN);
1874
- try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
2199
+
2200
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
1875
2201
 
1876
2202
  // TODO: check for status berry
1877
2203
  }
@@ -1886,10 +2212,7 @@ pub const Effects = struct {
1886
2212
 
1887
2213
  const failed = !state.first or volatiles.Substitute or
1888
2214
  !try Rolls.protect(battle, player, volatiles.protect, options);
1889
- if (failed) {
1890
- try options.log.fail(battle.active(player), .None);
1891
- return;
1892
- }
2215
+ if (failed) return options.log.fail(.{ battle.active(player), .None });
1893
2216
 
1894
2217
  volatiles.protect = @max(8, volatiles.protect + 1);
1895
2218
  if (state.move == .Endure) {
@@ -1897,11 +2220,20 @@ pub const Effects = struct {
1897
2220
  } else {
1898
2221
  volatiles.Protect = true;
1899
2222
  }
1900
- try options.log.singleturn(battle.active(player), state.move);
2223
+ try options.log.singleturn(.{ battle.active(player), state.move });
1901
2224
  }
1902
2225
 
1903
- pub fn psychUp(battle: anytype, player: Player, state: *State, options: anytype) !void {
1904
- _ = .{ battle, player, state, options }; // TODO
2226
+ pub fn psychUp(battle: anytype, player: Player, _: *State, options: anytype) !void {
2227
+ var side = battle.side(player);
2228
+ const foe = battle.foe(player);
2229
+
2230
+ // Pokémon Showdown should fail when the enemy has no boosts/unboosts but doesn't
2231
+ if (!showdown and @as(u32, @bitCast(foe.active.boosts)) == 0) {
2232
+ return options.log.fail(.{ battle.active(player), .None });
2233
+ }
2234
+
2235
+ side.active.boosts = foe.active.boosts;
2236
+ try options.log.copyboost(.{ battle.active(player), battle.active(player.foe()) });
1905
2237
  }
1906
2238
 
1907
2239
  pub fn pursuit(battle: anytype, player: Player, state: *State, _: anytype) !void {
@@ -1912,15 +2244,14 @@ pub const Effects = struct {
1912
2244
  pub const rage = struct {
1913
2245
  pub fn start(battle: anytype, player: Player, _: *State, options: anytype) !void {
1914
2246
  battle.side(player).active.volatiles.Rage = true;
1915
- try options.log.singlemove(battle.active(player), Move.Rage);
2247
+ try options.log.singlemove(.{ battle.active(player), Move.Rage });
1916
2248
  }
1917
2249
 
1918
2250
  pub fn damage(battle: anytype, player: Player, state: *State, _: anytype) !void {
1919
- var side = battle.side(player);
1920
- var volatiles = &side.active.volatiles;
2251
+ const side = battle.side(player);
1921
2252
 
1922
- assert(volatiles.Rage);
1923
- state.damage *|= (volatiles.rage +| 1);
2253
+ assert(side.active.volatiles.Rage);
2254
+ state.damage *|= (side.active.volatiles.rage +| 1);
1924
2255
  }
1925
2256
  };
1926
2257
 
@@ -1928,16 +2259,37 @@ pub const Effects = struct {
1928
2259
  return weather(battle, .Rain, options);
1929
2260
  }
1930
2261
 
1931
- pub fn rapidSpin(battle: anytype, player: Player, state: *State, options: anytype) !void {
1932
- _ = .{ battle, player, state, options }; // TODO
1933
- }
2262
+ pub fn rapidSpin(battle: anytype, player: Player, _: *State, options: anytype) !void {
2263
+ var side = battle.side(player);
1934
2264
 
1935
- pub fn razorWind(battle: anytype, player: Player, state: *State, options: anytype) !void {
1936
- _ = .{ battle, player, state, options }; // TODO
2265
+ if (side.active.volatiles.LeechSeed) {
2266
+ try options.log.end(.{ battle.active(player), .LeechSeedFrom });
2267
+ side.active.volatiles.LeechSeed = false;
2268
+ }
2269
+
2270
+ if (side.conditions.Spikes) {
2271
+ try options.log.sideend(.{ player, protocol.Side.Spikes, battle.active(player) });
2272
+ side.conditions.Spikes = false;
2273
+ }
2274
+
2275
+ if (side.active.volatiles.bind.duration > 0) {
2276
+ const reason = @intFromEnum(End.Bind) + side.active.volatiles.bind.reason - 1;
2277
+ try options.log.end(.{ battle.active(player), @as(End, @enumFromInt(reason)) });
2278
+ side.active.volatiles.bind = .{};
2279
+ }
1937
2280
  }
1938
2281
 
1939
2282
  pub fn recoil(battle: anytype, player: Player, state: *State, options: anytype) !void {
1940
- _ = .{ battle, player, state, options }; // TODO
2283
+ var side = battle.side(player);
2284
+ var stored = side.stored();
2285
+ const ident = battle.active(player);
2286
+
2287
+ assert(state.damage > 0);
2288
+
2289
+ const damage: i16 = @intCast(@max(state.damage / 4, 1));
2290
+ stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
2291
+
2292
+ try options.log.damage(.{ ident, stored, .RecoilOf, battle.active(player.foe()) });
1941
2293
  }
1942
2294
 
1943
2295
  pub fn reversal(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1965,25 +2317,72 @@ pub const Effects = struct {
1965
2317
  try calcDamage(battle, player, state, options);
1966
2318
  }
1967
2319
 
1968
- pub fn rollout(battle: anytype, player: Player, state: *State, options: anytype) !void {
1969
- _ = .{ battle, player, state, options }; // TODO
2320
+ pub const rollout = struct {
2321
+ pub fn check(battle: anytype, player: Player, _: *State, _: anytype) !void {
2322
+ var side = battle.side(player);
2323
+
2324
+ if (!side.active.volatiles.Rollout) {
2325
+ side.active.volatiles.attacks = 0;
2326
+ } else {
2327
+ // TODO SkipToBattleCommand(doturn_command)
2328
+ }
2329
+ }
2330
+
2331
+ pub fn power(battle: anytype, player: Player, state: *State, _: anytype) !void {
2332
+ var side = battle.side(player);
2333
+
2334
+ if (Status.is(side.stored().status, .SLP)) return;
2335
+
2336
+ if (side.active.volatiles.attacks > 0) {
2337
+ // TODO set wSomeoneIsRampaging
2338
+ }
2339
+
2340
+ if (state.miss) {
2341
+ side.active.volatiles.Rollout = false;
2342
+ return;
2343
+ }
2344
+
2345
+ assert(side.active.volatiles.attacks < 5);
2346
+ side.active.volatiles.attacks += 1;
2347
+ side.active.volatiles.Rollout = side.active.volatiles.attacks != 5;
2348
+
2349
+ if (side.active.volatiles.DefenseCurl) state.damage *|= 2;
2350
+ }
2351
+ };
2352
+
2353
+ pub fn sandstorm(battle: anytype, player: Player, _: *State, options: anytype) !void {
2354
+ if (battle.field.weather != .Sandstorm) return weather(battle, .Sandstorm, options);
2355
+ return options.log.fail(.{ battle.active(player), .None });
1970
2356
  }
1971
2357
 
1972
- pub fn sacredFire(battle: anytype, player: Player, state: *State, options: anytype) !void {
1973
- _ = .{ battle, player, state, options }; // TODO
2358
+ pub fn safeguard(battle: anytype, player: Player, _: *State, options: anytype) !void {
2359
+ return screens(battle, player, "Safeguard", "safeguard", options);
1974
2360
  }
1975
2361
 
1976
- pub fn safeguard(battle: anytype, player: Player, state: *State, options: anytype) !void {
1977
- _ = .{ battle, player, state, options }; // TODO
2362
+ pub fn reflect(battle: anytype, player: Player, _: *State, options: anytype) !void {
2363
+ return screens(battle, player, "Reflect", "reflect", options);
1978
2364
  }
1979
2365
 
1980
- pub fn sandstorm(battle: anytype, player: Player, _: *State, options: anytype) !void {
1981
- if (battle.field.weather != .Sandstorm) return weather(battle, .Sandstorm, options);
1982
- return try options.log.fail(battle.active(player), .None);
2366
+ pub fn lightScreen(battle: anytype, player: Player, _: *State, options: anytype) !void {
2367
+ return screens(battle, player, "LightScreen", "light_screen", options);
1983
2368
  }
1984
2369
 
1985
- pub fn screens(battle: anytype, player: Player, state: *State, options: anytype) !void {
1986
- _ = .{ battle, player, state, options }; // TODO
2370
+ fn screens(
2371
+ battle: anytype,
2372
+ player: Player,
2373
+ comptime key: []const u8,
2374
+ comptime value: []const u8,
2375
+ options: anytype,
2376
+ ) !void {
2377
+ var side = battle.side(player);
2378
+
2379
+ if (@field(side.conditions, key)) {
2380
+ return options.log.fail(.{ battle.active(player), .None });
2381
+ }
2382
+
2383
+ @field(side.conditions, key) = true;
2384
+ @field(side.conditions, value) = 5;
2385
+ try options.log.sidestart(.{ player, @field(protocol.Side, key) });
1987
2386
  }
1988
2387
 
1989
2388
  pub fn sketch(battle: anytype, player: Player, _: *State, options: anytype) !void {
@@ -1994,34 +2393,26 @@ pub const Effects = struct {
1994
2393
 
1995
2394
  if (showdown) {
1996
2395
  // yes, actually "Splash". '|nothing' gets blindly mapped to '|-activate||move:Splash'
1997
- try options.log.activate(battle.active(player), .Splash);
2396
+ try options.log.activate(.{ battle.active(player), .Splash });
1998
2397
  } else {
1999
- try options.log.fail(battle.active(player), .None);
2398
+ try options.log.fail(.{ battle.active(player), .None });
2000
2399
  }
2001
2400
  }
2002
2401
 
2003
- pub fn skullBash(battle: anytype, player: Player, state: *State, options: anytype) !void {
2004
- _ = .{ battle, player, state, options }; // TODO
2005
- }
2006
-
2007
- pub fn skyAttack(battle: anytype, player: Player, state: *State, options: anytype) !void {
2008
- _ = .{ battle, player, state, options }; // TODO
2009
- }
2010
-
2011
2402
  pub fn sleep(battle: anytype, player: Player, state: *State, options: anytype) !void {
2012
2403
  var foe = battle.foe(player);
2013
2404
  var foe_stored = foe.stored();
2014
2405
  const foe_ident = battle.active(player.foe());
2015
2406
 
2016
2407
  if (Status.any(foe_stored.status)) {
2017
- return options.log.fail(
2408
+ return options.log.fail(.{
2018
2409
  foe_ident,
2019
- if (Status.is(foe_stored.status, .SLP)) .Sleep else .None,
2020
- );
2410
+ if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
2411
+ });
2021
2412
  }
2022
2413
 
2023
2414
  if (foe.active.volatiles.Substitute) {
2024
- return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
2415
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
2025
2416
  }
2026
2417
 
2027
2418
  // Sleep Clause Mod
@@ -2032,25 +2423,21 @@ pub const Effects = struct {
2032
2423
  }
2033
2424
 
2034
2425
  foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2035
- try options.log.statusFrom(foe_ident, foe_stored.status, state.move);
2426
+ try options.log.status(.{ foe_ident, foe_stored.status, .From, state.move });
2036
2427
  }
2037
2428
 
2038
2429
  pub fn sleepTalk(battle: anytype, player: Player, state: *State, options: anytype) !void {
2039
2430
  const side = battle.side(player);
2040
2431
 
2041
2432
  if (!Status.is(side.stored().status, .SLP)) {
2042
- if (!showdown) try options.log.fail(battle.active(player), .None);
2043
- return;
2433
+ return if (!showdown) try options.log.fail(.{ battle.active(player), .None });
2044
2434
  }
2045
2435
 
2046
2436
  var n: u3 = 0;
2047
2437
  for (side.active.moves, 0..) |m, i| {
2048
2438
  if (state.mslot - 1 != i and Move.get(m.id).extra.sleep_talk) n += 1;
2049
2439
  }
2050
- if (n == 0) {
2051
- try options.log.fail(battle.active(player), .None);
2052
- return;
2053
- }
2440
+ if (n == 0) return options.log.fail(.{ battle.active(player), .None });
2054
2441
 
2055
2442
  _ = try Rolls.sleepTalk(battle, player, n, state.mslot, options); // TODO
2056
2443
  }
@@ -2059,28 +2446,75 @@ pub const Effects = struct {
2059
2446
  if (Status.is(battle.side(player).stored().status, .SLP)) return true;
2060
2447
  state.damage = 0;
2061
2448
  state.miss = true; // TODO is this necessary?
2062
- if (!showdown) try options.log.fail(battle.active(player), .None);
2449
+ if (!showdown) try options.log.fail(.{ battle.active(player), .None });
2063
2450
  return false;
2064
2451
  }
2065
2452
 
2066
- pub fn spikes(battle: anytype, player: Player, state: *State, options: anytype) !void {
2067
- _ = .{ battle, player, state, options }; // TODO
2453
+ pub fn spikes(battle: anytype, player: Player, _: *State, options: anytype) !void {
2454
+ var foe = battle.foe(player);
2455
+ if (foe.conditions.Spikes) return options.log.fail(.{ battle.active(player), .None });
2456
+
2457
+ foe.conditions.Spikes = true;
2458
+ try options.log.sidestart(.{ player.foe(), protocol.Side.Spikes });
2068
2459
  }
2069
2460
 
2070
2461
  pub fn spite(battle: anytype, player: Player, state: *State, options: anytype) !void {
2071
- _ = .{ battle, player, state, options }; // TODO
2462
+ var foe = battle.foe(player);
2463
+
2464
+ const last = foe.lastMove(false);
2465
+ if (state.miss or last == .None or last == .Struggle) {
2466
+ return options.log.fail(.{ battle.active(player.foe()), .None });
2467
+ }
2468
+
2469
+ var slot: u4 = 0;
2470
+ for (foe.active.moves, 0..) |m, i| {
2471
+ if (m.id == last) slot = @intCast(i + 1);
2472
+ }
2473
+ // If the move was called via Metronome it might not exist
2474
+ if (slot == 0 or foe.active.move(slot).pp == 0) {
2475
+ return options.log.fail(.{ battle.active(player.foe()), .None });
2476
+ }
2477
+
2478
+ // Pokémon Showdown always deducts 4 instead of randomizing
2479
+ const pp = @min(
2480
+ foe.active.move(slot).pp,
2481
+ if (showdown) 4 else try Rolls.spite(battle, player, options),
2482
+ );
2483
+ foe.active.move(slot).pp -= pp;
2484
+ if (!foe.active.volatiles.Transform) {
2485
+ var move_slot = foe.stored().move(slot);
2486
+ assert(move_slot.pp > 0);
2487
+
2488
+ move_slot.pp = @as(u6, @intCast(move_slot.pp)) -% 1;
2489
+ assert(foe.active.move(slot).pp == foe.stored().move(slot).pp);
2490
+ }
2491
+
2492
+ try options.log.activate(.{ battle.active(player.foe()), .Spite, last, pp });
2072
2493
  }
2073
2494
 
2074
2495
  pub fn splash(battle: anytype, player: Player, _: *State, options: anytype) !void {
2075
- try options.log.activate(battle.active(player), .Splash);
2496
+ try options.log.activate(.{ battle.active(player), .Splash });
2076
2497
  }
2077
2498
 
2078
- pub fn stomp(battle: anytype, player: Player, state: *State, options: anytype) !void {
2079
- _ = .{ battle, player, state, options }; // TODO
2080
- }
2499
+ pub fn substitute(battle: anytype, player: Player, _: *State, options: anytype) !void {
2500
+ var side = battle.side(player);
2501
+ if (side.active.volatiles.Substitute) {
2502
+ return options.log.fail(.{ battle.active(player), .Substitute });
2503
+ }
2081
2504
 
2082
- pub fn substitute(battle: anytype, player: Player, state: *State, options: anytype) !void {
2083
- _ = .{ battle, player, state, options }; // TODO
2505
+ assert(side.stored().stats.hp <= 1023);
2506
+ const hp: u8 = @intCast(side.stored().stats.hp / 4);
2507
+
2508
+ if (hp == 0 or side.stored().hp <= hp) {
2509
+ return options.log.fail(.{ battle.active(player), .Weak });
2510
+ }
2511
+
2512
+ side.stored().hp -= hp;
2513
+ side.active.volatiles.substitute = hp;
2514
+ side.active.volatiles.Substitute = true;
2515
+ side.active.volatiles.bind = .{};
2516
+ try options.log.start(.{ battle.active(player), .Substitute });
2517
+ try options.log.damage(.{ battle.active(player), side.stored(), .None });
2084
2518
  }
2085
2519
 
2086
2520
  pub fn sunnyDay(battle: anytype, _: Player, _: *State, options: anytype) !void {
@@ -2090,22 +2524,14 @@ pub const Effects = struct {
2090
2524
  fn weather(battle: anytype, w: Weather, options: anytype) !void {
2091
2525
  battle.field.weather = w;
2092
2526
  battle.field.weather_duration = 5;
2093
- try options.log.weather(w, .None);
2094
- }
2095
-
2096
- pub fn swagger(battle: anytype, player: Player, state: *State, options: anytype) !void {
2097
- _ = .{ battle, player, state, options }; // TODO
2098
- }
2099
-
2100
- pub fn synthesis(battle: anytype, player: Player, state: *State, options: anytype) !void {
2101
- _ = .{ battle, player, state, options }; // TODO
2527
+ try options.log.weather(.{ w, protocol.Weather.None });
2102
2528
  }
2103
2529
 
2104
2530
  pub fn teleport(battle: anytype, player: Player, _: *State, options: anytype) !void {
2105
2531
  if (showdown) return;
2106
2532
 
2107
- try options.log.fail(battle.active(player), .None);
2108
- try options.log.laststill();
2533
+ try options.log.fail(.{ battle.active(player), .None });
2534
+ try options.log.laststill(.{});
2109
2535
  }
2110
2536
 
2111
2537
  pub fn thief(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -2120,15 +2546,21 @@ pub const Effects = struct {
2120
2546
  side.stored().item = foe.stored().item;
2121
2547
  foe.stored().item = .None;
2122
2548
 
2123
- try options.log.item(
2549
+ try options.log.item(.{
2124
2550
  battle.active(player),
2125
2551
  side.stored().item,
2126
2552
  battle.active(player.foe()),
2127
- );
2553
+ });
2128
2554
  }
2129
2555
 
2130
- pub fn thrashing(battle: anytype, player: Player, state: *State, options: anytype) !void {
2131
- _ = .{ battle, player, state, options }; // TODO
2556
+ pub fn thrashing(battle: anytype, player: Player, _: *State, options: anytype) !void {
2557
+ var side = battle.side(player);
2558
+ if (Status.is(side.stored().status, .SLP)) return;
2559
+
2560
+ assert(!side.active.volatiles.Thrashing);
2561
+ side.active.volatiles.Thrashing = true;
2562
+ side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
2563
+ // TODO wSomeoneIsRampaging = true;
2132
2564
  }
2133
2565
 
2134
2566
  pub fn transform(battle: anytype, player: Player, _: *State, options: anytype) !void {
@@ -2140,11 +2572,11 @@ pub const Effects = struct {
2140
2572
  side.active.volatiles.dirty = false;
2141
2573
 
2142
2574
  if (foe.active.volatiles.Transform) {
2143
- return try options.log.fail(battle.active(player), .None);
2575
+ return options.log.fail(.{ battle.active(player), .None });
2144
2576
  } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2145
- if (!showdown) return try options.log.fail(battle.active(player), .None);
2146
- try options.log.lastmiss();
2147
- return try options.log.miss(battle.active(player));
2577
+ if (!showdown) return options.log.fail(.{ battle.active(player), .None });
2578
+ try options.log.lastmiss(.{});
2579
+ return options.log.miss(.{battle.active(player)});
2148
2580
  }
2149
2581
 
2150
2582
  side.active.volatiles.Transform = true;
@@ -2166,7 +2598,7 @@ pub const Effects = struct {
2166
2598
  side.active.moves[i].pp = if (m.id != .None) if (m.id == .Sketch) 1 else 5 else 0;
2167
2599
  }
2168
2600
 
2169
- try options.log.transform(battle.active(player), foe_ident);
2601
+ try options.log.transform(.{ battle.active(player), foe_ident });
2170
2602
  }
2171
2603
 
2172
2604
  pub fn triAttack(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -2185,35 +2617,25 @@ pub const Effects = struct {
2185
2617
  _ = .{ battle, player, state, options }; // TODO
2186
2618
  }
2187
2619
 
2188
- pub fn twister(battle: anytype, player: Player, state: *State, options: anytype) !void {
2189
- _ = .{ battle, player, state, options }; // TODO
2190
- }
2191
-
2192
- const ALL_STAT_UP_STATS = [_][]const u8{ "atk", "def", "spe", "spa", "spd" };
2193
- const ALL_STAT_UP_REASONS =
2620
+ const STAT_UP_REASONS =
2194
2621
  [_]Boost{ .Attack, .Defense, .Speed, .SpecialAttack, .SpecialDefense };
2195
2622
 
2196
2623
  pub fn allStatUpChance(battle: anytype, player: Player, state: *State, options: anytype) !void {
2197
2624
  var side = battle.side(player);
2198
2625
 
2199
- var stats = &side.active.stats;
2200
- var boosts = &side.active.boosts;
2626
+ const stats = &side.active.stats;
2627
+ const boosts = &side.active.boosts;
2201
2628
 
2202
2629
  if (!state.proc) return;
2203
- inline for (ALL_STAT_UP_STATS, ALL_STAT_UP_REASONS) |s, r| {
2630
+ inline for (STATS, STAT_UP_REASONS) |s, r| {
2204
2631
  assert(@field(boosts, s) >= -6 and @field(boosts, s) <= 6);
2205
2632
  if (@field(boosts, s) == 6 or @field(stats, s) == MAX_STAT_VALUE) {
2206
- try options.log.boost(battle.active(player), r, 0);
2633
+ try options.log.boost(.{ battle.active(player), r, 0 });
2207
2634
  } else {
2208
- @field(boosts, s) = @intCast(@min(6, @as(i8, @field(boosts, s)) + 1));
2209
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, @field(boosts, s)) + 6))];
2210
- const stat = @field(unmodifiedStats(battle, side), s);
2211
- @field(stats, s) = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2212
- try options.log.boost(battle.active(player), r, 1);
2635
+ try boostStat(battle, side, stats, boosts, s, 1);
2636
+ try options.log.boost(.{ battle.active(player), r, 1 });
2213
2637
  }
2214
2638
  }
2215
-
2216
- statusModify(side.stored().status, stats);
2217
2639
  }
2218
2640
 
2219
2641
  pub fn boost(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -2221,84 +2643,65 @@ pub const Effects = struct {
2221
2643
  var side = battle.side(player);
2222
2644
  const ident = battle.active(player);
2223
2645
 
2224
- if (state.miss) return try log.fail(ident, .None);
2646
+ if (state.miss) return log.fail(.{ ident, .None });
2225
2647
 
2226
- var stats = &side.active.stats;
2648
+ const stats = &side.active.stats;
2227
2649
  var boosts = &side.active.boosts;
2228
2650
 
2229
2651
  const move = Move.get(state.move);
2230
2652
  switch (move.effect) {
2231
- .AttackUp1, .AttackUp2, .Rage => {
2653
+ .AttackUp1, .AttackUp2, .Rage, .Swagger => {
2232
2654
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2233
2655
  if (boosts.atk == 6 or stats.atk == MAX_STAT_VALUE) {
2234
- return try log.fail(ident, .None);
2656
+ return log.fail(.{ ident, .None });
2235
2657
  }
2236
- const n: u2 = if (move.effect == .AttackUp2) 2 else 1;
2237
- boosts.atk = @as(i4, @intCast(@min(6, @as(i8, boosts.atk) + n)));
2658
+ const n: u2 = if (move.effect == .AttackUp2 or move.effect == .Swagger) 2 else 1;
2659
+ try boostStat(battle, side, stats, boosts, "atk", n);
2238
2660
  const reason = if (showdown and move.effect == .Rage) Boost.Rage else Boost.Attack;
2239
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2240
- const stat = unmodifiedStats(battle, side).atk;
2241
- stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2242
- try log.boost(ident, reason, n);
2661
+ try log.boost(.{ ident, reason, n });
2243
2662
  },
2244
- .DefenseUp1, .DefenseUp2 => {
2663
+ .DefenseUp1, .DefenseUp2, .SkullBash => {
2245
2664
  assert(boosts.def >= -6 and boosts.def <= 6);
2246
2665
  if (boosts.def == 6 or stats.def == MAX_STAT_VALUE) {
2247
- return try log.fail(ident, .None);
2666
+ return log.fail(.{ ident, .None });
2248
2667
  }
2249
2668
  const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
2250
- boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
2251
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2252
- const stat = unmodifiedStats(battle, side).def;
2253
- stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2254
- try log.boost(ident, .Defense, n);
2669
+ try boostStat(battle, side, stats, boosts, "def", n);
2670
+ try log.boost(.{ ident, .Defense, n });
2255
2671
  },
2256
2672
  .SpeedUp2 => {
2257
2673
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2258
2674
  if (boosts.spe == 6 or stats.spe == MAX_STAT_VALUE) {
2259
- return try log.fail(ident, .None);
2675
+ return log.fail(.{ ident, .None });
2260
2676
  }
2261
- boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
2262
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2263
- const stat = unmodifiedStats(battle, side).spe;
2264
- stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2265
- try log.boost(ident, .Speed, 2);
2677
+ try boostStat(battle, side, stats, boosts, "spe", 2);
2678
+ try log.boost(.{ ident, .Speed, 2 });
2266
2679
  },
2267
2680
  .SpAtkUp1 => {
2268
2681
  assert(boosts.spa >= -6 and boosts.spa <= 6);
2269
2682
  if (boosts.spa == 6 or stats.spa == MAX_STAT_VALUE) {
2270
- return try log.fail(ident, .None);
2683
+ return log.fail(.{ ident, .None });
2271
2684
  }
2272
- boosts.spa = @intCast(@min(6, @as(i8, boosts.spa) + 1));
2273
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.spa) + 6))];
2274
- const stat = unmodifiedStats(battle, side).spa;
2275
- stats.spa = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2276
- try log.boost(ident, .SpecialAttack, 1);
2685
+ try boostStat(battle, side, stats, boosts, "spa", 1);
2686
+ try log.boost(.{ ident, .SpecialAttack, 1 });
2277
2687
  },
2278
2688
  .SpDefUp2 => {
2279
2689
  assert(boosts.spd >= -6 and boosts.spd <= 6);
2280
2690
  if (boosts.spd == 6 or stats.spd == MAX_STAT_VALUE) {
2281
- return try log.fail(ident, .None);
2691
+ return log.fail(.{ ident, .None });
2282
2692
  }
2283
- boosts.spd = @intCast(@min(6, @as(i8, boosts.spd) + 2));
2284
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.spd) + 6))];
2285
- const stat = unmodifiedStats(battle, side).spd;
2286
- stats.spd = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2287
- try log.boost(ident, .SpecialDefense, 2);
2693
+ try boostStat(battle, side, stats, boosts, "spd", 2);
2694
+ try log.boost(.{ ident, .SpecialDefense, 2 });
2288
2695
  },
2289
2696
  .EvasionUp1 => {
2290
2697
  assert(boosts.evasion >= -6 and boosts.evasion <= 6);
2291
- if (boosts.evasion == 6) {
2292
- return try log.fail(ident, .None);
2293
- }
2698
+ if (boosts.evasion == 6) return log.fail(.{ ident, .None });
2294
2699
  boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
2295
- try log.boost(ident, .Evasion, 1);
2700
+ try log.boost(.{ ident, .Evasion, 1 });
2296
2701
  if (state.move == .Minimize) side.active.volatiles.minimized = true;
2297
2702
  },
2298
2703
  else => unreachable,
2299
2704
  }
2300
-
2301
- statusModify(side.stored().status, stats);
2302
2705
  }
2303
2706
 
2304
2707
  pub fn unboost(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -2307,102 +2710,86 @@ pub const Effects = struct {
2307
2710
  const foe_ident = battle.active(player.foe());
2308
2711
 
2309
2712
  if (foe.active.volatiles.Mist) {
2310
- try log.activate(foe_ident, .Mist);
2311
- return try log.fail(foe_ident, .None);
2713
+ try log.activate(.{ foe_ident, .Mist });
2714
+ return log.fail(.{ foe_ident, .None });
2312
2715
  }
2313
2716
 
2314
2717
  const fail = foe.active.volatiles.Substitute or state.miss or !state.proc;
2315
2718
 
2316
- var stats = &foe.active.stats;
2719
+ const stats = &foe.active.stats;
2317
2720
  var boosts = &foe.active.boosts;
2318
2721
 
2319
2722
  const move = Move.get(state.move);
2723
+ const none: Fail = .None;
2320
2724
  switch (move.effect) {
2321
2725
  .AttackDown1, .AttackDown2, .AttackDownChance => {
2322
2726
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2323
2727
  if (fail or boosts.atk == -6) {
2324
- return if (move.effect != .AttackDownChance) try log.fail(foe_ident, .None);
2728
+ return if (move.effect != .AttackDownChance) try log.fail(.{ foe_ident, none });
2325
2729
  } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2326
- try options.log.lastmiss();
2327
- try options.log.miss(battle.active(player));
2328
- return;
2730
+ try options.log.lastmiss(.{});
2731
+ return options.log.miss(.{battle.active(player)});
2329
2732
  } else if (stats.atk == 1) {
2330
- return if (move.effect != .AttackDownChance) try log.fail(foe_ident, .None);
2733
+ return if (move.effect != .AttackDownChance) try log.fail(.{ foe_ident, none });
2331
2734
  }
2332
2735
  const n: u2 = if (move.effect == .AttackDown2) 2 else 1;
2333
- boosts.atk = @intCast(@max(-6, @as(i8, boosts.atk) - n));
2334
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2335
- const stat = unmodifiedStats(battle, foe).atk;
2336
- stats.atk = @max(1, stat * mod[0] / mod[1]);
2337
- try log.boost(foe_ident, .Attack, -@as(i8, n));
2736
+ try unboostStat(battle, foe, stats, boosts, "atk", n);
2737
+ try log.boost(.{ foe_ident, .Attack, -@as(i8, n) });
2338
2738
  },
2339
2739
  .DefenseDown1, .DefenseDown2, .DefenseDownChance => {
2340
2740
  assert(boosts.def >= -6 and boosts.def <= 6);
2341
2741
  if (fail or boosts.def == -6) {
2342
- return if (move.effect != .DefenseDownChance) try log.fail(foe_ident, .None);
2343
- } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2344
- try options.log.lastmiss();
2345
- try options.log.miss(battle.active(player));
2742
+ if (move.effect != .DefenseDownChance) try log.fail(.{ foe_ident, none });
2346
2743
  return;
2744
+ } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2745
+ try options.log.lastmiss(.{});
2746
+ return options.log.miss(.{battle.active(player)});
2347
2747
  } else if (stats.def == 1) {
2348
- return if (move.effect != .DefenseDownChance) try log.fail(foe_ident, .None);
2748
+ if (move.effect != .DefenseDownChance) try log.fail(.{ foe_ident, none });
2749
+ return;
2349
2750
  }
2350
2751
  const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
2351
- boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
2352
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2353
- const stat = unmodifiedStats(battle, foe).def;
2354
- stats.def = @max(1, stat * mod[0] / mod[1]);
2355
- try log.boost(foe_ident, .Defense, -@as(i8, n));
2752
+ try unboostStat(battle, foe, stats, boosts, "def", n);
2753
+ try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
2356
2754
  },
2357
2755
  .SpeedDown1, .SpeedDown2, .SpeedDownChance => {
2358
2756
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2359
2757
  if (fail or boosts.spe == -6) {
2360
- return if (move.effect != .SpeedDownChance) try log.fail(foe_ident, .None);
2758
+ return if (move.effect != .SpeedDownChance) try log.fail(.{ foe_ident, none });
2361
2759
  } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2362
- try options.log.lastmiss();
2363
- try options.log.miss(battle.active(player));
2364
- return;
2760
+ try options.log.lastmiss(.{});
2761
+ return options.log.miss(.{battle.active(player)});
2365
2762
  } else if (stats.spe == 1) {
2366
- return if (move.effect != .SpeedDownChance) try log.fail(foe_ident, .None);
2763
+ return if (move.effect != .SpeedDownChance) try log.fail(.{ foe_ident, none });
2367
2764
  }
2368
2765
  const n: u2 = if (move.effect == .SpeedDown2) 2 else 1;
2369
- boosts.spe = @intCast(@max(-6, @as(i8, boosts.spe) - 1));
2370
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2371
- const stat = unmodifiedStats(battle, foe).spe;
2372
- stats.spe = @max(1, stat * mod[0] / mod[1]);
2373
- try log.boost(foe_ident, .Speed, -@as(i8, n));
2374
- assert(boosts.spe >= -6);
2766
+ try unboostStat(battle, foe, stats, boosts, "spe", n);
2767
+ try log.boost(.{ foe_ident, .Speed, -@as(i8, n) });
2375
2768
  },
2376
2769
  .SpDefDownChance => {
2377
2770
  assert(boosts.spd >= -6 and boosts.spd <= 6);
2378
2771
  if (fail or boosts.spd == -6) return;
2379
2772
  if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2380
- try options.log.lastmiss();
2381
- try options.log.miss(battle.active(player));
2382
- return;
2773
+ try options.log.lastmiss(.{});
2774
+ return options.log.miss(.{battle.active(player)});
2383
2775
  } else if (stats.spd == 1) return;
2384
- boosts.spd = @intCast(@max(-6, @as(i8, boosts.spd) - 1));
2385
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.spd) + 6))];
2386
- const stat = unmodifiedStats(battle, foe).spd;
2387
- stats.spd = @max(1, stat * mod[0] / mod[1]);
2388
- try log.boost(foe_ident, .SpecialDefense, -1);
2776
+ try unboostStat(battle, foe, stats, boosts, "spd", 1);
2777
+ try log.boost(.{ foe_ident, .SpecialDefense, -1 });
2389
2778
  },
2390
2779
  .AccuracyDown1, .AccuracyDownChance => {
2391
2780
  assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
2392
2781
  if (fail or boosts.accuracy == -6) {
2393
- return if (move.effect != .AccuracyDownChance) try log.fail(foe_ident, .None);
2394
- } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2395
- try options.log.lastmiss();
2396
- try options.log.miss(battle.active(player));
2782
+ if (move.effect != .AccuracyDownChance) try log.fail(.{ foe_ident, none });
2397
2783
  return;
2784
+ } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2785
+ try options.log.lastmiss(.{});
2786
+ return options.log.miss(.{battle.active(player)});
2398
2787
  }
2399
2788
  boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
2400
- try log.boost(foe_ident, .Accuracy, -1);
2789
+ try log.boost(.{ foe_ident, .Accuracy, -1 });
2401
2790
  },
2402
2791
  else => unreachable,
2403
2792
  }
2404
-
2405
- statusModify(foe.stored().status, stats);
2406
2793
  }
2407
2794
  };
2408
2795
 
@@ -2412,6 +2799,15 @@ fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
2412
2799
  return &battle.side(id.player).pokemon[id.id - 1].stats;
2413
2800
  }
2414
2801
 
2802
+ fn recomputeStats(battle: anytype, side: *Side) void {
2803
+ const stats = unmodifiedStats(battle, side);
2804
+ inline for (STATS) |s| {
2805
+ const mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, @field(side.active.boosts, s)) + 6))];
2806
+ const val = @field(stats, s) * mod[0] / mod[1];
2807
+ @field(side.active.stats, s) = @max(1, @min(MAX_STAT_VALUE, val));
2808
+ }
2809
+ }
2810
+
2415
2811
  fn statusModify(status: u8, stats: *Stats(u16)) void {
2416
2812
  if (Status.is(status, .PAR)) {
2417
2813
  stats.spe = @max(stats.spe / 4, 1);
@@ -2420,6 +2816,52 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
2420
2816
  }
2421
2817
  }
2422
2818
 
2819
+ fn boostStat(
2820
+ battle: anytype,
2821
+ side: *Side,
2822
+ stats: *Stats(u16),
2823
+ boosts: *Boosts,
2824
+ comptime s: []const u8,
2825
+ n: u2,
2826
+ ) !void {
2827
+ @field(boosts, s) = @intCast(@min(6, @as(i8, @field(boosts, s)) + n));
2828
+ const mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, @field(boosts, s)) + 6))];
2829
+ const stat = @field(unmodifiedStats(battle, side), s);
2830
+ @field(stats, s) = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2831
+ if (comptime std.mem.eql(u8, s, "atk")) {
2832
+ if (Status.is(side.stored().status, .BRN)) {
2833
+ @field(stats, s) = @max(@field(stats, s) / 2, 1);
2834
+ }
2835
+ } else if (comptime std.mem.eql(u8, s, "spe")) {
2836
+ if (Status.is(side.stored().status, .PAR)) {
2837
+ @field(stats, s) = @max(@field(stats, s) / 4, 1);
2838
+ }
2839
+ }
2840
+ }
2841
+
2842
+ fn unboostStat(
2843
+ battle: anytype,
2844
+ side: *Side,
2845
+ stats: *Stats(u16),
2846
+ boosts: *Boosts,
2847
+ comptime s: []const u8,
2848
+ n: u2,
2849
+ ) !void {
2850
+ @field(boosts, s) = @intCast(@max(-6, @as(i8, @field(boosts, s)) - n));
2851
+ const mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, @field(boosts, s)) + 6))];
2852
+ const stat = @field(unmodifiedStats(battle, side), s);
2853
+ @field(stats, s) = @max(1, stat * mod[0] / mod[1]);
2854
+ if (comptime std.mem.eql(u8, s, "atk")) {
2855
+ if (Status.is(side.stored().status, .BRN)) {
2856
+ @field(stats, s) = @max(@field(stats, s) / 2, 1);
2857
+ }
2858
+ } else if (comptime std.mem.eql(u8, s, "spe")) {
2859
+ if (Status.is(side.stored().status, .PAR)) {
2860
+ @field(stats, s) = @max(@field(stats, s) / 4, 1);
2861
+ }
2862
+ }
2863
+ }
2864
+
2423
2865
  fn isForced(active: anytype) bool {
2424
2866
  return active.volatiles.Recharging or active.volatiles.Rage or
2425
2867
  active.volatiles.Thrashing or active.volatiles.Charging or
@@ -2609,7 +3051,7 @@ pub const Rolls = struct {
2609
3051
  }
2610
3052
 
2611
3053
  fn magnitude(battle: anytype, player: Player, options: anytype) !u8 {
2612
- const num = if (options.calc.overridden(player, .magnitude)) |val|
3054
+ const num: u8 = if (options.calc.overridden(player, .magnitude)) |val|
2613
3055
  val + 3
2614
3056
  else if (showdown) num: {
2615
3057
  const r = battle.rng.range(u8, 0, 100);
@@ -2653,13 +3095,12 @@ pub const Rolls = struct {
2653
3095
  return hits;
2654
3096
  }
2655
3097
 
2656
- fn spite(battle: anytype, player: Player, options: anytype) !u8 {
2657
- const pp = if (options.calc.overridden(player, .spite)) |val|
3098
+ fn spite(battle: anytype, player: Player, options: anytype) !u3 {
3099
+ assert(!showdown);
3100
+ const pp: u3 = if (options.calc.overridden(player, .spite)) |val|
2658
3101
  val + 1
2659
- else if (showdown)
2660
- battle.rng.range(u8, 2, 6)
2661
3102
  else
2662
- (battle.rng.next() & 3) + 2;
3103
+ @intCast((battle.rng.next() & 3) + 2);
2663
3104
 
2664
3105
  assert(pp >= 2 and pp <= 5);
2665
3106
  try options.chance.spite(player, pp);
@@ -2868,12 +3309,12 @@ pub const Rolls = struct {
2868
3309
  }
2869
3310
 
2870
3311
  fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2871
- const moves = battle.get(player).active.moves;
3312
+ const moves = battle.side(player).active.moves;
2872
3313
 
2873
3314
  var n: u2 = 0;
2874
3315
  for (moves) |m| {
2875
- if (m == .None) break;
2876
- if (Move.get(m).metronome) n += 1;
3316
+ if (m.id == .None) break;
3317
+ if (Move.get(m.id).extra.metronome) n += 1;
2877
3318
  }
2878
3319
 
2879
3320
  // TODO: consider throwing error instead of rerolling?
@@ -2884,10 +3325,11 @@ pub const Rolls = struct {
2884
3325
 
2885
3326
  const move: Move = overridden orelse move: {
2886
3327
  if (showdown) {
2887
- var r = battle.rng.range(u8, 0, Move.METRONOME.len - n);
3328
+ var r = battle.rng.range(u8, 0, @as(u8, @intCast(Move.METRONOME.len - n)));
2888
3329
  for (moves) |m| {
2889
- if (m == .None or @intFromEnum(m) > @intFromEnum(Move.METRONOME[r])) break;
2890
- if (Move.get(m).metronome) r -= 1;
3330
+ if (m.id == .None) break;
3331
+ if (@intFromEnum(m.id) > @intFromEnum(Move.METRONOME[r])) break;
3332
+ if (Move.get(m.id).extra.metronome) r -= 1;
2891
3333
  }
2892
3334
  break :move Move.METRONOME[r];
2893
3335
  } else {
@@ -2902,7 +3344,7 @@ pub const Rolls = struct {
2902
3344
  }
2903
3345
  };
2904
3346
 
2905
- assert(Move.get(move).metronome);
3347
+ assert(Move.get(move).extra.metronome);
2906
3348
  try options.chance.metronome(player, move, n);
2907
3349
  return move;
2908
3350
  }
@@ -3106,7 +3548,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3106
3548
  .Move => {
3107
3549
  const side = battle.side(player);
3108
3550
 
3109
- var active = &side.active;
3551
+ const active = &side.active;
3110
3552
  const stored = side.stored();
3111
3553
 
3112
3554
  // While players are not given any input options on the cartridge in these cases,
@@ -3126,7 +3568,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3126
3568
  }
3127
3569
 
3128
3570
  var slot: u4 = 2;
3129
- while (!active.volatiles.trapped and slot <= 6) : (slot += 1) {
3571
+ while (!active.volatiles.Trapped and slot <= 6) : (slot += 1) {
3130
3572
  const id = side.order[slot - 1];
3131
3573
  if (id == 0 or side.pokemon[id - 1].hp == 0) continue;
3132
3574
  out[n] = .{ .type = .Switch, .data = slot };