@pkmn/engine 0.1.0-dev.d56bdadd → 0.1.0-dev.d952b7c9

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 (67) hide show
  1. package/LICENSE +1 -3
  2. package/README.md +46 -19
  3. package/build/pkg/addon/browser.d.ts +3 -0
  4. package/build/pkg/addon/browser.js +43 -0
  5. package/build/pkg/addon/browser.js.map +1 -0
  6. package/build/pkg/addon/node.d.ts +3 -0
  7. package/build/pkg/addon/node.js +96 -0
  8. package/build/pkg/addon/node.js.map +1 -0
  9. package/build/pkg/addon.d.ts +2 -0
  10. package/build/pkg/addon.js +35 -58
  11. package/build/pkg/addon.js.map +1 -1
  12. package/build/pkg/data.d.ts +27 -5
  13. package/build/pkg/data.js.map +1 -1
  14. package/build/pkg/gen1.js +1 -1
  15. package/build/pkg/gen1.js.map +1 -1
  16. package/build/pkg/index.d.ts +24 -10
  17. package/build/pkg/index.js +15 -10
  18. package/build/pkg/index.js.map +1 -1
  19. package/build/pkg/protocol.d.ts +20 -1
  20. package/build/pkg/protocol.js +109 -107
  21. package/build/pkg/protocol.js.map +1 -1
  22. package/build/tools/debug.js +36 -68
  23. package/build/tools/debug.js.map +1 -1
  24. package/build/tools/display/debug.jsx +128 -0
  25. package/build/tools/display/debug.jsx.map +1 -0
  26. package/build/tools/display/index.js +132 -0
  27. package/build/tools/display/index.js.map +1 -0
  28. package/build/tools/display/ui.jsx +349 -0
  29. package/build/tools/display/ui.jsx.map +1 -0
  30. package/build/tools/display/util.js +18 -0
  31. package/build/tools/display/util.js.map +1 -0
  32. package/build.zig +206 -81
  33. package/package.json +51 -30
  34. package/src/bin/install-pkmn-engine +29 -19
  35. package/src/bin/pkmn-debug +1 -1
  36. package/src/lib/common/array.zig +77 -0
  37. package/src/lib/common/data.zig +4 -4
  38. package/src/lib/common/debug.zig +11 -6
  39. package/src/lib/common/js.zig +4 -2
  40. package/src/lib/common/optional.zig +14 -10
  41. package/src/lib/common/options.zig +4 -0
  42. package/src/lib/common/protocol.zig +389 -407
  43. package/src/lib/common/rational.zig +121 -27
  44. package/src/lib/common/rng.zig +7 -7
  45. package/src/lib/common/util.zig +50 -1
  46. package/src/lib/gen1/calc.zig +657 -133
  47. package/src/lib/gen1/chance.zig +376 -267
  48. package/src/lib/gen1/data/species.zig +1 -1
  49. package/src/lib/gen1/data.zig +17 -16
  50. package/src/lib/gen1/helpers.zig +95 -538
  51. package/src/lib/gen1/mechanics.zig +492 -389
  52. package/src/lib/gen2/calc.zig +34 -16
  53. package/src/lib/gen2/chance.zig +84 -496
  54. package/src/lib/gen2/data.zig +8 -7
  55. package/src/lib/gen2/generated.zig +4 -6
  56. package/src/lib/gen2/helpers.zig +41 -13
  57. package/src/lib/gen2/mechanics.zig +448 -377
  58. package/src/lib/{binding/node.zig → node.zig} +9 -12
  59. package/src/lib/pkmn.zig +32 -15
  60. package/src/lib/{binding/wasm.zig → wasm.zig} +2 -1
  61. package/src/tools/display/dom.ts +96 -0
  62. package/src/tools/display/ui.css +351 -0
  63. package/build/test/display.js +0 -400
  64. package/build/test/display.js.map +0 -1
  65. package/src/lib/gen1/README.md +0 -894
  66. package/src/lib/gen2/README.md +0 -329
  67. package/src/test/display/pkmn.html.tmpl +0 -368
@@ -6,7 +6,6 @@ const common = @import("../common/data.zig");
6
6
  const DEBUG = @import("../common/debug.zig").print;
7
7
  const protocol = @import("../common/protocol.zig");
8
8
  const rng = @import("../common/rng.zig");
9
- const util = @import("../common/util.zig");
10
9
 
11
10
  const data = @import("data.zig");
12
11
  const generated = @import("generated.zig");
@@ -26,25 +25,20 @@ const Result = common.Result;
26
25
 
27
26
  const Activate = protocol.Activate;
28
27
  const Boost = protocol.Boost;
29
- const Cant = protocol.Cant;
30
28
  const Damage = protocol.Damage;
31
29
  const End = protocol.End;
30
+ const Fail = protocol.Fail;
32
31
  const Heal = protocol.Heal;
33
32
  const Start = protocol.Start;
34
33
 
35
34
  const Gen12 = rng.Gen12;
36
35
 
37
- const PointerType = util.PointerType;
38
-
39
36
  const ActivePokemon = data.ActivePokemon;
40
37
  const Boosts = data.Boosts;
41
38
  const Effectiveness = data.Effectiveness;
42
39
  const Item = data.Item;
43
40
  const Move = data.Move;
44
- const MoveSlot = data.MoveSlot;
45
- const Pokemon = data.Pokemon;
46
41
  const Side = data.Side;
47
- const Species = data.Species;
48
42
  const Stats = data.Stats;
49
43
  const Status = data.Status;
50
44
  const TriAttack = data.TriAttack;
@@ -106,6 +100,7 @@ pub const State = struct {
106
100
  type: Type, // BATTLE_VARS_MOVE_TYPE
107
101
  accuracy: u8, // wEnemyMoveStruct + MOVE_ACC
108
102
  move: Move, // wCurPlayerMove
103
+ hit: u3 = 0,
109
104
  from: Move = .None,
110
105
  mslot: u4, // wCurMoveNum
111
106
  first: bool, // wEnemyGoesFirst
@@ -140,11 +135,11 @@ fn start(battle: anytype, options: anytype) !Result {
140
135
  const p1 = battle.side(.P1);
141
136
  const p2 = battle.side(.P2);
142
137
 
143
- var p1_slot = findFirstAlive(p1);
138
+ const p1_slot = findFirstAlive(p1);
144
139
  assert(!showdown or p1_slot == 1);
145
140
  if (p1_slot == 0) return if (findFirstAlive(p2) == 0) Result.Tie else Result.Lose;
146
141
 
147
- var p2_slot = findFirstAlive(p2);
142
+ const p2_slot = findFirstAlive(p2);
148
143
  assert(!showdown or p2_slot == 1);
149
144
  if (p2_slot == 0) return Result.Win;
150
145
 
@@ -252,15 +247,14 @@ fn switchIn(battle: anytype, player: Player, slot: u8, kind: SwitchIn, options:
252
247
  statusModify(incoming.status, &active.stats);
253
248
 
254
249
  if (kind == .Drag) {
255
- try options.log.drag(battle.active(player), incoming);
250
+ try options.log.drag(.{ battle.active(player), incoming });
256
251
  } else {
257
- try options.log.switched(battle.active(player), incoming);
252
+ try options.log.switched(.{ battle.active(player), incoming });
258
253
  }
259
- options.chance.switched(player, side.pokemon[0].position, out);
260
254
 
261
255
  if (side.conditions.Spikes and !active.types.includes(.Flying)) {
262
256
  incoming.hp -|= @max(incoming.stats.hp / 8, 1);
263
- try options.log.damage(battle.active(player), incoming, .Spikes);
257
+ try options.log.damage(.{ battle.active(player), incoming, .Spikes });
264
258
  }
265
259
  }
266
260
 
@@ -403,7 +397,9 @@ fn executeMove(
403
397
 
404
398
  assert(choice.type == .Move);
405
399
  var mslot: u4 = @intCast(choice.data);
400
+ _ = &mslot;
406
401
  var move = Move.Pound; // TODO
402
+ _ = &move;
407
403
 
408
404
  if (!try beforeMove(battle, player, move, options)) return null;
409
405
 
@@ -433,31 +429,22 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
433
429
  if (volatiles.Recharging) {
434
430
  volatiles.Recharging = false;
435
431
  resetCant(volatiles);
436
- try log.cant(ident, .Recharge);
432
+ try log.cant(.{ ident, .Recharge });
437
433
  return false;
438
434
  }
439
435
 
440
436
  if (Status.is(stored.status, .SLP)) slp: {
441
437
  const before = stored.status;
442
438
  // Even if the EXT bit is set this will still correctly modify the sleep duration
443
- // if (options.calc.modify(player, .sleep)) |extend| {
444
- // if (!extend) stored.status = 0;
445
- // } else {
446
439
  stored.status -= 1;
447
- // }
448
-
449
- const duration = Status.duration(stored.status);
450
- if (!Status.is(stored.status, .EXT)) {
451
- try options.chance.sleep(player, duration);
452
- }
453
440
 
454
- if (duration == 0) {
455
- try log.curestatus(ident, before, .Message);
441
+ if (Status.duration(stored.status) == 0) {
442
+ try log.curestatus(.{ ident, before, .Message });
456
443
  stored.status = 0; // clears EXT if present
457
444
  resetCant(volatiles);
458
445
  volatiles.Nightmare = false;
459
446
  } else {
460
- try log.cant(ident, .Sleep);
447
+ try log.cant(.{ ident, .Sleep });
461
448
  if (move == .Snore or move == .SleepTalk) break :slp;
462
449
  resetCant(volatiles);
463
450
  return false;
@@ -466,7 +453,7 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
466
453
 
467
454
  if (Status.is(stored.status, .FRZ) and !(move == .FlameWheel or move == .SacredFire)) {
468
455
  resetCant(volatiles);
469
- try log.cant(ident, .Freeze);
456
+ try log.cant(.{ ident, .Freeze });
470
457
  return false;
471
458
  }
472
459
 
@@ -476,39 +463,28 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
476
463
  // if (!showdown) volatiles.Flinch = false;
477
464
  volatiles.Flinch = false;
478
465
  resetCant(volatiles);
479
- try log.cant(ident, .Flinch);
466
+ try log.cant(.{ ident, .Flinch });
480
467
  return false;
481
468
  }
482
469
 
483
470
  if (volatiles.disable.duration > 0) {
484
- // if (options.calc.modify(player, .disable)) |extend| {
485
- // if (!extend) volatiles.disable.duration = 0;
486
- // } else {
487
471
  volatiles.disable.duration -= 1;
488
- // }
489
- try options.chance.disable(player, volatiles.disable.duration);
490
472
 
491
473
  if (volatiles.disable.duration == 0) {
492
474
  volatiles.disable.move = 0;
493
- try log.end(ident, .Disable);
475
+ try log.end(.{ ident, .Disable });
494
476
  }
495
477
  }
496
478
 
497
479
  if (volatiles.Confusion) {
498
480
  assert(volatiles.confusion > 0);
499
-
500
- // if (options.calc.modify(player, .confusion)) |extend| {
501
- // if (!extend) volatiles.confusion = 0;
502
- // } else {
503
481
  volatiles.confusion -= 1;
504
- // }
505
- try options.chance.confusion(player, volatiles.confusion);
506
482
 
507
483
  if (volatiles.confusion == 0) {
508
484
  volatiles.Confusion = false;
509
- try log.end(ident, .Confusion);
485
+ try log.end(.{ ident, .Confusion });
510
486
  } else {
511
- try log.activate(ident, .Confusion);
487
+ try log.activate(.{ ident, .Confusion });
512
488
 
513
489
  if (try Rolls.confused(battle, player, options)) {
514
490
  volatiles.BeatUp = false;
@@ -523,11 +499,11 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
523
499
  }
524
500
 
525
501
  if (volatiles.Attract) {
526
- try log.activate(ident, .Attract);
502
+ try log.activate(.{ ident, .Attract });
527
503
 
528
504
  if (try Rolls.attract(battle, player, options)) {
529
505
  resetCant(volatiles);
530
- try log.cant(ident, .Attract);
506
+ try log.cant(.{ ident, .Attract });
531
507
  return false;
532
508
  }
533
509
  }
@@ -538,14 +514,14 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
538
514
  if (m != .None and m == move) {
539
515
  side.active.volatiles.Charging = false;
540
516
  resetCant(volatiles);
541
- try options.log.disabled(ident, move);
517
+ try options.log.cant(.{ ident, .Disable, move });
542
518
  return false;
543
519
  }
544
520
  }
545
521
 
546
522
  if (Status.is(stored.status, .PAR) and try Rolls.paralyzed(battle, player, options)) {
547
523
  resetCant(volatiles);
548
- try log.cant(ident, .Paralysis);
524
+ try log.cant(.{ ident, .Paralysis });
549
525
  return false;
550
526
  }
551
527
 
@@ -565,11 +541,11 @@ fn resetCant(volatiles: *align(8) Volatiles) void {
565
541
 
566
542
  pub fn canMove(battle: anytype, player: Player, state: *State, options: anytype) !bool {
567
543
  var side = battle.side(player);
568
- var volatiles = &side.active.volatiles;
544
+ const volatiles = &side.active.volatiles;
569
545
 
570
546
  // FIXME: pass in target to avoid lookup?
571
547
  const target = if (Move.get(state.move).target == .Self) player else player.foe();
572
- try options.log.moveFrom(battle.active(player), state.move, battle.active(target), state.from);
548
+ try options.log.move(.{ battle.active(player), state.move, battle.active(target), state.from });
573
549
 
574
550
  const charging = false; // TODO charging uses doUse
575
551
  const skip_pp = charging or state.move == .Struggle or
@@ -663,7 +639,7 @@ pub fn checkCriticalHit(battle: anytype, player: Player, state: *State, options:
663
639
  }
664
640
  assert(stage <= 4);
665
641
 
666
- state.crit = try Rolls.criticalHit(battle, player, CRITICAL_HITS[stage], options);
642
+ state.crit = try Rolls.criticalHit(battle, player, state.hit, CRITICAL_HITS[stage], options);
667
643
  }
668
644
 
669
645
  pub fn calcDamage(
@@ -731,7 +707,7 @@ pub fn adjustDamage(battle: anytype, player: Player, state: *State, _: anytype)
731
707
 
732
708
  pub fn randomizeDamage(battle: anytype, player: Player, state: *State, options: anytype) !void {
733
709
  if (state.damage <= 1) return;
734
- const random = try Rolls.damage(battle, player, options);
710
+ const random = try Rolls.damage(battle, player, state.hit, options);
735
711
  options.calc.base(player.foe(), state.damage);
736
712
  state.damage = @intCast(@as(u32, state.damage) *% random / 255);
737
713
  options.calc.final(player.foe(), state.damage);
@@ -849,13 +825,13 @@ fn checkFaint(
849
825
  if (foe_fainted) try faint(battle, player.foe(), !more, options);
850
826
 
851
827
  if (player_out and foe_out) {
852
- try options.log.tie();
828
+ try options.log.tie(.{});
853
829
  return Result.Tie;
854
830
  } else if (player_out) {
855
- try options.log.win(player.foe());
831
+ try options.log.win(.{player.foe()});
856
832
  return if (player == .P1) Result.Lose else Result.Win;
857
833
  } else if (foe_out) {
858
- try options.log.win(player);
834
+ try options.log.win(.{player});
859
835
  return if (player == .P1) Result.Win else Result.Lose;
860
836
  }
861
837
 
@@ -885,19 +861,19 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !void {
885
861
  side.stored().stats.spe;
886
862
  }
887
863
 
888
- try options.log.faint(battle.active(player), done);
864
+ try options.log.faint(.{ battle.active(player), done });
889
865
  options.calc.capped(player);
890
866
  }
891
867
 
892
868
  // TODO: see if we can move *before* applyDamage and include failuretext logic
893
869
  pub fn reportOutcome(battle: anytype, player: Player, state: *State, options: anytype) !void {
894
870
  const foe_ident = battle.active(player.foe());
895
- if (state.crit) try options.log.crit(foe_ident);
871
+ if (state.crit) try options.log.crit(.{foe_ident});
896
872
  if (!state.immune()) {
897
873
  if (state.effectiveness > Effectiveness.neutral) {
898
- try options.log.supereffective(foe_ident);
874
+ try options.log.supereffective(.{foe_ident});
899
875
  } else if (state.effectiveness < Effectiveness.neutral) {
900
- try options.log.resisted(foe_ident);
876
+ try options.log.resisted(.{foe_ident});
901
877
  }
902
878
  }
903
879
  }
@@ -930,8 +906,8 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !bool {
930
906
  }
931
907
 
932
908
  stored.hp -|= damage;
933
- // Pokémon Showdown uses damageOf here but its not relevant in Generation II
934
- try options.log.damage(ident, stored, if (brn) Damage.Burn else Damage.Poison);
909
+ // Pokémon Showdown uses damage [of] here but its not relevant in Generation II
910
+ try options.log.damage(.{ ident, stored, if (brn) Damage.Burn else Damage.Poison });
935
911
  if (stored.hp == 0) return true;
936
912
  }
937
913
 
@@ -947,27 +923,27 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !bool {
947
923
 
948
924
  const damage = @max(stored.stats.hp / 8, 1);
949
925
  stored.hp -|= damage;
950
- // As above, Pokémon Showdown uses damageOf but its not relevant
951
- try options.log.damage(ident, stored, .LeechSeed);
926
+ // As above, Pokémon Showdown uses damage [of] but its not relevant
927
+ try options.log.damage(.{ ident, stored, .LeechSeed });
952
928
 
953
929
  const before = foe_stored.hp;
954
930
  foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
955
931
  // Pokémon Showdown uses the less specific heal here instead of drain... because reasons?
956
- if (foe_stored.hp > before) try options.log.heal(foe_ident, foe_stored, .Silent);
932
+ if (foe_stored.hp > before) try options.log.heal(.{ foe_ident, foe_stored, .Silent });
957
933
  if (stored.hp == 0) return true;
958
934
  }
959
935
 
960
936
  if (volatiles.Nightmare) {
961
937
  stored.hp -= @max(stored.stats.hp / 4, 1);
962
938
  // ibid
963
- try options.log.damage(ident, stored, .Nightmare);
939
+ try options.log.damage(.{ ident, stored, .Nightmare });
964
940
  if (stored.hp == 0) return true;
965
941
  }
966
942
 
967
943
  if (volatiles.Curse) {
968
944
  stored.hp -= @max(stored.stats.hp / 4, 1);
969
945
  // ibid
970
- try options.log.damage(ident, stored, .Curse);
946
+ try options.log.damage(.{ ident, stored, .Curse });
971
947
  if (stored.hp == 0) return true;
972
948
  }
973
949
 
@@ -988,7 +964,7 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
988
964
  future_sight.count -= 1;
989
965
  assert(future_sight.count != 0);
990
966
  if (future_sight.count == 1) {
991
- try options.log.end(battle.active(player), .FutureSight);
967
+ try options.log.end(.{ battle.active(player), .FutureSight });
992
968
  // TODO doMove
993
969
  }
994
970
  }
@@ -997,21 +973,24 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
997
973
 
998
974
  // Weather
999
975
  if (battle.field.weather != .None) {
1000
- assert(battle.field.weather_duration > 0);
1001
976
  battle.field.weather_duration -= 1;
1002
977
 
1003
978
  if (battle.field.weather_duration == 0) {
1004
979
  battle.field.weather = .None;
1005
- try options.log.weather(battle.field.weather, .None);
980
+ try options.log.weather(.{ battle.field.weather, protocol.Weather.None });
1006
981
  } else {
1007
- try options.log.weather(battle.field.weather, .Upkeep);
982
+ try options.log.weather(.{ battle.field.weather, protocol.Weather.Upkeep });
1008
983
  if (battle.field.weather == .Sandstorm) {
1009
984
  inline for (players) |player| {
1010
985
  var side = battle.side(player);
1011
986
  const active = side.active;
1012
987
  if (!(active.volatiles.Underground or active.types.sandstormImmune())) {
1013
988
  side.stored().hp -|= @max(side.stored().stats.hp / 8, 1);
1014
- try options.log.damage(battle.active(player), side.stored(), .Sandstorm);
989
+ try options.log.damage(.{
990
+ battle.active(player),
991
+ side.stored(),
992
+ .Sandstorm,
993
+ });
1015
994
  }
1016
995
  }
1017
996
  if (try checkFaint(battle, .P1, options)) |r| return r;
@@ -1023,19 +1002,24 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1023
1002
  inline for (players) |player| {
1024
1003
  var side = battle.side(player);
1025
1004
  var volatiles = &side.active.volatiles;
1005
+ const ident = battle.active(player);
1026
1006
 
1027
1007
  if (volatiles.bind.duration > 0 and !volatiles.Substitute) {
1028
1008
  volatiles.bind.duration -= 1;
1029
1009
  if (volatiles.bind.duration == 0) {
1030
1010
  const reason = @intFromEnum(End.Bind) + volatiles.bind.reason - 1;
1031
- try options.log.end(battle.active(player), @enumFromInt(reason));
1011
+ try options.log.end(.{ ident, @as(End, @enumFromInt(reason)) });
1032
1012
  } else {
1033
1013
  var reason = @intFromEnum(Activate.Bind) + volatiles.bind.reason - 1;
1034
- try options.log.activate(battle.active(player), @enumFromInt(reason));
1014
+ try options.log.activate(.{ ident, @as(Activate, @enumFromInt(reason)) });
1035
1015
 
1036
1016
  reason = @intFromEnum(Damage.Bind) + volatiles.bind.reason - 1;
1037
1017
  side.stored().hp -|= @max(side.stored().stats.hp / 16, 1);
1038
- try options.log.damage(battle.active(player), side.stored(), @enumFromInt(reason));
1018
+ try options.log.damage(.{
1019
+ ident,
1020
+ side.stored(),
1021
+ @as(Damage, @enumFromInt(reason)),
1022
+ });
1039
1023
  }
1040
1024
  }
1041
1025
  }
@@ -1047,12 +1031,11 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1047
1031
  var volatiles = &side.active.volatiles;
1048
1032
 
1049
1033
  if (volatiles.PerishSong) {
1050
- assert(volatiles.perish_song > 0);
1051
1034
  volatiles.perish_song -= 1;
1052
1035
 
1053
1036
  assert(volatiles.perish_song < 3);
1054
1037
  const reason = @intFromEnum(Start.PerishSong0) + volatiles.perish_song;
1055
- try options.log.start(battle.active(player), @enumFromInt(reason));
1038
+ try options.log.start(.{ battle.active(player), @as(Start, @enumFromInt(reason)) });
1056
1039
 
1057
1040
  if (volatiles.perish_song == 0) {
1058
1041
  volatiles.PerishSong = false;
@@ -1069,13 +1052,15 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1069
1052
  if (stored.item == .Leftovers) {
1070
1053
  const before = stored.hp;
1071
1054
  stored.hp = @min(stored.hp + @max(stored.stats.hp / 16, 1), stored.stats.hp);
1072
- if (stored.hp > before) try options.log.heal(battle.active(player), stored, .Leftovers);
1055
+ if (stored.hp > before) {
1056
+ try options.log.heal(.{ battle.active(player), stored, .Leftovers });
1057
+ }
1073
1058
  }
1074
1059
  }
1075
1060
 
1076
1061
  // Mystery Berry
1077
1062
  inline for (players) |player| {
1078
- var stored = battle.side(player).stored();
1063
+ const stored = battle.side(player).stored();
1079
1064
 
1080
1065
  if (stored.item == .MysteryBerry) {
1081
1066
  // TODO stored and active moves, transform, etc
@@ -1090,7 +1075,7 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1090
1075
  !side.active.volatiles.frozen and
1091
1076
  try Rolls.defrost(battle, player, options))
1092
1077
  {
1093
- try options.log.curestatus(battle.active(player), side.stored().status, .Message);
1078
+ try options.log.curestatus(.{ battle.active(player), side.stored().status, .Message });
1094
1079
  side.stored().status = 0;
1095
1080
  }
1096
1081
  }
@@ -1131,14 +1116,14 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1131
1116
  stored.hp < stored.stats.hp;
1132
1117
  if (proc) {
1133
1118
  assert(stored.hp != 0);
1134
- try options.log.enditem(ident, stored.item, .Eat);
1119
+ try options.log.enditem(.{ ident, stored.item, .Eat });
1135
1120
  stored.item = .None;
1136
1121
 
1137
1122
  const offset = num - @intFromEnum(Item.Berry);
1138
1123
  assert(offset < 3);
1139
1124
  stored.hp = @min(stored.hp + ((offset + 1) * 10), stored.stats.hp);
1140
1125
  const reason: Heal = @enumFromInt(@intFromEnum(Heal.Berry) + offset);
1141
- try options.log.heal(ident, stored, reason);
1126
+ try options.log.heal(.{ ident, stored, reason });
1142
1127
  }
1143
1128
  } else if (num < @intFromEnum(Item.BitterBerry)) {
1144
1129
  const proc = if (stored.item == .MiracleBerry)
@@ -1148,7 +1133,7 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1148
1133
 
1149
1134
  if (proc) {
1150
1135
  assert(Status.any(stored.status));
1151
- try options.log.enditem(ident, stored.item, .Eat);
1136
+ try options.log.enditem(.{ ident, stored.item, .Eat });
1152
1137
  stored.item = .None;
1153
1138
 
1154
1139
  // FIXME wtf?
@@ -1156,21 +1141,21 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1156
1141
  // side.active.volatiles.Nightmare = false;
1157
1142
  if (stored.item == .MiracleBerry) {
1158
1143
  side.active.volatiles.Confusion = false;
1159
- try options.log.end(ident, .Confusion);
1144
+ try options.log.end(.{ ident, .Confusion });
1160
1145
  }
1161
1146
  stored.status = 0;
1162
1147
 
1163
1148
  // FIXME recalc stats
1164
1149
 
1165
- try options.log.curestatus(ident, stored.status, .Message);
1150
+ try options.log.curestatus(.{ ident, stored.status, .Message });
1166
1151
  }
1167
1152
  } else if (num == @intFromEnum(Item.BitterBerry)) {
1168
1153
  if (side.active.volatiles.Confusion) {
1169
- try options.log.enditem(ident, stored.item, .Eat);
1154
+ try options.log.enditem(.{ ident, stored.item, .Eat });
1170
1155
  stored.item = .None;
1171
1156
 
1172
1157
  side.active.volatiles.Confusion = false;
1173
- try options.log.end(ident, .Confusion);
1158
+ try options.log.end(.{ ident, .Confusion });
1174
1159
  }
1175
1160
  }
1176
1161
  }
@@ -1182,12 +1167,11 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1182
1167
  var volatiles = &side.active.volatiles;
1183
1168
 
1184
1169
  if (volatiles.Encore) {
1185
- assert(volatiles.encore > 0);
1186
1170
  volatiles.encore -= 1;
1187
1171
 
1188
1172
  if (volatiles.encore == 0 or side.active.move(mslot).pp == 0) {
1189
1173
  volatiles.Encore = false;
1190
- try options.log.end(battle.active(player), .Encore);
1174
+ try options.log.end(.{ battle.active(player), .Encore });
1191
1175
  }
1192
1176
  }
1193
1177
  }
@@ -1204,12 +1188,11 @@ fn updateSideCondition(
1204
1188
  ) !void {
1205
1189
  var side = battle.side(player);
1206
1190
  if (@field(side.conditions, key)) {
1207
- assert(@field(side.conditions, value) > 0);
1208
1191
  @field(side.conditions, value) -= 1;
1209
1192
 
1210
1193
  if (@field(side.conditions, value) == 0) {
1211
1194
  @field(side.conditions, key) = false;
1212
- try options.log.sideend(player, @field(protocol.Side, key));
1195
+ try options.log.sideend(.{ player, @field(protocol.Side, key) });
1213
1196
  }
1214
1197
  }
1215
1198
  }
@@ -1218,13 +1201,13 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
1218
1201
  battle.turn += 1;
1219
1202
 
1220
1203
  if (showdown and battle.turn >= 1000) {
1221
- try options.log.tie();
1204
+ try options.log.tie(.{});
1222
1205
  return Result.Tie;
1223
1206
  } else if (battle.turn >= 65535) {
1224
1207
  return Result.Error;
1225
1208
  }
1226
1209
 
1227
- try options.log.turn(battle.turn);
1210
+ try options.log.turn(.{battle.turn});
1228
1211
 
1229
1212
  return Result.Default;
1230
1213
  }
@@ -1240,7 +1223,7 @@ pub fn buildRage(battle: anytype, player: Player, state: *State, options: anytyp
1240
1223
  }
1241
1224
  } else {
1242
1225
  foe.active.volatiles.rage +|= 1;
1243
- try options.log.activate(battle.active(player), .Rage);
1226
+ try options.log.activate(.{ battle.active(player), .Rage });
1244
1227
  }
1245
1228
  }
1246
1229
  }
@@ -1263,7 +1246,7 @@ pub fn destinyBond(battle: anytype, player: Player, _: *State, options: anytype)
1263
1246
 
1264
1247
  if (foe.active.volatiles.DestinyBond) {
1265
1248
  battle.side(player).stored().hp = 0;
1266
- try options.log.activate(battle.active(player.foe()), .DestinyBond);
1249
+ try options.log.activate(.{ battle.active(player.foe()), .DestinyBond });
1267
1250
  }
1268
1251
  return true;
1269
1252
  }
@@ -1276,14 +1259,14 @@ pub const Effects = struct {
1276
1259
  const gender: u8 = @intFromEnum(side.stored().dvs.gender);
1277
1260
  const foe_gender: u8 = @intFromEnum(foe.stored().dvs.gender);
1278
1261
  if (gender + foe_gender != 1) {
1279
- return options.log.immune(battle.active(player.foe()), .None);
1262
+ return options.log.immune(.{ battle.active(player.foe()), .None });
1280
1263
  } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
1281
- try options.log.lastmiss();
1282
- return options.log.miss(battle.active(player));
1264
+ try options.log.lastmiss(.{});
1265
+ return options.log.miss(.{battle.active(player)});
1283
1266
  }
1284
1267
 
1285
1268
  foe.active.volatiles.Attract = true;
1286
- try options.log.start(battle.active(player.foe()), .Attract);
1269
+ try options.log.start(.{ battle.active(player.foe()), .Attract });
1287
1270
  }
1288
1271
 
1289
1272
  pub fn batonPass(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1295,14 +1278,32 @@ pub const Effects = struct {
1295
1278
  }
1296
1279
 
1297
1280
  pub fn bellyDrum(battle: anytype, player: Player, state: *State, options: anytype) !void {
1298
- _ = .{ battle, player, state, options }; // TODO
1299
- // assert(!state.miss);
1300
- // var side = battle.side(player);
1301
- // try boostStat(battle, side, &side.active.stats, &side.active.boosts, "atk", 2);
1302
- // TODO
1303
- // |-boost|p1a: Poliwag|atk|2|[silent]
1304
- // |-fail|p1a: Poliwag
1305
- //
1281
+ assert(!state.miss);
1282
+ var side = battle.side(player);
1283
+
1284
+ const ident = battle.active(player);
1285
+
1286
+ assert(side.active.boosts.atk >= -6 and side.active.boosts.atk <= 6);
1287
+ if (side.active.boosts.atk == 6 or side.active.boosts.atk == MAX_STAT_VALUE) {
1288
+ return options.log.fail(.{ ident, .None });
1289
+ }
1290
+
1291
+ const hp = @max(side.stored().stats.hp / 2, 1);
1292
+ if (side.stored().hp <= hp) {
1293
+ try boostStat(battle, side, &side.active.stats, &side.active.boosts, "atk", 2);
1294
+ try options.log.boost(.{ ident, .Attack, @as(i8, 2), .Silent });
1295
+ return options.log.fail(.{ ident, .None });
1296
+ } else {
1297
+ side.stored().hp -= hp;
1298
+ try options.log.damage(.{ ident, side.stored(), .None });
1299
+
1300
+ const before = side.active.boosts.atk;
1301
+ var i: usize = 0;
1302
+ while (i < 6 and side.active.stats.atk < MAX_STAT_VALUE) : (i += 1) {
1303
+ try boostStat(battle, side, &side.active.stats, &side.active.boosts, "atk", 2);
1304
+ }
1305
+ try options.log.boost(.{ ident, .Attack, @as(i8, side.active.boosts.atk - before) });
1306
+ }
1306
1307
  }
1307
1308
 
1308
1309
  pub const bide = struct {
@@ -1314,18 +1315,11 @@ pub const Effects = struct {
1314
1315
 
1315
1316
  if (!volatiles.Bide) return;
1316
1317
 
1317
- assert(volatiles.attacks > 0);
1318
- // if (options.calc.modify(player, .attacking)) |extend| {
1319
- // if (!extend) volatiles.attacks = 0;
1320
- // } else {
1321
1318
  volatiles.attacks -= 1;
1322
- // }
1323
- try options.chance.attacking(player, volatiles.attacks);
1324
-
1325
- if (volatiles.attacks != 0) return options.log.activate(ident, .Bide);
1319
+ if (volatiles.attacks != 0) return options.log.activate(.{ ident, .Bide });
1326
1320
 
1327
1321
  volatiles.Bide = false;
1328
- try options.log.end(ident, .Bide);
1322
+ try options.log.end(.{ ident, .Bide });
1329
1323
 
1330
1324
  state.damage = volatiles.bide *% 2;
1331
1325
  volatiles.bide = 0;
@@ -1359,11 +1353,11 @@ pub const Effects = struct {
1359
1353
  if (Status.any(foe_stored.status)) {
1360
1354
  if (Status.is(foe_stored.status, .FRZ)) {
1361
1355
  assert(Move.get(state.move).type == .Fire or state.move == .TriAttack);
1362
- try options.log.curestatus(
1356
+ try options.log.curestatus(.{
1363
1357
  battle.active(player.foe()),
1364
1358
  foe_stored.status,
1365
1359
  .Message,
1366
- );
1360
+ });
1367
1361
  foe_stored.status = 0;
1368
1362
  }
1369
1363
  return;
@@ -1378,7 +1372,8 @@ pub const Effects = struct {
1378
1372
 
1379
1373
  foe_stored.status = Status.init(.BRN);
1380
1374
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
1381
- try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
1375
+
1376
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
1382
1377
 
1383
1378
  // TODO: check for status berry
1384
1379
  }
@@ -1398,7 +1393,7 @@ pub const Effects = struct {
1398
1393
  for (side.active.moves) |m| {
1399
1394
  if (convertible(side.active, m.id)) n += 1;
1400
1395
  }
1401
- if (n == 0) return options.log.fail(battle.active(player), .None);
1396
+ if (n == 0) return options.log.fail(.{ battle.active(player), .None });
1402
1397
 
1403
1398
  side.active.types.type1 = try Rolls.conversion(battle, player, n, options);
1404
1399
  side.active.types.type2 = side.active.types.type1;
@@ -1408,9 +1403,9 @@ pub const Effects = struct {
1408
1403
  var side = battle.side(player);
1409
1404
 
1410
1405
  const last = battle.foe(player).lastMove(false);
1411
- if (last == .None) return options.log.fail(battle.active(player), .None);
1406
+ if (last == .None) return options.log.fail(.{ battle.active(player), .None });
1412
1407
  const move = Move.get(last);
1413
- if (move.type == .@"???") return options.log.fail(battle.active(player), .None);
1408
+ if (move.type == .@"???") return options.log.fail(.{ battle.active(player), .None });
1414
1409
 
1415
1410
  side.active.types.type1 = try Rolls.conversion2(battle, player, move.type, options);
1416
1411
  side.active.types.type2 = side.active.types.type1;
@@ -1452,41 +1447,41 @@ pub const Effects = struct {
1452
1447
 
1453
1448
  if (side.active.types.includes(.Ghost)) {
1454
1449
  if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
1455
- try options.log.lastmiss();
1456
- return options.log.miss(battle.active(player));
1450
+ try options.log.lastmiss(.{});
1451
+ return options.log.miss(.{battle.active(player)});
1457
1452
  } else if (foe.active.volatiles.Substitute) {
1458
- return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
1453
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
1459
1454
  } else if (foe.active.volatiles.Curse) {
1460
- return options.log.fail(foe_ident, .None);
1455
+ return options.log.fail(.{ foe_ident, .None });
1461
1456
  }
1462
1457
 
1463
1458
  foe.active.volatiles.Curse = true;
1464
- try options.log.start(foe_ident, .Curse);
1459
+ try options.log.start(.{ foe_ident, .Curse });
1465
1460
 
1466
1461
  side.stored().hp -= @max(side.stored().stats.hp / 2, 1);
1467
- try options.log.damage(ident, side.stored(), .Curse);
1462
+ try options.log.damage(.{ ident, side.stored(), .Curse });
1468
1463
  } else {
1469
- var stats = &side.active.stats;
1470
- var boosts = &side.active.boosts;
1464
+ const stats = &side.active.stats;
1465
+ const boosts = &side.active.boosts;
1471
1466
 
1472
1467
  const max_atk = boosts.atk == 6 or stats.atk == MAX_STAT_VALUE;
1473
1468
  const max_def = boosts.def == 6 or stats.def == MAX_STAT_VALUE;
1474
- if (max_atk and max_def) return options.log.fail(ident, .None);
1469
+ if (max_atk and max_def) return options.log.fail(.{ ident, .None });
1475
1470
 
1476
1471
  if (!(boosts.spe == -6 or stats.spe == 1)) {
1477
1472
  assert(boosts.spe >= -6 and boosts.spe <= 6);
1478
1473
  try unboostStat(battle, side, stats, boosts, "spe", 1);
1479
- try options.log.boost(ident, .Speed, @as(i8, -1));
1474
+ try options.log.boost(.{ ident, .Speed, @as(i8, -1) });
1480
1475
  }
1481
1476
  if (!max_atk) {
1482
1477
  assert(boosts.atk >= -6 and boosts.atk <= 6);
1483
1478
  try boostStat(battle, side, stats, boosts, "atk", 1);
1484
- try options.log.boost(ident, .Attack, @as(i8, 1));
1479
+ try options.log.boost(.{ ident, .Attack, @as(i8, 1) });
1485
1480
  }
1486
1481
  if (!max_def) {
1487
1482
  assert(boosts.def >= -6 and boosts.def <= 6);
1488
1483
  try boostStat(battle, side, stats, boosts, "def", 1);
1489
- try options.log.boost(ident, .Defense, @as(i8, 1));
1484
+ try options.log.boost(.{ ident, .Defense, @as(i8, 1) });
1490
1485
  }
1491
1486
  }
1492
1487
  }
@@ -1498,14 +1493,13 @@ pub const Effects = struct {
1498
1493
  pub fn defrost(battle: anytype, player: Player, _: *State, options: anytype) !void {
1499
1494
  var side = battle.side(player);
1500
1495
  assert(Status.is(side.stored().status, .FRZ));
1501
-
1502
- try options.log.curestatus(battle.active(player), side.stored().status, .Message);
1496
+ try options.log.curestatus(.{ battle.active(player), side.stored().status, .Message });
1503
1497
  side.stored().status = 0;
1504
1498
  }
1505
1499
 
1506
1500
  pub fn destinyBond(battle: anytype, player: Player, _: *State, options: anytype) !void {
1507
1501
  battle.side(player).active.volatiles.DestinyBond = true;
1508
- try options.log.singlemove(battle.active(player), Move.DestinyBond);
1502
+ try options.log.singlemove(.{ battle.active(player), Move.DestinyBond });
1509
1503
  }
1510
1504
 
1511
1505
  pub fn disable(battle: anytype, player: Player, _: *State, options: anytype) !void {
@@ -1513,14 +1507,10 @@ pub const Effects = struct {
1513
1507
  var volatiles = &foe.active.volatiles;
1514
1508
  const foe_ident = battle.active(player.foe());
1515
1509
 
1516
- if (volatiles.disable.move != 0) {
1517
- return options.log.fail(foe_ident, .None);
1518
- }
1510
+ if (volatiles.disable.move != 0) return options.log.fail(.{ foe_ident, .None });
1519
1511
 
1520
1512
  const last = battle.foe(player).lastMove(false);
1521
- if (last == .None or last == .Struggle) {
1522
- return options.log.fail(foe_ident, .None);
1523
- }
1513
+ if (last == .None or last == .Struggle) return options.log.fail(.{ foe_ident, .None });
1524
1514
 
1525
1515
  var slot: u3 = 0;
1526
1516
  for (foe.active.moves) |m| {
@@ -1529,12 +1519,12 @@ pub const Effects = struct {
1529
1519
  if (m.id == last) break;
1530
1520
  }
1531
1521
  const move = foe.active.move(slot);
1532
- if (move.pp == 0) return options.log.fail(foe_ident, .None);
1522
+ if (move.pp == 0) return options.log.fail(.{ foe_ident, .None });
1533
1523
 
1534
1524
  volatiles.disable.move = slot;
1535
1525
  volatiles.disable.duration = Rolls.disableDuration(battle, player, options);
1536
1526
 
1537
- try options.log.startEffect(foe_ident, .Disable, move.id);
1527
+ try options.log.start(.{ foe_ident, .Disable, move.id });
1538
1528
  }
1539
1529
 
1540
1530
  pub const double = struct {
@@ -1553,13 +1543,14 @@ pub const Effects = struct {
1553
1543
 
1554
1544
  pub fn drainHP(battle: anytype, player: Player, state: *State, options: anytype) !void {
1555
1545
  var stored = battle.side(player).stored();
1546
+ const ident = battle.active(player);
1556
1547
 
1557
1548
  assert(state.damage != 0);
1558
1549
 
1559
1550
  if (stored.hp == stored.stats.hp) return;
1560
1551
  stored.hp = @min(stored.stats.hp, stored.hp + @max(state.damage / 2, 1));
1561
1552
 
1562
- try options.log.drain(battle.active(player), stored, battle.active(player.foe()));
1553
+ try options.log.heal(.{ ident, stored, .Drain, battle.active(player.foe()) });
1563
1554
  }
1564
1555
 
1565
1556
  pub fn encore(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1571,7 +1562,7 @@ pub const Effects = struct {
1571
1562
  const failed = volatiles.Encore or
1572
1563
  (last == .None or last == .Struggle or
1573
1564
  last == .Encore or .last == .MirrorMove);
1574
- if (failed) return options.log.fail(foe_ident, .None);
1565
+ if (failed) return options.log.fail(.{ foe_ident, .None });
1575
1566
 
1576
1567
  var slot: u3 = 0;
1577
1568
  for (foe.active.moves) |m| {
@@ -1580,7 +1571,7 @@ pub const Effects = struct {
1580
1571
  if (m.id == last) break;
1581
1572
  }
1582
1573
  const move = foe.active.move(slot);
1583
- if (move.pp == 0) return options.log.fail(foe_ident, .None);
1574
+ if (move.pp == 0) return options.log.fail(.{ foe_ident, .None });
1584
1575
 
1585
1576
  volatiles.Encore = true;
1586
1577
  volatiles.encore = Rolls.encoreDuration(battle, player, options);
@@ -1588,11 +1579,19 @@ pub const Effects = struct {
1588
1579
  // TODO ???
1589
1580
  _ = state;
1590
1581
 
1591
- try options.log.startEffect(foe_ident, .Encore, move.id);
1582
+ try options.log.start(.{ foe_ident, .Encore, move.id });
1592
1583
  }
1593
1584
 
1594
- pub fn explode(battle: anytype, player: Player, state: *State, options: anytype) !void {
1595
- _ = .{ battle, player, state, options }; // TODO
1585
+ pub fn explode(battle: anytype, player: Player, _: *State, _: anytype) !void {
1586
+ var side = battle.side(player);
1587
+ var stored = side.stored();
1588
+
1589
+ stored.hp = 0;
1590
+ // Pokémon Showdown sets the status to 0 on faint(), and we need to do the same to be able
1591
+ // to correctly implement Pokémon Showdown's dumb speed-based switch mechanics
1592
+ if (!showdown) stored.status = 0;
1593
+ side.active.volatiles.LeechSeed = false;
1594
+ side.active.volatiles.DestinyBond = false;
1596
1595
  }
1597
1596
 
1598
1597
  pub fn falseSwipe(battle: anytype, player: Player, state: *State, _: anytype) !void {
@@ -1642,12 +1641,11 @@ pub const Effects = struct {
1642
1641
  var side = battle.side(player);
1643
1642
 
1644
1643
  if (side.active.volatiles.FocusEnergy) {
1645
- if (!showdown) try options.log.fail(battle.active(player), .None);
1646
- return;
1644
+ return if (!showdown) try options.log.fail(.{ battle.active(player), .None });
1647
1645
  }
1648
1646
 
1649
1647
  side.active.volatiles.FocusEnergy = true;
1650
- try options.log.start(battle.active(player), .FocusEnergy);
1648
+ try options.log.start(.{ battle.active(player), .FocusEnergy });
1651
1649
  }
1652
1650
 
1653
1651
  var SLOTS: [5]u4 = .{ 0, 0, 0, 0, 0 };
@@ -1657,7 +1655,7 @@ pub const Effects = struct {
1657
1655
 
1658
1656
  const foe = battle.foe(player);
1659
1657
 
1660
- if (state.first) return options.log.fail(battle.active(player), .None);
1658
+ if (state.first) return options.log.fail(.{ battle.active(player), .None });
1661
1659
 
1662
1660
  var n: u4 = 0;
1663
1661
  var i: u4 = 0;
@@ -1671,7 +1669,7 @@ pub const Effects = struct {
1671
1669
  }
1672
1670
  }
1673
1671
 
1674
- if (n == 0) return options.log.fail(battle.active(player), .None);
1672
+ if (n == 0) return options.log.fail(.{ battle.active(player), .None });
1675
1673
 
1676
1674
  const slot = try Rolls.forceSwitch(battle, player, SLOTS[0..i], n, options);
1677
1675
  try switchIn(battle, player.foe(), slot, .Drag, options);
@@ -1681,14 +1679,14 @@ pub const Effects = struct {
1681
1679
  var foe = battle.foe(player);
1682
1680
 
1683
1681
  if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
1684
- try options.log.lastmiss();
1685
- return options.log.miss(battle.active(player));
1682
+ try options.log.lastmiss(.{});
1683
+ return options.log.miss(.{battle.active(player)});
1686
1684
  } else if (foe.active.volatiles.Foresight) {
1687
- return options.log.fail(battle.active(player), .None);
1685
+ return options.log.fail(.{ battle.active(player), .None });
1688
1686
  }
1689
1687
 
1690
1688
  foe.active.volatiles.Foresight = true;
1691
- try options.log.start(battle.active(player.foe()), .FocusEnergy);
1689
+ try options.log.start(.{ battle.active(player.foe()), .FocusEnergy });
1692
1690
  }
1693
1691
 
1694
1692
  pub fn freezeChance(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1711,7 +1709,7 @@ pub const Effects = struct {
1711
1709
  if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1712
1710
 
1713
1711
  foe_stored.status = Status.init(.FRZ);
1714
- try options.log.status(foe_ident, foe_stored.status, .None);
1712
+ try options.log.status(.{ foe_ident, foe_stored.status, .None });
1715
1713
 
1716
1714
  // TODO: check for status berry
1717
1715
 
@@ -1719,8 +1717,26 @@ pub const Effects = struct {
1719
1717
  foe.active.volatiles.frozen = true;
1720
1718
  }
1721
1719
 
1722
- pub fn furyCutter(battle: anytype, player: Player, state: *State, options: anytype) !void {
1723
- _ = .{ battle, player, state, options }; // TODO
1720
+ // TODO FIXME showdown calls this before calcDamage (after checkHit) and modifies state.bp!
1721
+ pub fn furyCutter(battle: anytype, player: Player, state: *State, _: anytype) !void {
1722
+ var side = battle.side(player);
1723
+ var volatiles = &side.active.volatiles;
1724
+
1725
+ if (state.miss) {
1726
+ volatiles.fury_cutter = 0;
1727
+ return;
1728
+ }
1729
+
1730
+ assert(volatiles.fury_cutter >= 0 and volatiles.fury_cutter <= 5);
1731
+ if (volatiles.fury_cutter == 5) return;
1732
+ volatiles.fury_cutter += 1;
1733
+
1734
+ if (showdown) {
1735
+ state.bp *= 2;
1736
+ assert(state.bp <= 160);
1737
+ } else {
1738
+ state.damage *|= 2;
1739
+ }
1724
1740
  }
1725
1741
 
1726
1742
  pub fn futureSight(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1746,7 +1762,7 @@ pub const Effects = struct {
1746
1762
  inline for (STATS) |s| @field(foe.active.stats, s) = @field(stats, s);
1747
1763
  statusModify(foe.stored().status, &foe.active.stats);
1748
1764
 
1749
- try options.log.clearallboost();
1765
+ try options.log.clearallboost(.{});
1750
1766
  }
1751
1767
 
1752
1768
  pub fn heal(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1754,7 +1770,7 @@ pub const Effects = struct {
1754
1770
  var stored = side.stored();
1755
1771
  const ident = battle.active(player);
1756
1772
 
1757
- if (stored.stats.hp == stored.hp) return options.log.fail(ident, .None);
1773
+ if (stored.stats.hp == stored.hp) return options.log.fail(.{ ident, .None });
1758
1774
 
1759
1775
  const rest = state.move == .Rest;
1760
1776
  if (rest) {
@@ -1763,19 +1779,19 @@ pub const Effects = struct {
1763
1779
  side.active.volatiles.Toxic = false;
1764
1780
  stored.status = Status.slf(2);
1765
1781
  recomputeStats(battle, side);
1766
- try options.log.statusFrom(ident, stored.status, Move.Rest);
1782
+ try options.log.status(.{ ident, stored.status, .From, Move.Rest });
1767
1783
  stored.hp = stored.stats.hp;
1768
1784
  } else {
1769
1785
  stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
1770
1786
  }
1771
- try options.log.heal(ident, stored, if (rest) Heal.Silent else Heal.None);
1787
+ try options.log.heal(.{ ident, stored, if (rest) Heal.Silent else Heal.None });
1772
1788
  }
1773
1789
 
1774
1790
  pub fn healBell(battle: anytype, player: Player, _: *State, options: anytype) !void {
1775
1791
  var side = battle.side(player);
1776
1792
  side.active.volatiles.Nightmare = false;
1777
1793
 
1778
- try options.log.cureteam(battle.active(player));
1794
+ try options.log.cureteam(.{battle.active(player)});
1779
1795
  for (&side.pokemon) |*p| p.status = 0;
1780
1796
 
1781
1797
  // We technically only need to recompute stats if the active Pokémon was burned/paralyzed,
@@ -1791,7 +1807,7 @@ pub const Effects = struct {
1791
1807
 
1792
1808
  pub fn hyperBeam(battle: anytype, player: Player, _: *State, options: anytype) !void {
1793
1809
  battle.side(player).active.volatiles.Recharging = true;
1794
- try options.log.mustrecharge(battle.active(player));
1810
+ try options.log.mustrecharge(.{battle.active(player)});
1795
1811
  }
1796
1812
 
1797
1813
  pub fn leechSeed(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1801,12 +1817,12 @@ pub const Effects = struct {
1801
1817
  // Invulnerability trumps type immunity on Pokémon Showdown
1802
1818
  const invulnerable = foe.active.volatiles.Underground or foe.active.volatiles.Flying;
1803
1819
  if (!invulnerable and foe.active.types.includes(.Grass)) {
1804
- return options.log.immune(battle.active(player.foe()), .None);
1820
+ return options.log.immune(.{ battle.active(player.foe()), .None });
1805
1821
  }
1806
1822
  try checkHit(battle, player, state, options);
1807
1823
  if (state.miss) {
1808
- try options.log.lastmiss();
1809
- return options.log.miss(battle.active(player));
1824
+ try options.log.lastmiss(.{});
1825
+ return options.log.miss(.{battle.active(player)});
1810
1826
  }
1811
1827
  if (foe.active.volatiles.LeechSeed) return;
1812
1828
  } else {
@@ -1814,14 +1830,14 @@ pub const Effects = struct {
1814
1830
  const miss =
1815
1831
  state.miss or foe.active.types.includes(.Grass) or foe.active.volatiles.LeechSeed;
1816
1832
  if (miss) {
1817
- try options.log.lastmiss();
1818
- return options.log.miss(battle.active(player));
1833
+ try options.log.lastmiss(.{});
1834
+ return options.log.miss(.{battle.active(player)});
1819
1835
  }
1820
1836
  }
1821
1837
 
1822
1838
  foe.active.volatiles.LeechSeed = true;
1823
1839
 
1824
- try options.log.start(battle.active(player.foe()), .LeechSeed);
1840
+ try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
1825
1841
  }
1826
1842
 
1827
1843
  pub fn lockOn(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1829,20 +1845,20 @@ pub const Effects = struct {
1829
1845
  const foe_ident = battle.active(player.foe());
1830
1846
 
1831
1847
  if (foe.active.volatiles.Substitute) {
1832
- return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
1848
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
1833
1849
  } else if (state.miss) {
1834
- return options.log.fail(foe_ident, .None);
1850
+ return options.log.fail(.{ foe_ident, .None });
1835
1851
  }
1836
1852
 
1837
1853
  foe.active.volatiles.LockOn = true;
1838
- try options.log.activate(battle.active(player), .LockOn);
1854
+ try options.log.activate(.{ battle.active(player), .LockOn });
1839
1855
  }
1840
1856
 
1841
1857
  const MAGNITUDE_POWER = [_]u8{ 10, 30, 50, 70, 90, 110, 150 };
1842
1858
 
1843
1859
  pub fn magnitude(battle: anytype, player: Player, state: *State, options: anytype) !void {
1844
1860
  const num = try Rolls.magnitude(battle, player, options);
1845
- try options.log.magnitude(battle.active(player), num);
1861
+ try options.log.activate(.{ battle.active(player), .Magnitude, num });
1846
1862
  state.bp = MAGNITUDE_POWER[num - 4];
1847
1863
  }
1848
1864
 
@@ -1850,11 +1866,11 @@ pub const Effects = struct {
1850
1866
  var foe = battle.foe(player);
1851
1867
 
1852
1868
  if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
1853
- if (!showdown) return options.log.fail(battle.active(player.foe()), .None);
1854
- try options.log.lastmiss();
1855
- return options.log.miss(battle.active(player));
1869
+ if (!showdown) return options.log.fail(.{ battle.active(player.foe()), .None });
1870
+ try options.log.lastmiss(.{});
1871
+ return options.log.miss(.{battle.active(player)});
1856
1872
  } else if (foe.active.volatiles.Trapped) {
1857
- return options.log.fail(battle.active(player.foe()), .None);
1873
+ return options.log.fail(.{ battle.active(player.foe()), .None });
1858
1874
  }
1859
1875
 
1860
1876
  foe.active.volatiles.Trapped = true;
@@ -1881,7 +1897,29 @@ pub const Effects = struct {
1881
1897
  }
1882
1898
 
1883
1899
  pub fn mimic(battle: anytype, player: Player, state: *State, options: anytype) !void {
1884
- _ = .{ battle, player, state, options }; // TODO
1900
+ var side = battle.side(player);
1901
+
1902
+ if (state.miss) {
1903
+ if (!showdown) return options.log.fail(.{ battle.active(player.foe()), .None });
1904
+ try options.log.lastmiss(.{});
1905
+ return options.log.miss(.{battle.active(player)});
1906
+ }
1907
+
1908
+ const move = battle.foe(player).lastMove(false);
1909
+ if (move == .None or move == .Struggle) {
1910
+ return options.log.fail(.{ battle.active(player.foe()), .None });
1911
+ }
1912
+
1913
+ var slot: u4 = 0;
1914
+ for (side.active.moves, 0..) |m, i| {
1915
+ if (m.id == move) return options.log.fail(.{ battle.active(player.foe()), .None });
1916
+ if (m.id == .Mimic) slot = @intCast(i + 1);
1917
+ }
1918
+ assert(slot != 0 and slot == state.mslot);
1919
+
1920
+ side.active.move(slot).id = move;
1921
+ side.active.move(slot).pp = 5;
1922
+ try options.log.start(.{ battle.active(player), .Mimic, move });
1885
1923
  }
1886
1924
 
1887
1925
  pub fn mirrorMove(
@@ -1898,11 +1936,11 @@ pub const Effects = struct {
1898
1936
 
1899
1937
  const last = foe.lastMove(false);
1900
1938
 
1901
- if (last == .None) return options.log.fail(battle.active(player), .None);
1939
+ if (last == .None) return options.log.fail(.{ battle.active(player), .None });
1902
1940
  const knows = knows: for (side.active.moves) |m| {
1903
1941
  if (last == m.id) break :knows true;
1904
1942
  } else false;
1905
- if (knows) return options.log.fail(battle.active(player), .None);
1943
+ if (knows) return options.log.fail(.{ battle.active(player), .None });
1906
1944
 
1907
1945
  // if (wPlayerCharging) ???
1908
1946
  // wPlayerCharging = 1
@@ -1916,17 +1954,17 @@ pub const Effects = struct {
1916
1954
  pub fn mist(battle: anytype, player: Player, _: *State, options: anytype) !void {
1917
1955
  var side = battle.side(player);
1918
1956
 
1919
- if (side.active.volatiles.Mist) return options.log.fail(battle.active(player), .None);
1957
+ if (side.active.volatiles.Mist) return options.log.fail(.{ battle.active(player), .None });
1920
1958
  side.active.volatiles.Mist = true;
1921
1959
 
1922
- try options.log.start(battle.active(player), .Mist);
1960
+ try options.log.start(.{ battle.active(player), .Mist });
1923
1961
  }
1924
1962
 
1925
1963
  pub fn weatherHeal(battle: anytype, player: Player, _: *State, options: anytype) !void {
1926
1964
  var stored = battle.side(player).stored();
1927
1965
  const ident = battle.active(player);
1928
1966
 
1929
- if (stored.hp == stored.stats.hp) return options.log.fail(ident, .None);
1967
+ if (stored.hp == stored.stats.hp) return options.log.fail(.{ ident, .None });
1930
1968
 
1931
1969
  stored.hp = switch (battle.field.weather) {
1932
1970
  .Sun => stored.stats.hp,
@@ -1934,7 +1972,7 @@ pub const Effects = struct {
1934
1972
  else => @min(stored.stats.hp, stored.hp + (stored.stats.hp / 4)),
1935
1973
  };
1936
1974
 
1937
- try options.log.heal(ident, stored, Heal.None);
1975
+ try options.log.heal(.{ ident, stored, .None });
1938
1976
  }
1939
1977
 
1940
1978
  pub fn multiHit(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1947,17 +1985,17 @@ pub const Effects = struct {
1947
1985
  const foe_ident = battle.active(player.foe());
1948
1986
 
1949
1987
  if (foe_volatiles.Flying or foe_volatiles.Underground) {
1950
- if (!showdown) return options.log.fail(battle.active(player.foe()), .None);
1951
- try options.log.lastmiss();
1952
- return options.log.miss(battle.active(player));
1988
+ if (!showdown) return options.log.fail(.{ battle.active(player.foe()), .None });
1989
+ try options.log.lastmiss(.{});
1990
+ return options.log.miss(.{battle.active(player)});
1953
1991
  } else if (foe_volatiles.Substitute) {
1954
- return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
1992
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
1955
1993
  } else if (!Status.is(foe.stored().status, .SLP) or foe_volatiles.Nightmare) {
1956
- return options.log.fail(foe_ident, .None);
1994
+ return options.log.fail(.{ foe_ident, .None });
1957
1995
  }
1958
1996
 
1959
1997
  foe_volatiles.Nightmare = true;
1960
- try options.log.start(battle.active(player.foe()), .Nightmare);
1998
+ try options.log.start(.{ battle.active(player.foe()), .Nightmare });
1961
1999
  }
1962
2000
 
1963
2001
  pub const ohko = struct {
@@ -1981,9 +2019,9 @@ pub const Effects = struct {
1981
2019
  const foe_ident = battle.active(player.foe());
1982
2020
 
1983
2021
  state.damage = 0;
1984
- if (state.immune()) return options.log.immune(foe_ident, .None);
2022
+ if (state.immune()) return options.log.immune(.{ foe_ident, .None });
1985
2023
  const delta = side.stored().level -| foe.stored().level;
1986
- if (delta == 0) return options.log.immune(foe_ident, .OHKO);
2024
+ if (delta == 0) return options.log.immune(.{ foe_ident, .OHKO });
1987
2025
 
1988
2026
  state.accuracy = 2 * delta +| Gen12.percent(30);
1989
2027
  try checkHit(battle, player, state, options);
@@ -1999,37 +2037,39 @@ pub const Effects = struct {
1999
2037
  const ident = battle.active(player);
2000
2038
  const foe_ident = battle.active(player.foe());
2001
2039
 
2002
- if (state.miss) return options.log.fail(foe_ident, .None);
2040
+ if (state.miss) return options.log.fail(.{ foe_ident, .None });
2003
2041
  if (foe.active.volatiles.Substitute) {
2004
- return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
2042
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
2005
2043
  }
2006
2044
 
2007
2045
  const avg = @max((foe.stored().hp + side.stored().hp) / 2, 1);
2008
2046
  foe.stored().hp -= avg;
2009
2047
  side.stored().hp = avg;
2010
2048
 
2011
- try options.log.sethp(foe_ident, foe.stored(), .Silent);
2012
- try options.log.sethp(ident, side.stored(), .None);
2049
+ try options.log.sethp(.{ foe_ident, foe.stored(), .Silent });
2050
+ try options.log.sethp(.{ ident, side.stored(), .None });
2013
2051
  }
2014
2052
 
2015
2053
  pub fn paralyze(battle: anytype, player: Player, state: *State, options: anytype) !void {
2016
2054
  var foe = battle.foe(player);
2017
2055
  const foe_ident = battle.active(player.foe());
2018
2056
 
2019
- if (Status.is(foe.stored().status, .PAR)) return options.log.fail(foe_ident, .Paralysis);
2020
- if (state.immune()) return options.log.immune(foe_ident, .None);
2021
- if (Status.any(foe.stored().status)) return options.log.fail(foe_ident, .None);
2057
+ if (Status.is(foe.stored().status, .PAR)) {
2058
+ return options.log.fail(.{ foe_ident, .Paralysis });
2059
+ }
2060
+ if (state.immune()) return options.log.immune(.{ foe_ident, .None });
2061
+ if (Status.any(foe.stored().status)) return options.log.fail(.{ foe_ident, .None });
2022
2062
  if (state.miss) {
2023
- try options.log.lastmiss();
2024
- return options.log.miss(battle.active(player));
2063
+ try options.log.lastmiss(.{});
2064
+ return options.log.miss(.{battle.active(player)});
2025
2065
  } else if (foe.active.volatiles.Substitute) {
2026
- return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
2066
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
2027
2067
  }
2028
2068
 
2029
2069
  foe.stored().status = Status.init(.PAR);
2030
2070
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2031
2071
 
2032
- try options.log.status(foe_ident, foe.stored().status, .None);
2072
+ try options.log.status(.{ foe_ident, foe.stored().status, .None });
2033
2073
  }
2034
2074
 
2035
2075
  pub fn paralyzeChance(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -2044,14 +2084,15 @@ pub const Effects = struct {
2044
2084
 
2045
2085
  foe_stored.status = Status.init(.PAR);
2046
2086
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2047
- try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
2087
+
2088
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
2048
2089
 
2049
2090
  // TODO: check for status berry
2050
2091
  }
2051
2092
 
2052
2093
  pub fn payDay(battle: anytype, player: Player, _: *State, options: anytype) !void {
2053
2094
  if (!showdown or !battle.foe(player).active.volatiles.Substitute) {
2054
- try options.log.fieldactivate(); // FIXME .PayDay
2095
+ try options.log.fieldactivate(.{}); // FIXME .PayDay
2055
2096
  }
2056
2097
  }
2057
2098
 
@@ -2060,17 +2101,17 @@ pub const Effects = struct {
2060
2101
  var foe = battle.foe(player);
2061
2102
 
2062
2103
  if (foe.active.volatiles.PerishSong) {
2063
- return options.log.fail(battle.active(player.foe()), .None);
2104
+ return options.log.fail(.{ battle.active(player.foe()), .None });
2064
2105
  } else if (!side.active.volatiles.PerishSong) {
2065
2106
  side.active.volatiles.PerishSong = true;
2066
2107
  side.active.volatiles.perish_song = 4;
2067
- try options.log.start(battle.active(player), .PerishSong3Silent);
2108
+ try options.log.start(.{ battle.active(player), .PerishSong3Silent });
2068
2109
  }
2069
2110
  foe.active.volatiles.PerishSong = true;
2070
2111
  foe.active.volatiles.perish_song = 4;
2071
- try options.log.start(battle.active(player.foe()), .PerishSong3Silent);
2112
+ try options.log.start(.{ battle.active(player.foe()), .PerishSong3Silent });
2072
2113
 
2073
- try options.log.fieldactivate(); // FIXME .PerishSong
2114
+ try options.log.fieldactivate(.{}); // FIXME .PerishSong
2074
2115
  }
2075
2116
 
2076
2117
  pub fn poison(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -2080,24 +2121,24 @@ pub const Effects = struct {
2080
2121
 
2081
2122
  const toxic = state.move == .Toxic;
2082
2123
  if (state.immune() or foe.active.types.includes(.Poison)) {
2083
- return options.log.immune(foe_ident, .None);
2124
+ return options.log.immune(.{ foe_ident, .None });
2084
2125
  } else if (Status.any(foe_stored.status)) {
2085
2126
  // Pokémon Showdown considers Toxic to be a status even in Generation II and so
2086
2127
  // will not include a fail reason for Toxic vs. Poison or vice-versa...
2087
- return options.log.fail(foe_ident, if (Status.is(foe_stored.status, .PSN))
2128
+ return options.log.fail(.{ foe_ident, if (Status.is(foe_stored.status, .PSN))
2088
2129
  if (!showdown)
2089
- .Poison
2130
+ Fail.Poison
2090
2131
  else if (toxic == (foe_stored.status == Status.TOX))
2091
- if (toxic) .Toxic else .Poison
2132
+ if (toxic) Fail.Toxic else Fail.Poison
2092
2133
  else
2093
- .None
2134
+ Fail.None
2094
2135
  else
2095
- .None);
2136
+ Fail.None });
2096
2137
  } else if (foe.active.volatiles.Substitute) {
2097
- return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
2138
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
2098
2139
  } else if (state.miss) {
2099
- try options.log.lastmiss();
2100
- return options.log.miss(battle.active(player));
2140
+ try options.log.lastmiss(.{});
2141
+ return options.log.miss(.{battle.active(player)});
2101
2142
  }
2102
2143
 
2103
2144
  foe_stored.status = Status.init(.PSN);
@@ -2107,7 +2148,7 @@ pub const Effects = struct {
2107
2148
  foe.active.volatiles.toxic = 0;
2108
2149
  }
2109
2150
 
2110
- try options.log.status(foe_ident, foe_stored.status, .None);
2151
+ try options.log.status(.{ foe_ident, foe_stored.status, .None });
2111
2152
 
2112
2153
  // TODO: check for status berry
2113
2154
  }
@@ -2123,7 +2164,8 @@ pub const Effects = struct {
2123
2164
  if (foe.conditions.Safeguard) return;
2124
2165
 
2125
2166
  foe_stored.status = Status.init(.PSN);
2126
- try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
2167
+
2168
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
2127
2169
 
2128
2170
  // TODO: check for status berry
2129
2171
  }
@@ -2138,9 +2180,7 @@ pub const Effects = struct {
2138
2180
 
2139
2181
  const failed = !state.first or volatiles.Substitute or
2140
2182
  !try Rolls.protect(battle, player, volatiles.protect, options);
2141
- if (failed) {
2142
- return options.log.fail(battle.active(player), .None);
2143
- }
2183
+ if (failed) return options.log.fail(.{ battle.active(player), .None });
2144
2184
 
2145
2185
  volatiles.protect = @max(8, volatiles.protect + 1);
2146
2186
  if (state.move == .Endure) {
@@ -2148,7 +2188,7 @@ pub const Effects = struct {
2148
2188
  } else {
2149
2189
  volatiles.Protect = true;
2150
2190
  }
2151
- try options.log.singleturn(battle.active(player), state.move);
2191
+ try options.log.singleturn(.{ battle.active(player), state.move });
2152
2192
  }
2153
2193
 
2154
2194
  pub fn psychUp(battle: anytype, player: Player, _: *State, options: anytype) !void {
@@ -2157,11 +2197,11 @@ pub const Effects = struct {
2157
2197
 
2158
2198
  // Pokémon Showdown should fail when the enemy has no boosts/unboosts but doesn't
2159
2199
  if (!showdown and @as(u32, @bitCast(foe.active.boosts)) == 0) {
2160
- return options.log.fail(battle.active(player), .None);
2200
+ return options.log.fail(.{ battle.active(player), .None });
2161
2201
  }
2162
2202
 
2163
2203
  side.active.boosts = foe.active.boosts;
2164
- try options.log.copyboost(battle.active(player), battle.active(player.foe()));
2204
+ try options.log.copyboost(.{ battle.active(player), battle.active(player.foe()) });
2165
2205
  }
2166
2206
 
2167
2207
  pub fn pursuit(battle: anytype, player: Player, state: *State, _: anytype) !void {
@@ -2172,15 +2212,14 @@ pub const Effects = struct {
2172
2212
  pub const rage = struct {
2173
2213
  pub fn start(battle: anytype, player: Player, _: *State, options: anytype) !void {
2174
2214
  battle.side(player).active.volatiles.Rage = true;
2175
- try options.log.singlemove(battle.active(player), Move.Rage);
2215
+ try options.log.singlemove(.{ battle.active(player), Move.Rage });
2176
2216
  }
2177
2217
 
2178
2218
  pub fn damage(battle: anytype, player: Player, state: *State, _: anytype) !void {
2179
- var side = battle.side(player);
2180
- var volatiles = &side.active.volatiles;
2219
+ const side = battle.side(player);
2181
2220
 
2182
- assert(volatiles.Rage);
2183
- state.damage *|= (volatiles.rage +| 1);
2221
+ assert(side.active.volatiles.Rage);
2222
+ state.damage *|= (side.active.volatiles.rage +| 1);
2184
2223
  }
2185
2224
  };
2186
2225
 
@@ -2192,18 +2231,18 @@ pub const Effects = struct {
2192
2231
  var side = battle.side(player);
2193
2232
 
2194
2233
  if (side.active.volatiles.LeechSeed) {
2195
- try options.log.end(battle.active(player), .LeechSeedFrom);
2234
+ try options.log.end(.{ battle.active(player), .LeechSeedFrom });
2196
2235
  side.active.volatiles.LeechSeed = false;
2197
2236
  }
2198
2237
 
2199
2238
  if (side.conditions.Spikes) {
2200
- try options.log.spikesend(battle.active(player));
2239
+ try options.log.sideend(.{ player, protocol.Side.Spikes, battle.active(player) });
2201
2240
  side.conditions.Spikes = false;
2202
2241
  }
2203
2242
 
2204
2243
  if (side.active.volatiles.bind.duration > 0) {
2205
2244
  const reason = @intFromEnum(End.Bind) + side.active.volatiles.bind.reason - 1;
2206
- try options.log.end(battle.active(player), @enumFromInt(reason));
2245
+ try options.log.end(.{ battle.active(player), @as(End, @enumFromInt(reason)) });
2207
2246
  side.active.volatiles.bind = .{};
2208
2247
  }
2209
2248
  }
@@ -2211,18 +2250,14 @@ pub const Effects = struct {
2211
2250
  pub fn recoil(battle: anytype, player: Player, state: *State, options: anytype) !void {
2212
2251
  var side = battle.side(player);
2213
2252
  var stored = side.stored();
2253
+ const ident = battle.active(player);
2214
2254
 
2215
2255
  assert(state.damage > 0);
2216
2256
 
2217
2257
  const damage: i16 = @intCast(@max(state.damage / 4, 1));
2218
2258
  stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
2219
2259
 
2220
- try options.log.damageOf(
2221
- battle.active(player),
2222
- stored,
2223
- .RecoilOf,
2224
- battle.active(player.foe()),
2225
- );
2260
+ try options.log.damage(.{ ident, stored, .RecoilOf, battle.active(player.foe()) });
2226
2261
  }
2227
2262
 
2228
2263
  pub fn reversal(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -2285,7 +2320,7 @@ pub const Effects = struct {
2285
2320
 
2286
2321
  pub fn sandstorm(battle: anytype, player: Player, _: *State, options: anytype) !void {
2287
2322
  if (battle.field.weather != .Sandstorm) return weather(battle, .Sandstorm, options);
2288
- return options.log.fail(battle.active(player), .None);
2323
+ return options.log.fail(.{ battle.active(player), .None });
2289
2324
  }
2290
2325
 
2291
2326
  pub fn safeguard(battle: anytype, player: Player, _: *State, options: anytype) !void {
@@ -2309,11 +2344,13 @@ pub const Effects = struct {
2309
2344
  ) !void {
2310
2345
  var side = battle.side(player);
2311
2346
 
2312
- if (@field(side.conditions, key)) return options.log.fail(battle.active(player), .None);
2347
+ if (@field(side.conditions, key)) {
2348
+ return options.log.fail(.{ battle.active(player), .None });
2349
+ }
2313
2350
 
2314
2351
  @field(side.conditions, key) = true;
2315
2352
  @field(side.conditions, value) = 5;
2316
- try options.log.sidestart(player, @field(protocol.Side, key));
2353
+ try options.log.sidestart(.{ player, @field(protocol.Side, key) });
2317
2354
  }
2318
2355
 
2319
2356
  pub fn sketch(battle: anytype, player: Player, _: *State, options: anytype) !void {
@@ -2324,9 +2361,9 @@ pub const Effects = struct {
2324
2361
 
2325
2362
  if (showdown) {
2326
2363
  // yes, actually "Splash". '|nothing' gets blindly mapped to '|-activate||move:Splash'
2327
- try options.log.activate(battle.active(player), .Splash);
2364
+ try options.log.activate(.{ battle.active(player), .Splash });
2328
2365
  } else {
2329
- try options.log.fail(battle.active(player), .None);
2366
+ try options.log.fail(.{ battle.active(player), .None });
2330
2367
  }
2331
2368
  }
2332
2369
 
@@ -2336,14 +2373,14 @@ pub const Effects = struct {
2336
2373
  const foe_ident = battle.active(player.foe());
2337
2374
 
2338
2375
  if (Status.any(foe_stored.status)) {
2339
- return options.log.fail(
2376
+ return options.log.fail(.{
2340
2377
  foe_ident,
2341
- if (Status.is(foe_stored.status, .SLP)) .Sleep else .None,
2342
- );
2378
+ if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
2379
+ });
2343
2380
  }
2344
2381
 
2345
2382
  if (foe.active.volatiles.Substitute) {
2346
- return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
2383
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
2347
2384
  }
2348
2385
 
2349
2386
  // Sleep Clause Mod
@@ -2354,22 +2391,21 @@ pub const Effects = struct {
2354
2391
  }
2355
2392
 
2356
2393
  foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2357
- try options.log.statusFrom(foe_ident, foe_stored.status, state.move);
2394
+ try options.log.status(.{ foe_ident, foe_stored.status, .From, state.move });
2358
2395
  }
2359
2396
 
2360
2397
  pub fn sleepTalk(battle: anytype, player: Player, state: *State, options: anytype) !void {
2361
2398
  const side = battle.side(player);
2362
2399
 
2363
2400
  if (!Status.is(side.stored().status, .SLP)) {
2364
- if (!showdown) try options.log.fail(battle.active(player), .None);
2365
- return;
2401
+ return if (!showdown) try options.log.fail(.{ battle.active(player), .None });
2366
2402
  }
2367
2403
 
2368
2404
  var n: u3 = 0;
2369
2405
  for (side.active.moves, 0..) |m, i| {
2370
2406
  if (state.mslot - 1 != i and Move.get(m.id).extra.sleep_talk) n += 1;
2371
2407
  }
2372
- if (n == 0) return options.log.fail(battle.active(player), .None);
2408
+ if (n == 0) return options.log.fail(.{ battle.active(player), .None });
2373
2409
 
2374
2410
  _ = try Rolls.sleepTalk(battle, player, n, state.mslot, options); // TODO
2375
2411
  }
@@ -2378,55 +2414,75 @@ pub const Effects = struct {
2378
2414
  if (Status.is(battle.side(player).stored().status, .SLP)) return true;
2379
2415
  state.damage = 0;
2380
2416
  state.miss = true; // TODO is this necessary?
2381
- if (!showdown) try options.log.fail(battle.active(player), .None);
2417
+ if (!showdown) try options.log.fail(.{ battle.active(player), .None });
2382
2418
  return false;
2383
2419
  }
2384
2420
 
2385
2421
  pub fn spikes(battle: anytype, player: Player, _: *State, options: anytype) !void {
2386
2422
  var foe = battle.foe(player);
2387
- if (foe.conditions.Spikes) return options.log.fail(battle.active(player), .None);
2423
+ if (foe.conditions.Spikes) return options.log.fail(.{ battle.active(player), .None });
2388
2424
 
2389
2425
  foe.conditions.Spikes = true;
2390
- try options.log.sidestart(player.foe(), .Spikes);
2426
+ try options.log.sidestart(.{ player.foe(), protocol.Side.Spikes });
2391
2427
  }
2392
2428
 
2393
2429
  pub fn spite(battle: anytype, player: Player, state: *State, options: anytype) !void {
2394
- _ = .{ battle, player, state, options }; // TODO
2395
- // var foe = battle.foe(player);
2430
+ var foe = battle.foe(player);
2396
2431
 
2397
- // if (state.miss) return options.log.fail(battle.active(player), .None);
2432
+ const last = foe.lastMove(false);
2433
+ if (state.miss or last == .None or last == .Struggle) {
2434
+ return options.log.fail(.{ battle.active(player.foe()), .None });
2435
+ }
2398
2436
 
2399
- // const last = foe.lastMove(false);
2400
- // if (showdown) {
2401
- // const roll = try Rolls.spite(battle, player, options);
2402
- // } else {
2403
- // if (last != .None) {}
2404
- // }
2437
+ var slot: u4 = 0;
2438
+ for (foe.active.moves, 0..) |m, i| {
2439
+ if (m.id == last) slot = @intCast(i + 1);
2440
+ }
2441
+ // If the move was called via Metronome it might not exist
2442
+ if (slot == 0 or foe.active.move(slot).pp == 0) {
2443
+ return options.log.fail(.{ battle.active(player.foe()), .None });
2444
+ }
2445
+
2446
+ // Pokémon Showdown always deducts 4 instead of randomizing
2447
+ const pp = @min(
2448
+ foe.active.move(slot).pp,
2449
+ if (showdown) 4 else try Rolls.spite(battle, player, options),
2450
+ );
2451
+ foe.active.move(slot).pp -= pp;
2452
+ if (!foe.active.volatiles.Transform) {
2453
+ var move_slot = foe.stored().move(slot);
2454
+ assert(move_slot.pp > 0);
2455
+
2456
+ move_slot.pp = @as(u6, @intCast(move_slot.pp)) -% 1;
2457
+ assert(foe.active.move(slot).pp == foe.stored().move(slot).pp);
2458
+ }
2459
+
2460
+ try options.log.activate(.{ battle.active(player.foe()), .Spite, last, pp });
2405
2461
  }
2406
2462
 
2407
2463
  pub fn splash(battle: anytype, player: Player, _: *State, options: anytype) !void {
2408
- try options.log.activate(battle.active(player), .Splash);
2464
+ try options.log.activate(.{ battle.active(player), .Splash });
2409
2465
  }
2410
2466
 
2411
2467
  pub fn substitute(battle: anytype, player: Player, _: *State, options: anytype) !void {
2412
2468
  var side = battle.side(player);
2413
2469
  if (side.active.volatiles.Substitute) {
2414
- return options.log.fail(battle.active(player), .Substitute);
2470
+ return options.log.fail(.{ battle.active(player), .Substitute });
2415
2471
  }
2416
2472
 
2417
2473
  assert(side.stored().stats.hp <= 1023);
2418
2474
  const hp: u8 = @intCast(side.stored().stats.hp / 4);
2419
2475
 
2420
2476
  if (hp == 0 or side.stored().hp <= hp) {
2421
- return options.log.fail(battle.active(player), .Weak);
2477
+ return options.log.fail(.{ battle.active(player), .Weak });
2422
2478
  }
2423
2479
 
2424
2480
  side.stored().hp -= hp;
2425
2481
  side.active.volatiles.substitute = hp;
2426
2482
  side.active.volatiles.Substitute = true;
2427
2483
  side.active.volatiles.bind = .{};
2428
- try options.log.start(battle.active(player), .Substitute);
2429
- try options.log.damage(battle.active(player), side.stored(), .None);
2484
+ try options.log.start(.{ battle.active(player), .Substitute });
2485
+ try options.log.damage(.{ battle.active(player), side.stored(), .None });
2430
2486
  }
2431
2487
 
2432
2488
  pub fn sunnyDay(battle: anytype, _: Player, _: *State, options: anytype) !void {
@@ -2436,14 +2492,14 @@ pub const Effects = struct {
2436
2492
  fn weather(battle: anytype, w: Weather, options: anytype) !void {
2437
2493
  battle.field.weather = w;
2438
2494
  battle.field.weather_duration = 5;
2439
- try options.log.weather(w, .None);
2495
+ try options.log.weather(.{ w, protocol.Weather.None });
2440
2496
  }
2441
2497
 
2442
2498
  pub fn teleport(battle: anytype, player: Player, _: *State, options: anytype) !void {
2443
2499
  if (showdown) return;
2444
2500
 
2445
- try options.log.fail(battle.active(player), .None);
2446
- try options.log.laststill();
2501
+ try options.log.fail(.{ battle.active(player), .None });
2502
+ try options.log.laststill(.{});
2447
2503
  }
2448
2504
 
2449
2505
  pub fn thief(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -2458,11 +2514,11 @@ pub const Effects = struct {
2458
2514
  side.stored().item = foe.stored().item;
2459
2515
  foe.stored().item = .None;
2460
2516
 
2461
- try options.log.item(
2517
+ try options.log.item(.{
2462
2518
  battle.active(player),
2463
2519
  side.stored().item,
2464
2520
  battle.active(player.foe()),
2465
- );
2521
+ });
2466
2522
  }
2467
2523
 
2468
2524
  pub fn thrashing(battle: anytype, player: Player, _: *State, options: anytype) !void {
@@ -2484,11 +2540,11 @@ pub const Effects = struct {
2484
2540
  side.active.volatiles.dirty = false;
2485
2541
 
2486
2542
  if (foe.active.volatiles.Transform) {
2487
- return options.log.fail(battle.active(player), .None);
2543
+ return options.log.fail(.{ battle.active(player), .None });
2488
2544
  } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2489
- if (!showdown) return options.log.fail(battle.active(player), .None);
2490
- try options.log.lastmiss();
2491
- return options.log.miss(battle.active(player));
2545
+ if (!showdown) return options.log.fail(.{ battle.active(player), .None });
2546
+ try options.log.lastmiss(.{});
2547
+ return options.log.miss(.{battle.active(player)});
2492
2548
  }
2493
2549
 
2494
2550
  side.active.volatiles.Transform = true;
@@ -2510,7 +2566,7 @@ pub const Effects = struct {
2510
2566
  side.active.moves[i].pp = if (m.id != .None) if (m.id == .Sketch) 1 else 5 else 0;
2511
2567
  }
2512
2568
 
2513
- try options.log.transform(battle.active(player), foe_ident);
2569
+ try options.log.transform(.{ battle.active(player), foe_ident });
2514
2570
  }
2515
2571
 
2516
2572
  pub fn triAttack(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -2535,17 +2591,17 @@ pub const Effects = struct {
2535
2591
  pub fn allStatUpChance(battle: anytype, player: Player, state: *State, options: anytype) !void {
2536
2592
  var side = battle.side(player);
2537
2593
 
2538
- var stats = &side.active.stats;
2539
- var boosts = &side.active.boosts;
2594
+ const stats = &side.active.stats;
2595
+ const boosts = &side.active.boosts;
2540
2596
 
2541
2597
  if (!state.proc) return;
2542
2598
  inline for (STATS, STAT_UP_REASONS) |s, r| {
2543
2599
  assert(@field(boosts, s) >= -6 and @field(boosts, s) <= 6);
2544
2600
  if (@field(boosts, s) == 6 or @field(stats, s) == MAX_STAT_VALUE) {
2545
- try options.log.boost(battle.active(player), r, 0);
2601
+ try options.log.boost(.{ battle.active(player), r, 0 });
2546
2602
  } else {
2547
2603
  try boostStat(battle, side, stats, boosts, s, 1);
2548
- try options.log.boost(battle.active(player), r, 1);
2604
+ try options.log.boost(.{ battle.active(player), r, 1 });
2549
2605
  }
2550
2606
  }
2551
2607
  }
@@ -2555,51 +2611,61 @@ pub const Effects = struct {
2555
2611
  var side = battle.side(player);
2556
2612
  const ident = battle.active(player);
2557
2613
 
2558
- if (state.miss) return log.fail(ident, .None);
2614
+ if (state.miss) return log.fail(.{ ident, .None });
2559
2615
 
2560
- var stats = &side.active.stats;
2616
+ const stats = &side.active.stats;
2561
2617
  var boosts = &side.active.boosts;
2562
2618
 
2563
2619
  const move = Move.get(state.move);
2564
2620
  switch (move.effect) {
2565
2621
  .AttackUp1, .AttackUp2, .Rage, .Swagger => {
2566
2622
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2567
- if (boosts.atk == 6 or stats.atk == MAX_STAT_VALUE) return log.fail(ident, .None);
2623
+ if (boosts.atk == 6 or stats.atk == MAX_STAT_VALUE) {
2624
+ return log.fail(.{ ident, .None });
2625
+ }
2568
2626
  const n: u2 = if (move.effect == .AttackUp2 or move.effect == .Swagger) 2 else 1;
2569
2627
  try boostStat(battle, side, stats, boosts, "atk", n);
2570
2628
  const reason = if (showdown and move.effect == .Rage) Boost.Rage else Boost.Attack;
2571
- try log.boost(ident, reason, n);
2629
+ try log.boost(.{ ident, reason, n });
2572
2630
  },
2573
2631
  .DefenseUp1, .DefenseUp2, .SkullBash => {
2574
2632
  assert(boosts.def >= -6 and boosts.def <= 6);
2575
- if (boosts.def == 6 or stats.def == MAX_STAT_VALUE) return log.fail(ident, .None);
2633
+ if (boosts.def == 6 or stats.def == MAX_STAT_VALUE) {
2634
+ return log.fail(.{ ident, .None });
2635
+ }
2576
2636
  const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
2577
2637
  try boostStat(battle, side, stats, boosts, "def", n);
2578
- try log.boost(ident, .Defense, n);
2638
+ try log.boost(.{ ident, .Defense, n });
2579
2639
  },
2580
2640
  .SpeedUp2 => {
2581
2641
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2582
- if (boosts.spe == 6 or stats.spe == MAX_STAT_VALUE) return log.fail(ident, .None);
2642
+ if (boosts.spe == 6 or stats.spe == MAX_STAT_VALUE) {
2643
+ return log.fail(.{ ident, .None });
2644
+ }
2583
2645
  try boostStat(battle, side, stats, boosts, "spe", 2);
2584
- try log.boost(ident, .Speed, 2);
2646
+ try log.boost(.{ ident, .Speed, 2 });
2585
2647
  },
2586
2648
  .SpAtkUp1 => {
2587
2649
  assert(boosts.spa >= -6 and boosts.spa <= 6);
2588
- if (boosts.spa == 6 or stats.spa == MAX_STAT_VALUE) return log.fail(ident, .None);
2650
+ if (boosts.spa == 6 or stats.spa == MAX_STAT_VALUE) {
2651
+ return log.fail(.{ ident, .None });
2652
+ }
2589
2653
  try boostStat(battle, side, stats, boosts, "spa", 1);
2590
- try log.boost(ident, .SpecialAttack, 1);
2654
+ try log.boost(.{ ident, .SpecialAttack, 1 });
2591
2655
  },
2592
2656
  .SpDefUp2 => {
2593
2657
  assert(boosts.spd >= -6 and boosts.spd <= 6);
2594
- if (boosts.spd == 6 or stats.spd == MAX_STAT_VALUE) return log.fail(ident, .None);
2658
+ if (boosts.spd == 6 or stats.spd == MAX_STAT_VALUE) {
2659
+ return log.fail(.{ ident, .None });
2660
+ }
2595
2661
  try boostStat(battle, side, stats, boosts, "spd", 2);
2596
- try log.boost(ident, .SpecialDefense, 2);
2662
+ try log.boost(.{ ident, .SpecialDefense, 2 });
2597
2663
  },
2598
2664
  .EvasionUp1 => {
2599
2665
  assert(boosts.evasion >= -6 and boosts.evasion <= 6);
2600
- if (boosts.evasion == 6) return log.fail(ident, .None);
2666
+ if (boosts.evasion == 6) return log.fail(.{ ident, .None });
2601
2667
  boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
2602
- try log.boost(ident, .Evasion, 1);
2668
+ try log.boost(.{ ident, .Evasion, 1 });
2603
2669
  if (state.move == .Minimize) side.active.volatiles.minimized = true;
2604
2670
  },
2605
2671
  else => unreachable,
@@ -2612,79 +2678,83 @@ pub const Effects = struct {
2612
2678
  const foe_ident = battle.active(player.foe());
2613
2679
 
2614
2680
  if (foe.active.volatiles.Mist) {
2615
- try log.activate(foe_ident, .Mist);
2616
- return log.fail(foe_ident, .None);
2681
+ try log.activate(.{ foe_ident, .Mist });
2682
+ return log.fail(.{ foe_ident, .None });
2617
2683
  }
2618
2684
 
2619
2685
  const fail = foe.active.volatiles.Substitute or state.miss or !state.proc;
2620
2686
 
2621
- var stats = &foe.active.stats;
2687
+ const stats = &foe.active.stats;
2622
2688
  var boosts = &foe.active.boosts;
2623
2689
 
2624
2690
  const move = Move.get(state.move);
2691
+ const none: Fail = .None;
2625
2692
  switch (move.effect) {
2626
2693
  .AttackDown1, .AttackDown2, .AttackDownChance => {
2627
2694
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2628
2695
  if (fail or boosts.atk == -6) {
2629
- return if (move.effect != .AttackDownChance) try log.fail(foe_ident, .None);
2696
+ return if (move.effect != .AttackDownChance) try log.fail(.{ foe_ident, none });
2630
2697
  } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2631
- try options.log.lastmiss();
2632
- return options.log.miss(battle.active(player));
2698
+ try options.log.lastmiss(.{});
2699
+ return options.log.miss(.{battle.active(player)});
2633
2700
  } else if (stats.atk == 1) {
2634
- return if (move.effect != .AttackDownChance) try log.fail(foe_ident, .None);
2701
+ return if (move.effect != .AttackDownChance) try log.fail(.{ foe_ident, none });
2635
2702
  }
2636
2703
  const n: u2 = if (move.effect == .AttackDown2) 2 else 1;
2637
2704
  try unboostStat(battle, foe, stats, boosts, "atk", n);
2638
- try log.boost(foe_ident, .Attack, -@as(i8, n));
2705
+ try log.boost(.{ foe_ident, .Attack, -@as(i8, n) });
2639
2706
  },
2640
2707
  .DefenseDown1, .DefenseDown2, .DefenseDownChance => {
2641
2708
  assert(boosts.def >= -6 and boosts.def <= 6);
2642
2709
  if (fail or boosts.def == -6) {
2643
- return if (move.effect != .DefenseDownChance) try log.fail(foe_ident, .None);
2710
+ if (move.effect != .DefenseDownChance) try log.fail(.{ foe_ident, none });
2711
+ return;
2644
2712
  } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2645
- try options.log.lastmiss();
2646
- return options.log.miss(battle.active(player));
2713
+ try options.log.lastmiss(.{});
2714
+ return options.log.miss(.{battle.active(player)});
2647
2715
  } else if (stats.def == 1) {
2648
- return if (move.effect != .DefenseDownChance) try log.fail(foe_ident, .None);
2716
+ if (move.effect != .DefenseDownChance) try log.fail(.{ foe_ident, none });
2717
+ return;
2649
2718
  }
2650
2719
  const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
2651
2720
  try unboostStat(battle, foe, stats, boosts, "def", n);
2652
- try log.boost(foe_ident, .Defense, -@as(i8, n));
2721
+ try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
2653
2722
  },
2654
2723
  .SpeedDown1, .SpeedDown2, .SpeedDownChance => {
2655
2724
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2656
2725
  if (fail or boosts.spe == -6) {
2657
- return if (move.effect != .SpeedDownChance) try log.fail(foe_ident, .None);
2726
+ return if (move.effect != .SpeedDownChance) try log.fail(.{ foe_ident, none });
2658
2727
  } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2659
- try options.log.lastmiss();
2660
- return options.log.miss(battle.active(player));
2728
+ try options.log.lastmiss(.{});
2729
+ return options.log.miss(.{battle.active(player)});
2661
2730
  } else if (stats.spe == 1) {
2662
- return if (move.effect != .SpeedDownChance) try log.fail(foe_ident, .None);
2731
+ return if (move.effect != .SpeedDownChance) try log.fail(.{ foe_ident, none });
2663
2732
  }
2664
2733
  const n: u2 = if (move.effect == .SpeedDown2) 2 else 1;
2665
2734
  try unboostStat(battle, foe, stats, boosts, "spe", n);
2666
- try log.boost(foe_ident, .Speed, -@as(i8, n));
2735
+ try log.boost(.{ foe_ident, .Speed, -@as(i8, n) });
2667
2736
  },
2668
2737
  .SpDefDownChance => {
2669
2738
  assert(boosts.spd >= -6 and boosts.spd <= 6);
2670
2739
  if (fail or boosts.spd == -6) return;
2671
2740
  if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2672
- try options.log.lastmiss();
2673
- return options.log.miss(battle.active(player));
2741
+ try options.log.lastmiss(.{});
2742
+ return options.log.miss(.{battle.active(player)});
2674
2743
  } else if (stats.spd == 1) return;
2675
2744
  try unboostStat(battle, foe, stats, boosts, "spd", 1);
2676
- try log.boost(foe_ident, .SpecialDefense, -1);
2745
+ try log.boost(.{ foe_ident, .SpecialDefense, -1 });
2677
2746
  },
2678
2747
  .AccuracyDown1, .AccuracyDownChance => {
2679
2748
  assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
2680
2749
  if (fail or boosts.accuracy == -6) {
2681
- return if (move.effect != .AccuracyDownChance) try log.fail(foe_ident, .None);
2750
+ if (move.effect != .AccuracyDownChance) try log.fail(.{ foe_ident, none });
2751
+ return;
2682
2752
  } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2683
- try options.log.lastmiss();
2684
- return options.log.miss(battle.active(player));
2753
+ try options.log.lastmiss(.{});
2754
+ return options.log.miss(.{battle.active(player)});
2685
2755
  }
2686
2756
  boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
2687
- try log.boost(foe_ident, .Accuracy, -1);
2757
+ try log.boost(.{ foe_ident, .Accuracy, -1 });
2688
2758
  },
2689
2759
  else => unreachable,
2690
2760
  }
@@ -2698,9 +2768,9 @@ fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
2698
2768
  }
2699
2769
 
2700
2770
  fn recomputeStats(battle: anytype, side: *Side) void {
2701
- var stats = unmodifiedStats(battle, side);
2771
+ const stats = unmodifiedStats(battle, side);
2702
2772
  inline for (STATS) |s| {
2703
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, @field(side.active.boosts, s)) + 6))];
2773
+ const mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, @field(side.active.boosts, s)) + 6))];
2704
2774
  const val = @field(stats, s) * mod[0] / mod[1];
2705
2775
  @field(side.active.stats, s) = @max(1, @min(MAX_STAT_VALUE, val));
2706
2776
  }
@@ -2723,7 +2793,7 @@ fn boostStat(
2723
2793
  n: u2,
2724
2794
  ) !void {
2725
2795
  @field(boosts, s) = @intCast(@min(6, @as(i8, @field(boosts, s)) + n));
2726
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, @field(boosts, s)) + 6))];
2796
+ const mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, @field(boosts, s)) + 6))];
2727
2797
  const stat = @field(unmodifiedStats(battle, side), s);
2728
2798
  @field(stats, s) = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2729
2799
  if (comptime std.mem.eql(u8, s, "atk")) {
@@ -2746,7 +2816,7 @@ fn unboostStat(
2746
2816
  n: u2,
2747
2817
  ) !void {
2748
2818
  @field(boosts, s) = @intCast(@max(-6, @as(i8, @field(boosts, s)) - n));
2749
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, @field(boosts, s)) + 6))];
2819
+ const mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, @field(boosts, s)) + 6))];
2750
2820
  const stat = @field(unmodifiedStats(battle, side), s);
2751
2821
  @field(stats, s) = @max(1, stat * mod[0] / mod[1]);
2752
2822
  if (comptime std.mem.eql(u8, s, "atk")) {
@@ -2774,7 +2844,7 @@ fn convertible(active: ActivePokemon, m: Move) bool {
2774
2844
 
2775
2845
  pub const Rolls = struct {
2776
2846
  fn speedTie(battle: anytype, options: anytype) !bool {
2777
- const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
2847
+ const p1 = if (options.calc.overridden(.P1, 0, .speed_tie)) |player|
2778
2848
  player == .P1
2779
2849
  else if (showdown)
2780
2850
  battle.rng.range(u8, 0, 2) == 0
@@ -2786,7 +2856,7 @@ pub const Rolls = struct {
2786
2856
  }
2787
2857
 
2788
2858
  fn quickClaw(battle: anytype, player: Player, options: anytype) !bool {
2789
- const qkc = if (options.calc.overridden(player, .quick_claw)) |val|
2859
+ const qkc = if (options.calc.overridden(player, 0, .quick_claw)) |val|
2790
2860
  val == .true
2791
2861
  else if (showdown)
2792
2862
  battle.rng.chance(u8, 60, 256)
@@ -2798,7 +2868,7 @@ pub const Rolls = struct {
2798
2868
  }
2799
2869
 
2800
2870
  fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) !bool {
2801
- const ok = if (options.calc.overridden(player, .hit)) |val|
2871
+ const ok = if (options.calc.overridden(player, 0, .hit)) |val|
2802
2872
  val == .true
2803
2873
  else if (showdown)
2804
2874
  battle.rng.chance(u8, accuracy, 256)
@@ -2809,20 +2879,22 @@ pub const Rolls = struct {
2809
2879
  return ok;
2810
2880
  }
2811
2881
 
2812
- fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2813
- const crit = if (options.calc.overridden(player, .critical_hit)) |val|
2882
+ fn criticalHit(battle: anytype, player: Player, i: u3, rate: u8, options: anytype) !bool {
2883
+ _ = .{ player, options };
2884
+ const crit = if (options.calc.overridden(player, i, .critical_hits)) |val|
2814
2885
  val == .true
2815
2886
  else if (showdown)
2816
2887
  battle.rng.chance(u8, rate, 256)
2817
2888
  else
2818
2889
  battle.rng.next() < rate;
2819
2890
 
2820
- try options.chance.criticalHit(player, crit, rate);
2891
+ try options.chance.criticalHit(player, i, crit, rate);
2821
2892
  return crit;
2822
2893
  }
2823
2894
 
2824
- fn damage(battle: anytype, player: Player, options: anytype) !u8 {
2825
- const roll = if (options.calc.overridden(player, .damage)) |val|
2895
+ fn damage(battle: anytype, player: Player, i: u3, options: anytype) !u8 {
2896
+ _ = .{ player, options };
2897
+ const roll = if (options.calc.overridden(player, i, .damages)) |val|
2826
2898
  val
2827
2899
  else roll: {
2828
2900
  if (showdown) break :roll battle.rng.range(u8, 217, 256);
@@ -2833,12 +2905,12 @@ pub const Rolls = struct {
2833
2905
  };
2834
2906
 
2835
2907
  assert(roll >= 217 and roll <= 255);
2836
- try options.chance.damage(player, roll);
2908
+ try options.chance.damage(player, i, roll);
2837
2909
  return roll;
2838
2910
  }
2839
2911
 
2840
2912
  fn confused(battle: anytype, player: Player, options: anytype) !bool {
2841
- const cfz = if (options.calc.overridden(player, .confused)) |val|
2913
+ const cfz = if (options.calc.overridden(player, 0, .confused)) |val|
2842
2914
  val == .true
2843
2915
  else if (showdown)
2844
2916
  !battle.rng.chance(u8, 128, 256)
@@ -2850,7 +2922,7 @@ pub const Rolls = struct {
2850
2922
  }
2851
2923
 
2852
2924
  fn attract(battle: anytype, player: Player, options: anytype) !bool {
2853
- const cant = if (options.calc.overridden(player, .attract)) |val|
2925
+ const cant = if (options.calc.overridden(player, 0, .attract)) |val|
2854
2926
  val == .true
2855
2927
  else if (showdown)
2856
2928
  battle.rng.chance(u8, 1, 2)
@@ -2862,7 +2934,7 @@ pub const Rolls = struct {
2862
2934
  }
2863
2935
 
2864
2936
  fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2865
- const par = if (options.calc.overridden(player, .paralyzed)) |val|
2937
+ const par = if (options.calc.overridden(player, 0, .paralyzed)) |val|
2866
2938
  val == .true
2867
2939
  else if (showdown)
2868
2940
  battle.rng.chance(u8, 63, 256)
@@ -2874,7 +2946,7 @@ pub const Rolls = struct {
2874
2946
  }
2875
2947
 
2876
2948
  fn defrost(battle: anytype, player: Player, options: anytype) !bool {
2877
- const thaw = if (options.calc.overridden(player, .defrost)) |val|
2949
+ const thaw = if (options.calc.overridden(player, 0, .defrost)) |val|
2878
2950
  val == .true
2879
2951
  else if (showdown)
2880
2952
  battle.rng.chance(u8, 25, 256)
@@ -2887,7 +2959,7 @@ pub const Rolls = struct {
2887
2959
 
2888
2960
  fn secondaryChance(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2889
2961
  // GLITCH: 100% secondary chance will still fail 1/256 of the time
2890
- const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2962
+ const proc = if (options.calc.overridden(player, 0, .secondary_chance)) |val|
2891
2963
  val == .true
2892
2964
  else if (showdown)
2893
2965
  battle.rng.chance(u8, rate, 256)
@@ -2899,7 +2971,7 @@ pub const Rolls = struct {
2899
2971
  }
2900
2972
 
2901
2973
  fn item(battle: anytype, player: Player, options: anytype) !bool {
2902
- const proc = if (options.calc.overridden(player, .item)) |val|
2974
+ const proc = if (options.calc.overridden(player, 0, .item)) |val|
2903
2975
  val == .true
2904
2976
  else if (showdown)
2905
2977
  battle.rng.chance(u8, 30, 256)
@@ -2911,7 +2983,7 @@ pub const Rolls = struct {
2911
2983
  }
2912
2984
 
2913
2985
  fn triAttack(battle: anytype, player: Player, options: anytype) !TriAttack {
2914
- const status: TriAttack = if (options.calc.overridden(player, .tri_attack)) |val|
2986
+ const status: TriAttack = if (options.calc.overridden(player, 0, .tri_attack)) |val|
2915
2987
  @enumFromInt(@intFromEnum(val) - 1)
2916
2988
  else if (showdown)
2917
2989
  @enumFromInt(battle.rng.range(u2, 0, 2))
@@ -2927,7 +2999,7 @@ pub const Rolls = struct {
2927
2999
  }
2928
3000
 
2929
3001
  fn present(battle: anytype, player: Player, options: anytype) !u8 {
2930
- const power = if (options.calc.overridden(player, .present)) |val|
3002
+ const power = if (options.calc.overridden(player, 0, .present)) |val|
2931
3003
  (val - 1) * 40
2932
3004
  else if (showdown) power: {
2933
3005
  const r = battle.rng.range(u8, 0, 10);
@@ -2949,7 +3021,7 @@ pub const Rolls = struct {
2949
3021
  }
2950
3022
 
2951
3023
  fn magnitude(battle: anytype, player: Player, options: anytype) !u8 {
2952
- const num: u8 = if (options.calc.overridden(player, .magnitude)) |val|
3024
+ const num: u8 = if (options.calc.overridden(player, 0, .magnitude)) |val|
2953
3025
  val + 3
2954
3026
  else if (showdown) num: {
2955
3027
  const r = battle.rng.range(u8, 0, 100);
@@ -2977,7 +3049,7 @@ pub const Rolls = struct {
2977
3049
  }
2978
3050
 
2979
3051
  fn tripleKick(battle: anytype, player: Player, options: anytype) !u2 {
2980
- const hits = if (options.calc.overridden(player, .triple_kick)) |val|
3052
+ const hits = if (options.calc.overridden(player, 0, .triple_kick)) |val|
2981
3053
  val
2982
3054
  else if (showdown)
2983
3055
  battle.rng.range(u2, 1, 4)
@@ -2993,13 +3065,12 @@ pub const Rolls = struct {
2993
3065
  return hits;
2994
3066
  }
2995
3067
 
2996
- fn spite(battle: anytype, player: Player, options: anytype) !u8 {
2997
- const pp = if (options.calc.overridden(player, .spite)) |val|
3068
+ fn spite(battle: anytype, player: Player, options: anytype) !u3 {
3069
+ assert(!showdown);
3070
+ const pp: u3 = if (options.calc.overridden(player, 0, .spite)) |val|
2998
3071
  val + 1
2999
- else if (showdown)
3000
- battle.rng.range(u8, 2, 6)
3001
3072
  else
3002
- (battle.rng.next() & 3) + 2;
3073
+ @intCast((battle.rng.next() & 3) + 2);
3003
3074
 
3004
3075
  assert(pp >= 2 and pp <= 5);
3005
3076
  try options.chance.spite(player, pp);
@@ -3013,7 +3084,7 @@ pub const Rolls = struct {
3013
3084
  var i: u8 = 0;
3014
3085
  const neutral = @intFromEnum(Effectiveness.Neutral);
3015
3086
  assert(mtype != .@"???");
3016
- const ty: Type = if (options.calc.overridden(player, .conversion_2)) |val|
3087
+ const ty: Type = if (options.calc.overridden(player, 0, .conversion_2)) |val|
3017
3088
  @enumFromInt(@intFromEnum(val) - 1)
3018
3089
  else if (showdown) ty: {
3019
3090
  for (Type.SHOWDOWN) |t| {
@@ -3047,7 +3118,7 @@ pub const Rolls = struct {
3047
3118
  assert(count <= 8);
3048
3119
  const num = if (count == 8) 0 else @as(u8, 255) >> @intCast(count);
3049
3120
 
3050
- const ok = if (options.calc.overridden(player, .protect)) |val|
3121
+ const ok = if (options.calc.overridden(player, 0, .protect)) |val|
3051
3122
  val == .true
3052
3123
  else if (showdown)
3053
3124
  battle.rng.chance(u8, num, 255)
@@ -3069,7 +3140,7 @@ pub const Rolls = struct {
3069
3140
  const moves = active.moves;
3070
3141
 
3071
3142
  // TODO: consider throwing error instead of rerolling?
3072
- const overridden = if (options.calc.overridden(player, .move_slot)) |val|
3143
+ const overridden = if (options.calc.overridden(player, 0, .move_slot)) |val|
3073
3144
  if (convertible(active, moves[val - 1].id)) val else null
3074
3145
  else
3075
3146
  null;
@@ -3106,7 +3177,7 @@ pub const Rolls = struct {
3106
3177
  const moves = active.moves;
3107
3178
 
3108
3179
  // TODO: consider throwing error instead of rerolling?
3109
- const overridden = if (options.calc.overridden(player, .move_slot)) |val|
3180
+ const overridden = if (options.calc.overridden(player, 0, .move_slot)) |val|
3110
3181
  if (val != mslot and Move.get(moves[val - 1].id).extra.sleep_talk) val else null
3111
3182
  else
3112
3183
  null;
@@ -3150,7 +3221,7 @@ pub const Rolls = struct {
3150
3221
  assert(n > 0 and n <= 6);
3151
3222
 
3152
3223
  // TODO: consider throwing error instead of rerolling?
3153
- const overridden = if (options.calc.overridden(player, .force_switch)) |val| val: {
3224
+ const overridden = if (options.calc.overridden(player, 0, .force_switch)) |val| val: {
3154
3225
  for (slots) |slot| if (slot == val) break :val if (foe.get(slot).hp > 0) val else null;
3155
3226
  break :val null;
3156
3227
  } else null;
@@ -3176,7 +3247,7 @@ pub const Rolls = struct {
3176
3247
  const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
3177
3248
 
3178
3249
  fn multiHit(battle: anytype, player: Player, options: anytype) !u3 {
3179
- const n: u3 = if (options.calc.overridden(player, .multi_hit)) |val|
3250
+ const n: u3 = if (options.calc.overridden(player, 0, .multi_hit)) |val|
3180
3251
  @intCast(val)
3181
3252
  else if (showdown)
3182
3253
  DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
@@ -3191,7 +3262,7 @@ pub const Rolls = struct {
3191
3262
  }
3192
3263
 
3193
3264
  fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
3194
- const power = if (options.calc.overridden(player, .psywave)) |val|
3265
+ const power = if (options.calc.overridden(player, 0, .psywave)) |val|
3195
3266
  val
3196
3267
  else if (showdown)
3197
3268
  battle.rng.range(u8, 1, max)
@@ -3217,14 +3288,14 @@ pub const Rolls = struct {
3217
3288
  }
3218
3289
 
3219
3290
  // TODO: consider throwing error instead of rerolling?
3220
- const overridden = if (options.calc.overridden(player, .metronome)) |val| val: {
3291
+ const overridden = if (options.calc.overridden(player, 0, .metronome)) |val| val: {
3221
3292
  for (moves) |m| if (m.id == val) break :val null;
3222
3293
  break :val val;
3223
3294
  } else null;
3224
3295
 
3225
3296
  const move: Move = overridden orelse move: {
3226
3297
  if (showdown) {
3227
- var r = battle.rng.range(u8, 0, @intCast(Move.METRONOME.len - n));
3298
+ var r = battle.rng.range(u8, 0, @as(u8, @intCast(Move.METRONOME.len - n)));
3228
3299
  for (moves) |m| {
3229
3300
  if (m.id == .None) break;
3230
3301
  if (@intFromEnum(m.id) > @intFromEnum(Move.METRONOME[r])) break;
@@ -3249,7 +3320,7 @@ pub const Rolls = struct {
3249
3320
  }
3250
3321
 
3251
3322
  fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
3252
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
3323
+ const duration: u3 = if (options.calc.overridden(player, 0, .duration)) |val|
3253
3324
  @intCast(val)
3254
3325
  else if (showdown)
3255
3326
  battle.rng.range(u3, 1, 7)
@@ -3261,12 +3332,12 @@ pub const Rolls = struct {
3261
3332
  };
3262
3333
 
3263
3334
  assert(duration >= 1 and duration <= 6);
3264
- options.chance.duration(.sleep, player, player.foe(), duration);
3335
+ options.chance.duration(player, duration);
3265
3336
  return duration + 1;
3266
3337
  }
3267
3338
 
3268
3339
  fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
3269
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
3340
+ const duration: u3 = if (options.calc.overridden(player, 0, .duration)) |val|
3270
3341
  @intCast(val)
3271
3342
  else if (showdown)
3272
3343
  // Pokémon Showdown incorrectly uses the same duration for self-confusion
@@ -3275,16 +3346,16 @@ pub const Rolls = struct {
3275
3346
  @intCast((battle.rng.next() & if (self) 1 else 3) + 2);
3276
3347
 
3277
3348
  assert(duration >= 2 and duration <= if (!showdown and self) 3 else 5);
3278
- options.chance.duration(.confusion, player, if (self) player else player.foe(), duration);
3349
+ options.chance.duration(player, duration);
3279
3350
  return duration;
3280
3351
  }
3281
3352
 
3282
3353
  fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
3283
- const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
3354
+ const duration: u3 = if (options.calc.overridden(player, 0, .duration)) |val|
3284
3355
  @intCast(val)
3285
3356
  else if (showdown)
3286
3357
  // Pokémon Showdown incorrectly inherits Generation III's Disable duration
3287
- battle.rng.range(u4, 2, 6)
3358
+ battle.rng.range(u3, 2, 6)
3288
3359
  else duration: {
3289
3360
  while (true) {
3290
3361
  const r = battle.rng.next() & 7;
@@ -3293,12 +3364,12 @@ pub const Rolls = struct {
3293
3364
  };
3294
3365
 
3295
3366
  assert(duration >= 2 and duration <= (if (showdown) 5 else 7));
3296
- options.chance.duration(.disable, player, player.foe(), duration);
3367
+ options.chance.duration(player, duration);
3297
3368
  return duration;
3298
3369
  }
3299
3370
 
3300
3371
  fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
3301
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
3372
+ const duration: u3 = if (options.calc.overridden(player, 0, .duration)) |val|
3302
3373
  @intCast(val)
3303
3374
  else if (showdown)
3304
3375
  battle.rng.range(u3, 2, 4)
@@ -3306,12 +3377,12 @@ pub const Rolls = struct {
3306
3377
  @intCast((battle.rng.next() & 1) + 2); // BUG: thrash is + 1 not +2
3307
3378
 
3308
3379
  assert(duration >= 2 and duration <= 3);
3309
- options.chance.duration(.attacking, player, player, duration);
3380
+ options.chance.duration(player, duration);
3310
3381
  return duration;
3311
3382
  }
3312
3383
 
3313
3384
  fn bindingDuration(battle: anytype, player: Player, options: anytype) u3 {
3314
- const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
3385
+ const duration: u3 = (if (options.calc.overridden(player, 0, .duration)) |val|
3315
3386
  @intCast(val)
3316
3387
  else if (showdown)
3317
3388
  // TODO: PS does range(3, 6) here which after subtracting 1 is actually 2-4 not 2-5!
@@ -3320,13 +3391,13 @@ pub const Rolls = struct {
3320
3391
  @intCast(battle.rng.next() & 3 + 2));
3321
3392
 
3322
3393
  assert(duration >= 2 and duration <= 5);
3323
- options.chance.duration(.binding, player, player.foe(), duration);
3394
+ options.chance.duration(player, duration);
3324
3395
  return duration + 1;
3325
3396
  }
3326
3397
 
3327
3398
  // TODO: consider sharing implementation with bindingDuration
3328
3399
  fn encoreDuration(battle: anytype, player: Player, options: anytype) u3 {
3329
- const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
3400
+ const duration: u3 = (if (options.calc.overridden(player, 0, .duration)) |val|
3330
3401
  @intCast(val)
3331
3402
  else if (showdown)
3332
3403
  battle.rng.range(u3, 2, 6)
@@ -3334,7 +3405,7 @@ pub const Rolls = struct {
3334
3405
  @intCast(battle.rng.next() & 3 + 2));
3335
3406
 
3336
3407
  assert(duration >= 2 and duration <= 5);
3337
- options.chance.duration(.encore, player, player.foe(), duration);
3408
+ options.chance.duration(player, duration);
3338
3409
  return duration + 1;
3339
3410
  }
3340
3411
  };
@@ -3447,7 +3518,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3447
3518
  .Move => {
3448
3519
  const side = battle.side(player);
3449
3520
 
3450
- var active = &side.active;
3521
+ const active = &side.active;
3451
3522
  const stored = side.stored();
3452
3523
 
3453
3524
  // While players are not given any input options on the cartridge in these cases,