@pkmn/engine 0.1.0-dev.30c63015 → 0.1.0-dev.3393edb3

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 +41 -14
  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 -70
  23. package/build/tools/debug.js.map +1 -1
  24. package/build/tools/display/debug.jsx +141 -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 +54 -29
  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 +32 -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 +222 -210
  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 +666 -133
  47. package/src/lib/gen1/chance.zig +375 -266
  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 +89 -538
  51. package/src/lib/gen1/mechanics.zig +461 -372
  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 +1 -3
  56. package/src/lib/gen2/helpers.zig +41 -13
  57. package/src/lib/gen2/mechanics.zig +328 -300
  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,30 +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
- const CureStatus = protocol.CureStatus;
31
28
  const Damage = protocol.Damage;
32
29
  const End = protocol.End;
33
- const EndItem = protocol.EndItem;
34
30
  const Fail = protocol.Fail;
35
31
  const Heal = protocol.Heal;
36
- const Immune = protocol.Immune;
37
- const SetHP = protocol.SetHP;
38
32
  const Start = protocol.Start;
39
33
 
40
34
  const Gen12 = rng.Gen12;
41
35
 
42
- const PointerType = util.PointerType;
43
-
44
36
  const ActivePokemon = data.ActivePokemon;
45
37
  const Boosts = data.Boosts;
46
38
  const Effectiveness = data.Effectiveness;
47
39
  const Item = data.Item;
48
40
  const Move = data.Move;
49
- const MoveSlot = data.MoveSlot;
50
- const Pokemon = data.Pokemon;
51
41
  const Side = data.Side;
52
- const Species = data.Species;
53
42
  const Stats = data.Stats;
54
43
  const Status = data.Status;
55
44
  const TriAttack = data.TriAttack;
@@ -111,6 +100,7 @@ pub const State = struct {
111
100
  type: Type, // BATTLE_VARS_MOVE_TYPE
112
101
  accuracy: u8, // wEnemyMoveStruct + MOVE_ACC
113
102
  move: Move, // wCurPlayerMove
103
+ hit: u3 = 0,
114
104
  from: Move = .None,
115
105
  mslot: u4, // wCurMoveNum
116
106
  first: bool, // wEnemyGoesFirst
@@ -145,11 +135,11 @@ fn start(battle: anytype, options: anytype) !Result {
145
135
  const p1 = battle.side(.P1);
146
136
  const p2 = battle.side(.P2);
147
137
 
148
- var p1_slot = findFirstAlive(p1);
138
+ const p1_slot = findFirstAlive(p1);
149
139
  assert(!showdown or p1_slot == 1);
150
140
  if (p1_slot == 0) return if (findFirstAlive(p2) == 0) Result.Tie else Result.Lose;
151
141
 
152
- var p2_slot = findFirstAlive(p2);
142
+ const p2_slot = findFirstAlive(p2);
153
143
  assert(!showdown or p2_slot == 1);
154
144
  if (p2_slot == 0) return Result.Win;
155
145
 
@@ -261,11 +251,10 @@ fn switchIn(battle: anytype, player: Player, slot: u8, kind: SwitchIn, options:
261
251
  } else {
262
252
  try options.log.switched(.{ battle.active(player), incoming });
263
253
  }
264
- options.chance.switched(player, side.pokemon[0].position, out);
265
254
 
266
255
  if (side.conditions.Spikes and !active.types.includes(.Flying)) {
267
256
  incoming.hp -|= @max(incoming.stats.hp / 8, 1);
268
- try options.log.damage(.{ battle.active(player), incoming, Damage.Spikes });
257
+ try options.log.damage(.{ battle.active(player), incoming, .Spikes });
269
258
  }
270
259
  }
271
260
 
@@ -408,7 +397,9 @@ fn executeMove(
408
397
 
409
398
  assert(choice.type == .Move);
410
399
  var mslot: u4 = @intCast(choice.data);
400
+ _ = &mslot;
411
401
  var move = Move.Pound; // TODO
402
+ _ = &move;
412
403
 
413
404
  if (!try beforeMove(battle, player, move, options)) return null;
414
405
 
@@ -438,31 +429,22 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
438
429
  if (volatiles.Recharging) {
439
430
  volatiles.Recharging = false;
440
431
  resetCant(volatiles);
441
- try log.cant(.{ ident, Cant.Recharge });
432
+ try log.cant(.{ ident, .Recharge });
442
433
  return false;
443
434
  }
444
435
 
445
436
  if (Status.is(stored.status, .SLP)) slp: {
446
437
  const before = stored.status;
447
438
  // Even if the EXT bit is set this will still correctly modify the sleep duration
448
- // if (options.calc.modify(player, .sleep)) |extend| {
449
- // if (!extend) stored.status = 0;
450
- // } else {
451
439
  stored.status -= 1;
452
- // }
453
-
454
- const duration = Status.duration(stored.status);
455
- if (!Status.is(stored.status, .EXT)) {
456
- try options.chance.sleep(player, duration);
457
- }
458
440
 
459
- if (duration == 0) {
460
- try log.curestatus(.{ ident, before, CureStatus.Message });
441
+ if (Status.duration(stored.status) == 0) {
442
+ try log.curestatus(.{ ident, before, .Message });
461
443
  stored.status = 0; // clears EXT if present
462
444
  resetCant(volatiles);
463
445
  volatiles.Nightmare = false;
464
446
  } else {
465
- try log.cant(.{ ident, Cant.Sleep });
447
+ try log.cant(.{ ident, .Sleep });
466
448
  if (move == .Snore or move == .SleepTalk) break :slp;
467
449
  resetCant(volatiles);
468
450
  return false;
@@ -471,7 +453,7 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
471
453
 
472
454
  if (Status.is(stored.status, .FRZ) and !(move == .FlameWheel or move == .SacredFire)) {
473
455
  resetCant(volatiles);
474
- try log.cant(.{ ident, Cant.Freeze });
456
+ try log.cant(.{ ident, .Freeze });
475
457
  return false;
476
458
  }
477
459
 
@@ -481,39 +463,28 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
481
463
  // if (!showdown) volatiles.Flinch = false;
482
464
  volatiles.Flinch = false;
483
465
  resetCant(volatiles);
484
- try log.cant(.{ ident, Cant.Flinch });
466
+ try log.cant(.{ ident, .Flinch });
485
467
  return false;
486
468
  }
487
469
 
488
470
  if (volatiles.disable.duration > 0) {
489
- // if (options.calc.modify(player, .disable)) |extend| {
490
- // if (!extend) volatiles.disable.duration = 0;
491
- // } else {
492
471
  volatiles.disable.duration -= 1;
493
- // }
494
- try options.chance.disable(player, volatiles.disable.duration);
495
472
 
496
473
  if (volatiles.disable.duration == 0) {
497
474
  volatiles.disable.move = 0;
498
- try log.end(.{ ident, End.Disable });
475
+ try log.end(.{ ident, .Disable });
499
476
  }
500
477
  }
501
478
 
502
479
  if (volatiles.Confusion) {
503
480
  assert(volatiles.confusion > 0);
504
-
505
- // if (options.calc.modify(player, .confusion)) |extend| {
506
- // if (!extend) volatiles.confusion = 0;
507
- // } else {
508
481
  volatiles.confusion -= 1;
509
- // }
510
- try options.chance.confusion(player, volatiles.confusion);
511
482
 
512
483
  if (volatiles.confusion == 0) {
513
484
  volatiles.Confusion = false;
514
- try log.end(.{ ident, End.Confusion });
485
+ try log.end(.{ ident, .Confusion });
515
486
  } else {
516
- try log.activate(ident, .Confusion);
487
+ try log.activate(.{ ident, .Confusion });
517
488
 
518
489
  if (try Rolls.confused(battle, player, options)) {
519
490
  volatiles.BeatUp = false;
@@ -528,11 +499,11 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
528
499
  }
529
500
 
530
501
  if (volatiles.Attract) {
531
- try log.activate(ident, .Attract);
502
+ try log.activate(.{ ident, .Attract });
532
503
 
533
504
  if (try Rolls.attract(battle, player, options)) {
534
505
  resetCant(volatiles);
535
- try log.cant(.{ ident, Cant.Attract });
506
+ try log.cant(.{ ident, .Attract });
536
507
  return false;
537
508
  }
538
509
  }
@@ -543,14 +514,14 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
543
514
  if (m != .None and m == move) {
544
515
  side.active.volatiles.Charging = false;
545
516
  resetCant(volatiles);
546
- try options.log.cant(.{ ident, Cant.Disable, move });
517
+ try options.log.cant(.{ ident, .Disable, move });
547
518
  return false;
548
519
  }
549
520
  }
550
521
 
551
522
  if (Status.is(stored.status, .PAR) and try Rolls.paralyzed(battle, player, options)) {
552
523
  resetCant(volatiles);
553
- try log.cant(.{ ident, Cant.Paralysis });
524
+ try log.cant(.{ ident, .Paralysis });
554
525
  return false;
555
526
  }
556
527
 
@@ -570,7 +541,7 @@ fn resetCant(volatiles: *align(8) Volatiles) void {
570
541
 
571
542
  pub fn canMove(battle: anytype, player: Player, state: *State, options: anytype) !bool {
572
543
  var side = battle.side(player);
573
- var volatiles = &side.active.volatiles;
544
+ const volatiles = &side.active.volatiles;
574
545
 
575
546
  // FIXME: pass in target to avoid lookup?
576
547
  const target = if (Move.get(state.move).target == .Self) player else player.foe();
@@ -668,7 +639,7 @@ pub fn checkCriticalHit(battle: anytype, player: Player, state: *State, options:
668
639
  }
669
640
  assert(stage <= 4);
670
641
 
671
- 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);
672
643
  }
673
644
 
674
645
  pub fn calcDamage(
@@ -736,7 +707,7 @@ pub fn adjustDamage(battle: anytype, player: Player, state: *State, _: anytype)
736
707
 
737
708
  pub fn randomizeDamage(battle: anytype, player: Player, state: *State, options: anytype) !void {
738
709
  if (state.damage <= 1) return;
739
- const random = try Rolls.damage(battle, player, options);
710
+ const random = try Rolls.damage(battle, player, state.hit, options);
740
711
  options.calc.base(player.foe(), state.damage);
741
712
  state.damage = @intCast(@as(u32, state.damage) *% random / 255);
742
713
  options.calc.final(player.foe(), state.damage);
@@ -953,26 +924,26 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !bool {
953
924
  const damage = @max(stored.stats.hp / 8, 1);
954
925
  stored.hp -|= damage;
955
926
  // As above, Pokémon Showdown uses damage [of] but its not relevant
956
- try options.log.damage(.{ ident, stored, Damage.LeechSeed });
927
+ try options.log.damage(.{ ident, stored, .LeechSeed });
957
928
 
958
929
  const before = foe_stored.hp;
959
930
  foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
960
931
  // Pokémon Showdown uses the less specific heal here instead of drain... because reasons?
961
- if (foe_stored.hp > before) try options.log.heal(.{ foe_ident, foe_stored, Heal.Silent });
932
+ if (foe_stored.hp > before) try options.log.heal(.{ foe_ident, foe_stored, .Silent });
962
933
  if (stored.hp == 0) return true;
963
934
  }
964
935
 
965
936
  if (volatiles.Nightmare) {
966
937
  stored.hp -= @max(stored.stats.hp / 4, 1);
967
938
  // ibid
968
- try options.log.damage(.{ ident, stored, Damage.Nightmare });
939
+ try options.log.damage(.{ ident, stored, .Nightmare });
969
940
  if (stored.hp == 0) return true;
970
941
  }
971
942
 
972
943
  if (volatiles.Curse) {
973
944
  stored.hp -= @max(stored.stats.hp / 4, 1);
974
945
  // ibid
975
- try options.log.damage(.{ ident, stored, Damage.Curse });
946
+ try options.log.damage(.{ ident, stored, .Curse });
976
947
  if (stored.hp == 0) return true;
977
948
  }
978
949
 
@@ -993,7 +964,7 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
993
964
  future_sight.count -= 1;
994
965
  assert(future_sight.count != 0);
995
966
  if (future_sight.count == 1) {
996
- try options.log.end(.{ battle.active(player), End.FutureSight });
967
+ try options.log.end(.{ battle.active(player), .FutureSight });
997
968
  // TODO doMove
998
969
  }
999
970
  }
@@ -1002,7 +973,6 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1002
973
 
1003
974
  // Weather
1004
975
  if (battle.field.weather != .None) {
1005
- assert(battle.field.weather_duration > 0);
1006
976
  battle.field.weather_duration -= 1;
1007
977
 
1008
978
  if (battle.field.weather_duration == 0) {
@@ -1019,7 +989,7 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1019
989
  try options.log.damage(.{
1020
990
  battle.active(player),
1021
991
  side.stored(),
1022
- Damage.Sandstorm,
992
+ .Sandstorm,
1023
993
  });
1024
994
  }
1025
995
  }
@@ -1032,20 +1002,21 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1032
1002
  inline for (players) |player| {
1033
1003
  var side = battle.side(player);
1034
1004
  var volatiles = &side.active.volatiles;
1005
+ const ident = battle.active(player);
1035
1006
 
1036
1007
  if (volatiles.bind.duration > 0 and !volatiles.Substitute) {
1037
1008
  volatiles.bind.duration -= 1;
1038
1009
  if (volatiles.bind.duration == 0) {
1039
1010
  const reason = @intFromEnum(End.Bind) + volatiles.bind.reason - 1;
1040
- try options.log.end(.{ battle.active(player), @as(End, @enumFromInt(reason)) });
1011
+ try options.log.end(.{ ident, @as(End, @enumFromInt(reason)) });
1041
1012
  } else {
1042
1013
  var reason = @intFromEnum(Activate.Bind) + volatiles.bind.reason - 1;
1043
- try options.log.activate(battle.active(player), @enumFromInt(reason));
1014
+ try options.log.activate(.{ ident, @as(Activate, @enumFromInt(reason)) });
1044
1015
 
1045
1016
  reason = @intFromEnum(Damage.Bind) + volatiles.bind.reason - 1;
1046
1017
  side.stored().hp -|= @max(side.stored().stats.hp / 16, 1);
1047
1018
  try options.log.damage(.{
1048
- battle.active(player),
1019
+ ident,
1049
1020
  side.stored(),
1050
1021
  @as(Damage, @enumFromInt(reason)),
1051
1022
  });
@@ -1060,12 +1031,11 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1060
1031
  var volatiles = &side.active.volatiles;
1061
1032
 
1062
1033
  if (volatiles.PerishSong) {
1063
- assert(volatiles.perish_song > 0);
1064
1034
  volatiles.perish_song -= 1;
1065
1035
 
1066
1036
  assert(volatiles.perish_song < 3);
1067
1037
  const reason = @intFromEnum(Start.PerishSong0) + volatiles.perish_song;
1068
- try options.log.start(battle.active(player), @enumFromInt(reason));
1038
+ try options.log.start(.{ battle.active(player), @as(Start, @enumFromInt(reason)) });
1069
1039
 
1070
1040
  if (volatiles.perish_song == 0) {
1071
1041
  volatiles.PerishSong = false;
@@ -1083,14 +1053,14 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1083
1053
  const before = stored.hp;
1084
1054
  stored.hp = @min(stored.hp + @max(stored.stats.hp / 16, 1), stored.stats.hp);
1085
1055
  if (stored.hp > before) {
1086
- try options.log.heal(.{ battle.active(player), stored, Heal.Leftovers });
1056
+ try options.log.heal(.{ battle.active(player), stored, .Leftovers });
1087
1057
  }
1088
1058
  }
1089
1059
  }
1090
1060
 
1091
1061
  // Mystery Berry
1092
1062
  inline for (players) |player| {
1093
- var stored = battle.side(player).stored();
1063
+ const stored = battle.side(player).stored();
1094
1064
 
1095
1065
  if (stored.item == .MysteryBerry) {
1096
1066
  // TODO stored and active moves, transform, etc
@@ -1105,8 +1075,7 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1105
1075
  !side.active.volatiles.frozen and
1106
1076
  try Rolls.defrost(battle, player, options))
1107
1077
  {
1108
- const reason: CureStatus = .Message;
1109
- try options.log.curestatus(.{ battle.active(player), side.stored().status, reason });
1078
+ try options.log.curestatus(.{ battle.active(player), side.stored().status, .Message });
1110
1079
  side.stored().status = 0;
1111
1080
  }
1112
1081
  }
@@ -1147,7 +1116,7 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1147
1116
  stored.hp < stored.stats.hp;
1148
1117
  if (proc) {
1149
1118
  assert(stored.hp != 0);
1150
- try options.log.enditem(.{ ident, stored.item, EndItem.Eat });
1119
+ try options.log.enditem(.{ ident, stored.item, .Eat });
1151
1120
  stored.item = .None;
1152
1121
 
1153
1122
  const offset = num - @intFromEnum(Item.Berry);
@@ -1164,7 +1133,7 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1164
1133
 
1165
1134
  if (proc) {
1166
1135
  assert(Status.any(stored.status));
1167
- try options.log.enditem(.{ ident, stored.item, EndItem.Eat });
1136
+ try options.log.enditem(.{ ident, stored.item, .Eat });
1168
1137
  stored.item = .None;
1169
1138
 
1170
1139
  // FIXME wtf?
@@ -1172,21 +1141,21 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1172
1141
  // side.active.volatiles.Nightmare = false;
1173
1142
  if (stored.item == .MiracleBerry) {
1174
1143
  side.active.volatiles.Confusion = false;
1175
- try options.log.end(.{ ident, End.Confusion });
1144
+ try options.log.end(.{ ident, .Confusion });
1176
1145
  }
1177
1146
  stored.status = 0;
1178
1147
 
1179
1148
  // FIXME recalc stats
1180
1149
 
1181
- try options.log.curestatus(.{ ident, stored.status, CureStatus.Message });
1150
+ try options.log.curestatus(.{ ident, stored.status, .Message });
1182
1151
  }
1183
1152
  } else if (num == @intFromEnum(Item.BitterBerry)) {
1184
1153
  if (side.active.volatiles.Confusion) {
1185
- try options.log.enditem(.{ ident, stored.item, EndItem.Eat });
1154
+ try options.log.enditem(.{ ident, stored.item, .Eat });
1186
1155
  stored.item = .None;
1187
1156
 
1188
1157
  side.active.volatiles.Confusion = false;
1189
- try options.log.end(.{ ident, End.Confusion });
1158
+ try options.log.end(.{ ident, .Confusion });
1190
1159
  }
1191
1160
  }
1192
1161
  }
@@ -1198,12 +1167,11 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1198
1167
  var volatiles = &side.active.volatiles;
1199
1168
 
1200
1169
  if (volatiles.Encore) {
1201
- assert(volatiles.encore > 0);
1202
1170
  volatiles.encore -= 1;
1203
1171
 
1204
1172
  if (volatiles.encore == 0 or side.active.move(mslot).pp == 0) {
1205
1173
  volatiles.Encore = false;
1206
- try options.log.end(.{ battle.active(player), End.Encore });
1174
+ try options.log.end(.{ battle.active(player), .Encore });
1207
1175
  }
1208
1176
  }
1209
1177
  }
@@ -1220,7 +1188,6 @@ fn updateSideCondition(
1220
1188
  ) !void {
1221
1189
  var side = battle.side(player);
1222
1190
  if (@field(side.conditions, key)) {
1223
- assert(@field(side.conditions, value) > 0);
1224
1191
  @field(side.conditions, value) -= 1;
1225
1192
 
1226
1193
  if (@field(side.conditions, value) == 0) {
@@ -1256,7 +1223,7 @@ pub fn buildRage(battle: anytype, player: Player, state: *State, options: anytyp
1256
1223
  }
1257
1224
  } else {
1258
1225
  foe.active.volatiles.rage +|= 1;
1259
- try options.log.activate(battle.active(player), .Rage);
1226
+ try options.log.activate(.{ battle.active(player), .Rage });
1260
1227
  }
1261
1228
  }
1262
1229
  }
@@ -1279,7 +1246,7 @@ pub fn destinyBond(battle: anytype, player: Player, _: *State, options: anytype)
1279
1246
 
1280
1247
  if (foe.active.volatiles.DestinyBond) {
1281
1248
  battle.side(player).stored().hp = 0;
1282
- try options.log.activate(battle.active(player.foe()), .DestinyBond);
1249
+ try options.log.activate(.{ battle.active(player.foe()), .DestinyBond });
1283
1250
  }
1284
1251
  return true;
1285
1252
  }
@@ -1292,14 +1259,14 @@ pub const Effects = struct {
1292
1259
  const gender: u8 = @intFromEnum(side.stored().dvs.gender);
1293
1260
  const foe_gender: u8 = @intFromEnum(foe.stored().dvs.gender);
1294
1261
  if (gender + foe_gender != 1) {
1295
- return options.log.immune(.{ battle.active(player.foe()), Immune.None });
1262
+ return options.log.immune(.{ battle.active(player.foe()), .None });
1296
1263
  } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
1297
1264
  try options.log.lastmiss(.{});
1298
1265
  return options.log.miss(.{battle.active(player)});
1299
1266
  }
1300
1267
 
1301
1268
  foe.active.volatiles.Attract = true;
1302
- try options.log.start(battle.active(player.foe()), .Attract);
1269
+ try options.log.start(.{ battle.active(player.foe()), .Attract });
1303
1270
  }
1304
1271
 
1305
1272
  pub fn batonPass(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1311,14 +1278,32 @@ pub const Effects = struct {
1311
1278
  }
1312
1279
 
1313
1280
  pub fn bellyDrum(battle: anytype, player: Player, state: *State, options: anytype) !void {
1314
- _ = .{ battle, player, state, options }; // TODO
1315
- // assert(!state.miss);
1316
- // var side = battle.side(player);
1317
- // try boostStat(battle, side, &side.active.stats, &side.active.boosts, "atk", 2);
1318
- // TODO
1319
- // |-boost|p1a: Poliwag|atk|2|[silent]
1320
- // |-fail|p1a: Poliwag
1321
- //
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
+ }
1322
1307
  }
1323
1308
 
1324
1309
  pub const bide = struct {
@@ -1330,18 +1315,11 @@ pub const Effects = struct {
1330
1315
 
1331
1316
  if (!volatiles.Bide) return;
1332
1317
 
1333
- assert(volatiles.attacks > 0);
1334
- // if (options.calc.modify(player, .attacking)) |extend| {
1335
- // if (!extend) volatiles.attacks = 0;
1336
- // } else {
1337
1318
  volatiles.attacks -= 1;
1338
- // }
1339
- try options.chance.attacking(player, volatiles.attacks);
1340
-
1341
- if (volatiles.attacks != 0) return options.log.activate(ident, .Bide);
1319
+ if (volatiles.attacks != 0) return options.log.activate(.{ ident, .Bide });
1342
1320
 
1343
1321
  volatiles.Bide = false;
1344
- try options.log.end(.{ ident, End.Bide });
1322
+ try options.log.end(.{ ident, .Bide });
1345
1323
 
1346
1324
  state.damage = volatiles.bide *% 2;
1347
1325
  volatiles.bide = 0;
@@ -1378,7 +1356,7 @@ pub const Effects = struct {
1378
1356
  try options.log.curestatus(.{
1379
1357
  battle.active(player.foe()),
1380
1358
  foe_stored.status,
1381
- CureStatus.Message,
1359
+ .Message,
1382
1360
  });
1383
1361
  foe_stored.status = 0;
1384
1362
  }
@@ -1395,8 +1373,7 @@ pub const Effects = struct {
1395
1373
  foe_stored.status = Status.init(.BRN);
1396
1374
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
1397
1375
 
1398
- const reason: protocol.Status = .None;
1399
- try options.log.status(.{ battle.active(player.foe()), foe_stored.status, reason });
1376
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
1400
1377
 
1401
1378
  // TODO: check for status berry
1402
1379
  }
@@ -1416,7 +1393,7 @@ pub const Effects = struct {
1416
1393
  for (side.active.moves) |m| {
1417
1394
  if (convertible(side.active, m.id)) n += 1;
1418
1395
  }
1419
- if (n == 0) return options.log.fail(.{ battle.active(player), Fail.None });
1396
+ if (n == 0) return options.log.fail(.{ battle.active(player), .None });
1420
1397
 
1421
1398
  side.active.types.type1 = try Rolls.conversion(battle, player, n, options);
1422
1399
  side.active.types.type2 = side.active.types.type1;
@@ -1426,9 +1403,9 @@ pub const Effects = struct {
1426
1403
  var side = battle.side(player);
1427
1404
 
1428
1405
  const last = battle.foe(player).lastMove(false);
1429
- if (last == .None) return options.log.fail(.{ battle.active(player), Fail.None });
1406
+ if (last == .None) return options.log.fail(.{ battle.active(player), .None });
1430
1407
  const move = Move.get(last);
1431
- if (move.type == .@"???") return options.log.fail(.{ battle.active(player), Fail.None });
1408
+ if (move.type == .@"???") return options.log.fail(.{ battle.active(player), .None });
1432
1409
 
1433
1410
  side.active.types.type1 = try Rolls.conversion2(battle, player, move.type, options);
1434
1411
  side.active.types.type2 = side.active.types.type1;
@@ -1473,38 +1450,38 @@ pub const Effects = struct {
1473
1450
  try options.log.lastmiss(.{});
1474
1451
  return options.log.miss(.{battle.active(player)});
1475
1452
  } else if (foe.active.volatiles.Substitute) {
1476
- return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
1453
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
1477
1454
  } else if (foe.active.volatiles.Curse) {
1478
- return options.log.fail(.{ foe_ident, Fail.None });
1455
+ return options.log.fail(.{ foe_ident, .None });
1479
1456
  }
1480
1457
 
1481
1458
  foe.active.volatiles.Curse = true;
1482
- try options.log.start(foe_ident, .Curse);
1459
+ try options.log.start(.{ foe_ident, .Curse });
1483
1460
 
1484
1461
  side.stored().hp -= @max(side.stored().stats.hp / 2, 1);
1485
- try options.log.damage(.{ ident, side.stored(), Damage.Curse });
1462
+ try options.log.damage(.{ ident, side.stored(), .Curse });
1486
1463
  } else {
1487
- var stats = &side.active.stats;
1488
- var boosts = &side.active.boosts;
1464
+ const stats = &side.active.stats;
1465
+ const boosts = &side.active.boosts;
1489
1466
 
1490
1467
  const max_atk = boosts.atk == 6 or stats.atk == MAX_STAT_VALUE;
1491
1468
  const max_def = boosts.def == 6 or stats.def == MAX_STAT_VALUE;
1492
- if (max_atk and max_def) return options.log.fail(.{ ident, Fail.None });
1469
+ if (max_atk and max_def) return options.log.fail(.{ ident, .None });
1493
1470
 
1494
1471
  if (!(boosts.spe == -6 or stats.spe == 1)) {
1495
1472
  assert(boosts.spe >= -6 and boosts.spe <= 6);
1496
1473
  try unboostStat(battle, side, stats, boosts, "spe", 1);
1497
- try options.log.boost(.{ ident, Boost.Speed, @as(i8, -1) });
1474
+ try options.log.boost(.{ ident, .Speed, @as(i8, -1) });
1498
1475
  }
1499
1476
  if (!max_atk) {
1500
1477
  assert(boosts.atk >= -6 and boosts.atk <= 6);
1501
1478
  try boostStat(battle, side, stats, boosts, "atk", 1);
1502
- try options.log.boost(.{ ident, Boost.Attack, @as(i8, 1) });
1479
+ try options.log.boost(.{ ident, .Attack, @as(i8, 1) });
1503
1480
  }
1504
1481
  if (!max_def) {
1505
1482
  assert(boosts.def >= -6 and boosts.def <= 6);
1506
1483
  try boostStat(battle, side, stats, boosts, "def", 1);
1507
- try options.log.boost(.{ ident, Boost.Defense, @as(i8, 1) });
1484
+ try options.log.boost(.{ ident, .Defense, @as(i8, 1) });
1508
1485
  }
1509
1486
  }
1510
1487
  }
@@ -1516,9 +1493,7 @@ pub const Effects = struct {
1516
1493
  pub fn defrost(battle: anytype, player: Player, _: *State, options: anytype) !void {
1517
1494
  var side = battle.side(player);
1518
1495
  assert(Status.is(side.stored().status, .FRZ));
1519
-
1520
- const reason: CureStatus = .Message;
1521
- try options.log.curestatus(.{ battle.active(player), side.stored().status, reason });
1496
+ try options.log.curestatus(.{ battle.active(player), side.stored().status, .Message });
1522
1497
  side.stored().status = 0;
1523
1498
  }
1524
1499
 
@@ -1532,14 +1507,10 @@ pub const Effects = struct {
1532
1507
  var volatiles = &foe.active.volatiles;
1533
1508
  const foe_ident = battle.active(player.foe());
1534
1509
 
1535
- if (volatiles.disable.move != 0) {
1536
- return options.log.fail(.{ foe_ident, Fail.None });
1537
- }
1510
+ if (volatiles.disable.move != 0) return options.log.fail(.{ foe_ident, .None });
1538
1511
 
1539
1512
  const last = battle.foe(player).lastMove(false);
1540
- if (last == .None or last == .Struggle) {
1541
- return options.log.fail(.{ foe_ident, Fail.None });
1542
- }
1513
+ if (last == .None or last == .Struggle) return options.log.fail(.{ foe_ident, .None });
1543
1514
 
1544
1515
  var slot: u3 = 0;
1545
1516
  for (foe.active.moves) |m| {
@@ -1548,12 +1519,12 @@ pub const Effects = struct {
1548
1519
  if (m.id == last) break;
1549
1520
  }
1550
1521
  const move = foe.active.move(slot);
1551
- if (move.pp == 0) return options.log.fail(.{ foe_ident, Fail.None });
1522
+ if (move.pp == 0) return options.log.fail(.{ foe_ident, .None });
1552
1523
 
1553
1524
  volatiles.disable.move = slot;
1554
1525
  volatiles.disable.duration = Rolls.disableDuration(battle, player, options);
1555
1526
 
1556
- try options.log.startEffect(foe_ident, .Disable, move.id);
1527
+ try options.log.start(.{ foe_ident, .Disable, move.id });
1557
1528
  }
1558
1529
 
1559
1530
  pub const double = struct {
@@ -1579,7 +1550,7 @@ pub const Effects = struct {
1579
1550
  if (stored.hp == stored.stats.hp) return;
1580
1551
  stored.hp = @min(stored.stats.hp, stored.hp + @max(state.damage / 2, 1));
1581
1552
 
1582
- try options.log.heal(.{ ident, stored, Heal.Drain, battle.active(player.foe()) });
1553
+ try options.log.heal(.{ ident, stored, .Drain, battle.active(player.foe()) });
1583
1554
  }
1584
1555
 
1585
1556
  pub fn encore(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1591,7 +1562,7 @@ pub const Effects = struct {
1591
1562
  const failed = volatiles.Encore or
1592
1563
  (last == .None or last == .Struggle or
1593
1564
  last == .Encore or .last == .MirrorMove);
1594
- if (failed) return options.log.fail(.{ foe_ident, Fail.None });
1565
+ if (failed) return options.log.fail(.{ foe_ident, .None });
1595
1566
 
1596
1567
  var slot: u3 = 0;
1597
1568
  for (foe.active.moves) |m| {
@@ -1600,7 +1571,7 @@ pub const Effects = struct {
1600
1571
  if (m.id == last) break;
1601
1572
  }
1602
1573
  const move = foe.active.move(slot);
1603
- if (move.pp == 0) return options.log.fail(.{ foe_ident, Fail.None });
1574
+ if (move.pp == 0) return options.log.fail(.{ foe_ident, .None });
1604
1575
 
1605
1576
  volatiles.Encore = true;
1606
1577
  volatiles.encore = Rolls.encoreDuration(battle, player, options);
@@ -1608,11 +1579,19 @@ pub const Effects = struct {
1608
1579
  // TODO ???
1609
1580
  _ = state;
1610
1581
 
1611
- try options.log.startEffect(foe_ident, .Encore, move.id);
1582
+ try options.log.start(.{ foe_ident, .Encore, move.id });
1612
1583
  }
1613
1584
 
1614
- pub fn explode(battle: anytype, player: Player, state: *State, options: anytype) !void {
1615
- _ = .{ 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;
1616
1595
  }
1617
1596
 
1618
1597
  pub fn falseSwipe(battle: anytype, player: Player, state: *State, _: anytype) !void {
@@ -1662,12 +1641,11 @@ pub const Effects = struct {
1662
1641
  var side = battle.side(player);
1663
1642
 
1664
1643
  if (side.active.volatiles.FocusEnergy) {
1665
- if (!showdown) try options.log.fail(.{ battle.active(player), Fail.None });
1666
- return;
1644
+ return if (!showdown) try options.log.fail(.{ battle.active(player), .None });
1667
1645
  }
1668
1646
 
1669
1647
  side.active.volatiles.FocusEnergy = true;
1670
- try options.log.start(battle.active(player), .FocusEnergy);
1648
+ try options.log.start(.{ battle.active(player), .FocusEnergy });
1671
1649
  }
1672
1650
 
1673
1651
  var SLOTS: [5]u4 = .{ 0, 0, 0, 0, 0 };
@@ -1677,7 +1655,7 @@ pub const Effects = struct {
1677
1655
 
1678
1656
  const foe = battle.foe(player);
1679
1657
 
1680
- if (state.first) return options.log.fail(.{ battle.active(player), Fail.None });
1658
+ if (state.first) return options.log.fail(.{ battle.active(player), .None });
1681
1659
 
1682
1660
  var n: u4 = 0;
1683
1661
  var i: u4 = 0;
@@ -1691,7 +1669,7 @@ pub const Effects = struct {
1691
1669
  }
1692
1670
  }
1693
1671
 
1694
- if (n == 0) return options.log.fail(.{ battle.active(player), Fail.None });
1672
+ if (n == 0) return options.log.fail(.{ battle.active(player), .None });
1695
1673
 
1696
1674
  const slot = try Rolls.forceSwitch(battle, player, SLOTS[0..i], n, options);
1697
1675
  try switchIn(battle, player.foe(), slot, .Drag, options);
@@ -1704,11 +1682,11 @@ pub const Effects = struct {
1704
1682
  try options.log.lastmiss(.{});
1705
1683
  return options.log.miss(.{battle.active(player)});
1706
1684
  } else if (foe.active.volatiles.Foresight) {
1707
- return options.log.fail(.{ battle.active(player), Fail.None });
1685
+ return options.log.fail(.{ battle.active(player), .None });
1708
1686
  }
1709
1687
 
1710
1688
  foe.active.volatiles.Foresight = true;
1711
- try options.log.start(battle.active(player.foe()), .FocusEnergy);
1689
+ try options.log.start(.{ battle.active(player.foe()), .FocusEnergy });
1712
1690
  }
1713
1691
 
1714
1692
  pub fn freezeChance(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1731,7 +1709,7 @@ pub const Effects = struct {
1731
1709
  if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1732
1710
 
1733
1711
  foe_stored.status = Status.init(.FRZ);
1734
- try options.log.status(.{ foe_ident, foe_stored.status, protocol.Status.None });
1712
+ try options.log.status(.{ foe_ident, foe_stored.status, .None });
1735
1713
 
1736
1714
  // TODO: check for status berry
1737
1715
 
@@ -1739,8 +1717,26 @@ pub const Effects = struct {
1739
1717
  foe.active.volatiles.frozen = true;
1740
1718
  }
1741
1719
 
1742
- pub fn furyCutter(battle: anytype, player: Player, state: *State, options: anytype) !void {
1743
- _ = .{ 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
+ }
1744
1740
  }
1745
1741
 
1746
1742
  pub fn futureSight(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1774,7 +1770,7 @@ pub const Effects = struct {
1774
1770
  var stored = side.stored();
1775
1771
  const ident = battle.active(player);
1776
1772
 
1777
- if (stored.stats.hp == stored.hp) return options.log.fail(.{ ident, Fail.None });
1773
+ if (stored.stats.hp == stored.hp) return options.log.fail(.{ ident, .None });
1778
1774
 
1779
1775
  const rest = state.move == .Rest;
1780
1776
  if (rest) {
@@ -1783,7 +1779,7 @@ pub const Effects = struct {
1783
1779
  side.active.volatiles.Toxic = false;
1784
1780
  stored.status = Status.slf(2);
1785
1781
  recomputeStats(battle, side);
1786
- try options.log.status(.{ ident, stored.status, protocol.Status.From, Move.Rest });
1782
+ try options.log.status(.{ ident, stored.status, .From, Move.Rest });
1787
1783
  stored.hp = stored.stats.hp;
1788
1784
  } else {
1789
1785
  stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
@@ -1821,7 +1817,7 @@ pub const Effects = struct {
1821
1817
  // Invulnerability trumps type immunity on Pokémon Showdown
1822
1818
  const invulnerable = foe.active.volatiles.Underground or foe.active.volatiles.Flying;
1823
1819
  if (!invulnerable and foe.active.types.includes(.Grass)) {
1824
- return options.log.immune(.{ battle.active(player.foe()), Immune.None });
1820
+ return options.log.immune(.{ battle.active(player.foe()), .None });
1825
1821
  }
1826
1822
  try checkHit(battle, player, state, options);
1827
1823
  if (state.miss) {
@@ -1841,7 +1837,7 @@ pub const Effects = struct {
1841
1837
 
1842
1838
  foe.active.volatiles.LeechSeed = true;
1843
1839
 
1844
- try options.log.start(battle.active(player.foe()), .LeechSeed);
1840
+ try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
1845
1841
  }
1846
1842
 
1847
1843
  pub fn lockOn(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1849,20 +1845,20 @@ pub const Effects = struct {
1849
1845
  const foe_ident = battle.active(player.foe());
1850
1846
 
1851
1847
  if (foe.active.volatiles.Substitute) {
1852
- return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
1848
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
1853
1849
  } else if (state.miss) {
1854
- return options.log.fail(.{ foe_ident, Fail.None });
1850
+ return options.log.fail(.{ foe_ident, .None });
1855
1851
  }
1856
1852
 
1857
1853
  foe.active.volatiles.LockOn = true;
1858
- try options.log.activate(battle.active(player), .LockOn);
1854
+ try options.log.activate(.{ battle.active(player), .LockOn });
1859
1855
  }
1860
1856
 
1861
1857
  const MAGNITUDE_POWER = [_]u8{ 10, 30, 50, 70, 90, 110, 150 };
1862
1858
 
1863
1859
  pub fn magnitude(battle: anytype, player: Player, state: *State, options: anytype) !void {
1864
1860
  const num = try Rolls.magnitude(battle, player, options);
1865
- try options.log.magnitude(battle.active(player), num);
1861
+ try options.log.activate(.{ battle.active(player), .Magnitude, num });
1866
1862
  state.bp = MAGNITUDE_POWER[num - 4];
1867
1863
  }
1868
1864
 
@@ -1870,11 +1866,11 @@ pub const Effects = struct {
1870
1866
  var foe = battle.foe(player);
1871
1867
 
1872
1868
  if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
1873
- if (!showdown) return options.log.fail(.{ battle.active(player.foe()), Fail.None });
1869
+ if (!showdown) return options.log.fail(.{ battle.active(player.foe()), .None });
1874
1870
  try options.log.lastmiss(.{});
1875
1871
  return options.log.miss(.{battle.active(player)});
1876
1872
  } else if (foe.active.volatiles.Trapped) {
1877
- return options.log.fail(.{ battle.active(player.foe()), Fail.None });
1873
+ return options.log.fail(.{ battle.active(player.foe()), .None });
1878
1874
  }
1879
1875
 
1880
1876
  foe.active.volatiles.Trapped = true;
@@ -1901,7 +1897,29 @@ pub const Effects = struct {
1901
1897
  }
1902
1898
 
1903
1899
  pub fn mimic(battle: anytype, player: Player, state: *State, options: anytype) !void {
1904
- _ = .{ 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 });
1905
1923
  }
1906
1924
 
1907
1925
  pub fn mirrorMove(
@@ -1918,11 +1936,11 @@ pub const Effects = struct {
1918
1936
 
1919
1937
  const last = foe.lastMove(false);
1920
1938
 
1921
- if (last == .None) return options.log.fail(.{ battle.active(player), Fail.None });
1939
+ if (last == .None) return options.log.fail(.{ battle.active(player), .None });
1922
1940
  const knows = knows: for (side.active.moves) |m| {
1923
1941
  if (last == m.id) break :knows true;
1924
1942
  } else false;
1925
- if (knows) return options.log.fail(.{ battle.active(player), Fail.None });
1943
+ if (knows) return options.log.fail(.{ battle.active(player), .None });
1926
1944
 
1927
1945
  // if (wPlayerCharging) ???
1928
1946
  // wPlayerCharging = 1
@@ -1936,19 +1954,17 @@ pub const Effects = struct {
1936
1954
  pub fn mist(battle: anytype, player: Player, _: *State, options: anytype) !void {
1937
1955
  var side = battle.side(player);
1938
1956
 
1939
- if (side.active.volatiles.Mist) {
1940
- return options.log.fail(.{ battle.active(player), Fail.None });
1941
- }
1957
+ if (side.active.volatiles.Mist) return options.log.fail(.{ battle.active(player), .None });
1942
1958
  side.active.volatiles.Mist = true;
1943
1959
 
1944
- try options.log.start(battle.active(player), .Mist);
1960
+ try options.log.start(.{ battle.active(player), .Mist });
1945
1961
  }
1946
1962
 
1947
1963
  pub fn weatherHeal(battle: anytype, player: Player, _: *State, options: anytype) !void {
1948
1964
  var stored = battle.side(player).stored();
1949
1965
  const ident = battle.active(player);
1950
1966
 
1951
- if (stored.hp == stored.stats.hp) return options.log.fail(.{ ident, Fail.None });
1967
+ if (stored.hp == stored.stats.hp) return options.log.fail(.{ ident, .None });
1952
1968
 
1953
1969
  stored.hp = switch (battle.field.weather) {
1954
1970
  .Sun => stored.stats.hp,
@@ -1956,7 +1972,7 @@ pub const Effects = struct {
1956
1972
  else => @min(stored.stats.hp, stored.hp + (stored.stats.hp / 4)),
1957
1973
  };
1958
1974
 
1959
- try options.log.heal(.{ ident, stored, Heal.None });
1975
+ try options.log.heal(.{ ident, stored, .None });
1960
1976
  }
1961
1977
 
1962
1978
  pub fn multiHit(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1969,17 +1985,17 @@ pub const Effects = struct {
1969
1985
  const foe_ident = battle.active(player.foe());
1970
1986
 
1971
1987
  if (foe_volatiles.Flying or foe_volatiles.Underground) {
1972
- if (!showdown) return options.log.fail(.{ battle.active(player.foe()), Fail.None });
1988
+ if (!showdown) return options.log.fail(.{ battle.active(player.foe()), .None });
1973
1989
  try options.log.lastmiss(.{});
1974
1990
  return options.log.miss(.{battle.active(player)});
1975
1991
  } else if (foe_volatiles.Substitute) {
1976
- return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
1992
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
1977
1993
  } else if (!Status.is(foe.stored().status, .SLP) or foe_volatiles.Nightmare) {
1978
- return options.log.fail(.{ foe_ident, Fail.None });
1994
+ return options.log.fail(.{ foe_ident, .None });
1979
1995
  }
1980
1996
 
1981
1997
  foe_volatiles.Nightmare = true;
1982
- try options.log.start(battle.active(player.foe()), .Nightmare);
1998
+ try options.log.start(.{ battle.active(player.foe()), .Nightmare });
1983
1999
  }
1984
2000
 
1985
2001
  pub const ohko = struct {
@@ -2003,9 +2019,9 @@ pub const Effects = struct {
2003
2019
  const foe_ident = battle.active(player.foe());
2004
2020
 
2005
2021
  state.damage = 0;
2006
- if (state.immune()) return options.log.immune(.{ foe_ident, Immune.None });
2022
+ if (state.immune()) return options.log.immune(.{ foe_ident, .None });
2007
2023
  const delta = side.stored().level -| foe.stored().level;
2008
- if (delta == 0) return options.log.immune(.{ foe_ident, Immune.OHKO });
2024
+ if (delta == 0) return options.log.immune(.{ foe_ident, .OHKO });
2009
2025
 
2010
2026
  state.accuracy = 2 * delta +| Gen12.percent(30);
2011
2027
  try checkHit(battle, player, state, options);
@@ -2021,17 +2037,17 @@ pub const Effects = struct {
2021
2037
  const ident = battle.active(player);
2022
2038
  const foe_ident = battle.active(player.foe());
2023
2039
 
2024
- if (state.miss) return options.log.fail(.{ foe_ident, Fail.None });
2040
+ if (state.miss) return options.log.fail(.{ foe_ident, .None });
2025
2041
  if (foe.active.volatiles.Substitute) {
2026
- return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
2042
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
2027
2043
  }
2028
2044
 
2029
2045
  const avg = @max((foe.stored().hp + side.stored().hp) / 2, 1);
2030
2046
  foe.stored().hp -= avg;
2031
2047
  side.stored().hp = avg;
2032
2048
 
2033
- try options.log.sethp(.{ foe_ident, foe.stored(), SetHP.Silent });
2034
- try options.log.sethp(.{ ident, side.stored(), SetHP.None });
2049
+ try options.log.sethp(.{ foe_ident, foe.stored(), .Silent });
2050
+ try options.log.sethp(.{ ident, side.stored(), .None });
2035
2051
  }
2036
2052
 
2037
2053
  pub fn paralyze(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -2039,21 +2055,21 @@ pub const Effects = struct {
2039
2055
  const foe_ident = battle.active(player.foe());
2040
2056
 
2041
2057
  if (Status.is(foe.stored().status, .PAR)) {
2042
- return options.log.fail(.{ foe_ident, Fail.Paralysis });
2058
+ return options.log.fail(.{ foe_ident, .Paralysis });
2043
2059
  }
2044
- if (state.immune()) return options.log.immune(.{ foe_ident, Immune.None });
2045
- if (Status.any(foe.stored().status)) return options.log.fail(.{ foe_ident, Fail.None });
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 });
2046
2062
  if (state.miss) {
2047
2063
  try options.log.lastmiss(.{});
2048
2064
  return options.log.miss(.{battle.active(player)});
2049
2065
  } else if (foe.active.volatiles.Substitute) {
2050
- return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
2066
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
2051
2067
  }
2052
2068
 
2053
2069
  foe.stored().status = Status.init(.PAR);
2054
2070
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2055
2071
 
2056
- try options.log.status(.{ foe_ident, foe.stored().status, protocol.Status.None });
2072
+ try options.log.status(.{ foe_ident, foe.stored().status, .None });
2057
2073
  }
2058
2074
 
2059
2075
  pub fn paralyzeChance(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -2069,8 +2085,7 @@ pub const Effects = struct {
2069
2085
  foe_stored.status = Status.init(.PAR);
2070
2086
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2071
2087
 
2072
- const reason: protocol.Status = .None;
2073
- try options.log.status(.{ battle.active(player.foe()), foe_stored.status, reason });
2088
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
2074
2089
 
2075
2090
  // TODO: check for status berry
2076
2091
  }
@@ -2086,15 +2101,15 @@ pub const Effects = struct {
2086
2101
  var foe = battle.foe(player);
2087
2102
 
2088
2103
  if (foe.active.volatiles.PerishSong) {
2089
- return options.log.fail(.{ battle.active(player.foe()), Fail.None });
2104
+ return options.log.fail(.{ battle.active(player.foe()), .None });
2090
2105
  } else if (!side.active.volatiles.PerishSong) {
2091
2106
  side.active.volatiles.PerishSong = true;
2092
2107
  side.active.volatiles.perish_song = 4;
2093
- try options.log.start(battle.active(player), .PerishSong3Silent);
2108
+ try options.log.start(.{ battle.active(player), .PerishSong3Silent });
2094
2109
  }
2095
2110
  foe.active.volatiles.PerishSong = true;
2096
2111
  foe.active.volatiles.perish_song = 4;
2097
- try options.log.start(battle.active(player.foe()), .PerishSong3Silent);
2112
+ try options.log.start(.{ battle.active(player.foe()), .PerishSong3Silent });
2098
2113
 
2099
2114
  try options.log.fieldactivate(.{}); // FIXME .PerishSong
2100
2115
  }
@@ -2106,7 +2121,7 @@ pub const Effects = struct {
2106
2121
 
2107
2122
  const toxic = state.move == .Toxic;
2108
2123
  if (state.immune() or foe.active.types.includes(.Poison)) {
2109
- return options.log.immune(.{ foe_ident, Immune.None });
2124
+ return options.log.immune(.{ foe_ident, .None });
2110
2125
  } else if (Status.any(foe_stored.status)) {
2111
2126
  // Pokémon Showdown considers Toxic to be a status even in Generation II and so
2112
2127
  // will not include a fail reason for Toxic vs. Poison or vice-versa...
@@ -2120,7 +2135,7 @@ pub const Effects = struct {
2120
2135
  else
2121
2136
  Fail.None });
2122
2137
  } else if (foe.active.volatiles.Substitute) {
2123
- return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
2138
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
2124
2139
  } else if (state.miss) {
2125
2140
  try options.log.lastmiss(.{});
2126
2141
  return options.log.miss(.{battle.active(player)});
@@ -2133,7 +2148,7 @@ pub const Effects = struct {
2133
2148
  foe.active.volatiles.toxic = 0;
2134
2149
  }
2135
2150
 
2136
- try options.log.status(.{ foe_ident, foe_stored.status, protocol.Status.None });
2151
+ try options.log.status(.{ foe_ident, foe_stored.status, .None });
2137
2152
 
2138
2153
  // TODO: check for status berry
2139
2154
  }
@@ -2149,8 +2164,8 @@ pub const Effects = struct {
2149
2164
  if (foe.conditions.Safeguard) return;
2150
2165
 
2151
2166
  foe_stored.status = Status.init(.PSN);
2152
- const reason: protocol.Status = .None;
2153
- try options.log.status(.{ battle.active(player.foe()), foe_stored.status, reason });
2167
+
2168
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
2154
2169
 
2155
2170
  // TODO: check for status berry
2156
2171
  }
@@ -2165,9 +2180,7 @@ pub const Effects = struct {
2165
2180
 
2166
2181
  const failed = !state.first or volatiles.Substitute or
2167
2182
  !try Rolls.protect(battle, player, volatiles.protect, options);
2168
- if (failed) {
2169
- return options.log.fail(.{ battle.active(player), Fail.None });
2170
- }
2183
+ if (failed) return options.log.fail(.{ battle.active(player), .None });
2171
2184
 
2172
2185
  volatiles.protect = @max(8, volatiles.protect + 1);
2173
2186
  if (state.move == .Endure) {
@@ -2184,7 +2197,7 @@ pub const Effects = struct {
2184
2197
 
2185
2198
  // Pokémon Showdown should fail when the enemy has no boosts/unboosts but doesn't
2186
2199
  if (!showdown and @as(u32, @bitCast(foe.active.boosts)) == 0) {
2187
- return options.log.fail(.{ battle.active(player), Fail.None });
2200
+ return options.log.fail(.{ battle.active(player), .None });
2188
2201
  }
2189
2202
 
2190
2203
  side.active.boosts = foe.active.boosts;
@@ -2203,11 +2216,10 @@ pub const Effects = struct {
2203
2216
  }
2204
2217
 
2205
2218
  pub fn damage(battle: anytype, player: Player, state: *State, _: anytype) !void {
2206
- var side = battle.side(player);
2207
- var volatiles = &side.active.volatiles;
2219
+ const side = battle.side(player);
2208
2220
 
2209
- assert(volatiles.Rage);
2210
- state.damage *|= (volatiles.rage +| 1);
2221
+ assert(side.active.volatiles.Rage);
2222
+ state.damage *|= (side.active.volatiles.rage +| 1);
2211
2223
  }
2212
2224
  };
2213
2225
 
@@ -2219,7 +2231,7 @@ pub const Effects = struct {
2219
2231
  var side = battle.side(player);
2220
2232
 
2221
2233
  if (side.active.volatiles.LeechSeed) {
2222
- try options.log.end(.{ battle.active(player), End.LeechSeedFrom });
2234
+ try options.log.end(.{ battle.active(player), .LeechSeedFrom });
2223
2235
  side.active.volatiles.LeechSeed = false;
2224
2236
  }
2225
2237
 
@@ -2238,18 +2250,14 @@ pub const Effects = struct {
2238
2250
  pub fn recoil(battle: anytype, player: Player, state: *State, options: anytype) !void {
2239
2251
  var side = battle.side(player);
2240
2252
  var stored = side.stored();
2253
+ const ident = battle.active(player);
2241
2254
 
2242
2255
  assert(state.damage > 0);
2243
2256
 
2244
2257
  const damage: i16 = @intCast(@max(state.damage / 4, 1));
2245
2258
  stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
2246
2259
 
2247
- try options.log.damage(.{
2248
- battle.active(player),
2249
- stored,
2250
- Damage.RecoilOf,
2251
- battle.active(player.foe()),
2252
- });
2260
+ try options.log.damage(.{ ident, stored, .RecoilOf, battle.active(player.foe()) });
2253
2261
  }
2254
2262
 
2255
2263
  pub fn reversal(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -2312,7 +2320,7 @@ pub const Effects = struct {
2312
2320
 
2313
2321
  pub fn sandstorm(battle: anytype, player: Player, _: *State, options: anytype) !void {
2314
2322
  if (battle.field.weather != .Sandstorm) return weather(battle, .Sandstorm, options);
2315
- return options.log.fail(.{ battle.active(player), Fail.None });
2323
+ return options.log.fail(.{ battle.active(player), .None });
2316
2324
  }
2317
2325
 
2318
2326
  pub fn safeguard(battle: anytype, player: Player, _: *State, options: anytype) !void {
@@ -2337,7 +2345,7 @@ pub const Effects = struct {
2337
2345
  var side = battle.side(player);
2338
2346
 
2339
2347
  if (@field(side.conditions, key)) {
2340
- return options.log.fail(.{ battle.active(player), Fail.None });
2348
+ return options.log.fail(.{ battle.active(player), .None });
2341
2349
  }
2342
2350
 
2343
2351
  @field(side.conditions, key) = true;
@@ -2353,9 +2361,9 @@ pub const Effects = struct {
2353
2361
 
2354
2362
  if (showdown) {
2355
2363
  // yes, actually "Splash". '|nothing' gets blindly mapped to '|-activate||move:Splash'
2356
- try options.log.activate(battle.active(player), .Splash);
2364
+ try options.log.activate(.{ battle.active(player), .Splash });
2357
2365
  } else {
2358
- try options.log.fail(.{ battle.active(player), Fail.None });
2366
+ try options.log.fail(.{ battle.active(player), .None });
2359
2367
  }
2360
2368
  }
2361
2369
 
@@ -2372,7 +2380,7 @@ pub const Effects = struct {
2372
2380
  }
2373
2381
 
2374
2382
  if (foe.active.volatiles.Substitute) {
2375
- return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
2383
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
2376
2384
  }
2377
2385
 
2378
2386
  // Sleep Clause Mod
@@ -2383,22 +2391,21 @@ pub const Effects = struct {
2383
2391
  }
2384
2392
 
2385
2393
  foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2386
- try options.log.status(.{ foe_ident, foe_stored.status, protocol.Status.From, state.move });
2394
+ try options.log.status(.{ foe_ident, foe_stored.status, .From, state.move });
2387
2395
  }
2388
2396
 
2389
2397
  pub fn sleepTalk(battle: anytype, player: Player, state: *State, options: anytype) !void {
2390
2398
  const side = battle.side(player);
2391
2399
 
2392
2400
  if (!Status.is(side.stored().status, .SLP)) {
2393
- if (!showdown) try options.log.fail(.{ battle.active(player), Fail.None });
2394
- return;
2401
+ return if (!showdown) try options.log.fail(.{ battle.active(player), .None });
2395
2402
  }
2396
2403
 
2397
2404
  var n: u3 = 0;
2398
2405
  for (side.active.moves, 0..) |m, i| {
2399
2406
  if (state.mslot - 1 != i and Move.get(m.id).extra.sleep_talk) n += 1;
2400
2407
  }
2401
- if (n == 0) return options.log.fail(.{ battle.active(player), Fail.None });
2408
+ if (n == 0) return options.log.fail(.{ battle.active(player), .None });
2402
2409
 
2403
2410
  _ = try Rolls.sleepTalk(battle, player, n, state.mslot, options); // TODO
2404
2411
  }
@@ -2407,55 +2414,75 @@ pub const Effects = struct {
2407
2414
  if (Status.is(battle.side(player).stored().status, .SLP)) return true;
2408
2415
  state.damage = 0;
2409
2416
  state.miss = true; // TODO is this necessary?
2410
- if (!showdown) try options.log.fail(.{ battle.active(player), Fail.None });
2417
+ if (!showdown) try options.log.fail(.{ battle.active(player), .None });
2411
2418
  return false;
2412
2419
  }
2413
2420
 
2414
2421
  pub fn spikes(battle: anytype, player: Player, _: *State, options: anytype) !void {
2415
2422
  var foe = battle.foe(player);
2416
- if (foe.conditions.Spikes) return options.log.fail(.{ battle.active(player), Fail.None });
2423
+ if (foe.conditions.Spikes) return options.log.fail(.{ battle.active(player), .None });
2417
2424
 
2418
2425
  foe.conditions.Spikes = true;
2419
2426
  try options.log.sidestart(.{ player.foe(), protocol.Side.Spikes });
2420
2427
  }
2421
2428
 
2422
2429
  pub fn spite(battle: anytype, player: Player, state: *State, options: anytype) !void {
2423
- _ = .{ battle, player, state, options }; // TODO
2424
- // var foe = battle.foe(player);
2430
+ var foe = battle.foe(player);
2425
2431
 
2426
- // if (state.miss) return options.log.fail(.{ battle.active(player), Fail.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
+ }
2427
2436
 
2428
- // const last = foe.lastMove(false);
2429
- // if (showdown) {
2430
- // const roll = try Rolls.spite(battle, player, options);
2431
- // } else {
2432
- // if (last != .None) {}
2433
- // }
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 });
2434
2461
  }
2435
2462
 
2436
2463
  pub fn splash(battle: anytype, player: Player, _: *State, options: anytype) !void {
2437
- try options.log.activate(battle.active(player), .Splash);
2464
+ try options.log.activate(.{ battle.active(player), .Splash });
2438
2465
  }
2439
2466
 
2440
2467
  pub fn substitute(battle: anytype, player: Player, _: *State, options: anytype) !void {
2441
2468
  var side = battle.side(player);
2442
2469
  if (side.active.volatiles.Substitute) {
2443
- return options.log.fail(.{ battle.active(player), Fail.Substitute });
2470
+ return options.log.fail(.{ battle.active(player), .Substitute });
2444
2471
  }
2445
2472
 
2446
2473
  assert(side.stored().stats.hp <= 1023);
2447
2474
  const hp: u8 = @intCast(side.stored().stats.hp / 4);
2448
2475
 
2449
2476
  if (hp == 0 or side.stored().hp <= hp) {
2450
- return options.log.fail(.{ battle.active(player), Fail.Weak });
2477
+ return options.log.fail(.{ battle.active(player), .Weak });
2451
2478
  }
2452
2479
 
2453
2480
  side.stored().hp -= hp;
2454
2481
  side.active.volatiles.substitute = hp;
2455
2482
  side.active.volatiles.Substitute = true;
2456
2483
  side.active.volatiles.bind = .{};
2457
- try options.log.start(battle.active(player), .Substitute);
2458
- try options.log.damage(.{ battle.active(player), side.stored(), Damage.None });
2484
+ try options.log.start(.{ battle.active(player), .Substitute });
2485
+ try options.log.damage(.{ battle.active(player), side.stored(), .None });
2459
2486
  }
2460
2487
 
2461
2488
  pub fn sunnyDay(battle: anytype, _: Player, _: *State, options: anytype) !void {
@@ -2471,7 +2498,7 @@ pub const Effects = struct {
2471
2498
  pub fn teleport(battle: anytype, player: Player, _: *State, options: anytype) !void {
2472
2499
  if (showdown) return;
2473
2500
 
2474
- try options.log.fail(.{ battle.active(player), Fail.None });
2501
+ try options.log.fail(.{ battle.active(player), .None });
2475
2502
  try options.log.laststill(.{});
2476
2503
  }
2477
2504
 
@@ -2513,9 +2540,9 @@ pub const Effects = struct {
2513
2540
  side.active.volatiles.dirty = false;
2514
2541
 
2515
2542
  if (foe.active.volatiles.Transform) {
2516
- return options.log.fail(.{ battle.active(player), Fail.None });
2543
+ return options.log.fail(.{ battle.active(player), .None });
2517
2544
  } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2518
- if (!showdown) return options.log.fail(.{ battle.active(player), Fail.None });
2545
+ if (!showdown) return options.log.fail(.{ battle.active(player), .None });
2519
2546
  try options.log.lastmiss(.{});
2520
2547
  return options.log.miss(.{battle.active(player)});
2521
2548
  }
@@ -2564,8 +2591,8 @@ pub const Effects = struct {
2564
2591
  pub fn allStatUpChance(battle: anytype, player: Player, state: *State, options: anytype) !void {
2565
2592
  var side = battle.side(player);
2566
2593
 
2567
- var stats = &side.active.stats;
2568
- var boosts = &side.active.boosts;
2594
+ const stats = &side.active.stats;
2595
+ const boosts = &side.active.boosts;
2569
2596
 
2570
2597
  if (!state.proc) return;
2571
2598
  inline for (STATS, STAT_UP_REASONS) |s, r| {
@@ -2584,9 +2611,9 @@ pub const Effects = struct {
2584
2611
  var side = battle.side(player);
2585
2612
  const ident = battle.active(player);
2586
2613
 
2587
- if (state.miss) return log.fail(.{ ident, Fail.None });
2614
+ if (state.miss) return log.fail(.{ ident, .None });
2588
2615
 
2589
- var stats = &side.active.stats;
2616
+ const stats = &side.active.stats;
2590
2617
  var boosts = &side.active.boosts;
2591
2618
 
2592
2619
  const move = Move.get(state.move);
@@ -2594,7 +2621,7 @@ pub const Effects = struct {
2594
2621
  .AttackUp1, .AttackUp2, .Rage, .Swagger => {
2595
2622
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2596
2623
  if (boosts.atk == 6 or stats.atk == MAX_STAT_VALUE) {
2597
- return log.fail(.{ ident, Fail.None });
2624
+ return log.fail(.{ ident, .None });
2598
2625
  }
2599
2626
  const n: u2 = if (move.effect == .AttackUp2 or move.effect == .Swagger) 2 else 1;
2600
2627
  try boostStat(battle, side, stats, boosts, "atk", n);
@@ -2604,41 +2631,41 @@ pub const Effects = struct {
2604
2631
  .DefenseUp1, .DefenseUp2, .SkullBash => {
2605
2632
  assert(boosts.def >= -6 and boosts.def <= 6);
2606
2633
  if (boosts.def == 6 or stats.def == MAX_STAT_VALUE) {
2607
- return log.fail(.{ ident, Fail.None });
2634
+ return log.fail(.{ ident, .None });
2608
2635
  }
2609
2636
  const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
2610
2637
  try boostStat(battle, side, stats, boosts, "def", n);
2611
- try log.boost(.{ ident, Boost.Defense, n });
2638
+ try log.boost(.{ ident, .Defense, n });
2612
2639
  },
2613
2640
  .SpeedUp2 => {
2614
2641
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2615
2642
  if (boosts.spe == 6 or stats.spe == MAX_STAT_VALUE) {
2616
- return log.fail(.{ ident, Fail.None });
2643
+ return log.fail(.{ ident, .None });
2617
2644
  }
2618
2645
  try boostStat(battle, side, stats, boosts, "spe", 2);
2619
- try log.boost(.{ ident, Boost.Speed, 2 });
2646
+ try log.boost(.{ ident, .Speed, 2 });
2620
2647
  },
2621
2648
  .SpAtkUp1 => {
2622
2649
  assert(boosts.spa >= -6 and boosts.spa <= 6);
2623
2650
  if (boosts.spa == 6 or stats.spa == MAX_STAT_VALUE) {
2624
- return log.fail(.{ ident, Fail.None });
2651
+ return log.fail(.{ ident, .None });
2625
2652
  }
2626
2653
  try boostStat(battle, side, stats, boosts, "spa", 1);
2627
- try log.boost(.{ ident, Boost.SpecialAttack, 1 });
2654
+ try log.boost(.{ ident, .SpecialAttack, 1 });
2628
2655
  },
2629
2656
  .SpDefUp2 => {
2630
2657
  assert(boosts.spd >= -6 and boosts.spd <= 6);
2631
2658
  if (boosts.spd == 6 or stats.spd == MAX_STAT_VALUE) {
2632
- return log.fail(.{ ident, Fail.None });
2659
+ return log.fail(.{ ident, .None });
2633
2660
  }
2634
2661
  try boostStat(battle, side, stats, boosts, "spd", 2);
2635
- try log.boost(.{ ident, Boost.SpecialDefense, 2 });
2662
+ try log.boost(.{ ident, .SpecialDefense, 2 });
2636
2663
  },
2637
2664
  .EvasionUp1 => {
2638
2665
  assert(boosts.evasion >= -6 and boosts.evasion <= 6);
2639
- if (boosts.evasion == 6) return log.fail(.{ ident, Fail.None });
2666
+ if (boosts.evasion == 6) return log.fail(.{ ident, .None });
2640
2667
  boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
2641
- try log.boost(.{ ident, Boost.Evasion, 1 });
2668
+ try log.boost(.{ ident, .Evasion, 1 });
2642
2669
  if (state.move == .Minimize) side.active.volatiles.minimized = true;
2643
2670
  },
2644
2671
  else => unreachable,
@@ -2651,13 +2678,13 @@ pub const Effects = struct {
2651
2678
  const foe_ident = battle.active(player.foe());
2652
2679
 
2653
2680
  if (foe.active.volatiles.Mist) {
2654
- try log.activate(foe_ident, .Mist);
2655
- return log.fail(.{ foe_ident, Fail.None });
2681
+ try log.activate(.{ foe_ident, .Mist });
2682
+ return log.fail(.{ foe_ident, .None });
2656
2683
  }
2657
2684
 
2658
2685
  const fail = foe.active.volatiles.Substitute or state.miss or !state.proc;
2659
2686
 
2660
- var stats = &foe.active.stats;
2687
+ const stats = &foe.active.stats;
2661
2688
  var boosts = &foe.active.boosts;
2662
2689
 
2663
2690
  const move = Move.get(state.move);
@@ -2675,7 +2702,7 @@ pub const Effects = struct {
2675
2702
  }
2676
2703
  const n: u2 = if (move.effect == .AttackDown2) 2 else 1;
2677
2704
  try unboostStat(battle, foe, stats, boosts, "atk", n);
2678
- try log.boost(.{ foe_ident, Boost.Attack, -@as(i8, n) });
2705
+ try log.boost(.{ foe_ident, .Attack, -@as(i8, n) });
2679
2706
  },
2680
2707
  .DefenseDown1, .DefenseDown2, .DefenseDownChance => {
2681
2708
  assert(boosts.def >= -6 and boosts.def <= 6);
@@ -2691,7 +2718,7 @@ pub const Effects = struct {
2691
2718
  }
2692
2719
  const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
2693
2720
  try unboostStat(battle, foe, stats, boosts, "def", n);
2694
- try log.boost(.{ foe_ident, Boost.Defense, -@as(i8, n) });
2721
+ try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
2695
2722
  },
2696
2723
  .SpeedDown1, .SpeedDown2, .SpeedDownChance => {
2697
2724
  assert(boosts.spe >= -6 and boosts.spe <= 6);
@@ -2705,7 +2732,7 @@ pub const Effects = struct {
2705
2732
  }
2706
2733
  const n: u2 = if (move.effect == .SpeedDown2) 2 else 1;
2707
2734
  try unboostStat(battle, foe, stats, boosts, "spe", n);
2708
- try log.boost(.{ foe_ident, Boost.Speed, -@as(i8, n) });
2735
+ try log.boost(.{ foe_ident, .Speed, -@as(i8, n) });
2709
2736
  },
2710
2737
  .SpDefDownChance => {
2711
2738
  assert(boosts.spd >= -6 and boosts.spd <= 6);
@@ -2715,7 +2742,7 @@ pub const Effects = struct {
2715
2742
  return options.log.miss(.{battle.active(player)});
2716
2743
  } else if (stats.spd == 1) return;
2717
2744
  try unboostStat(battle, foe, stats, boosts, "spd", 1);
2718
- try log.boost(.{ foe_ident, Boost.SpecialDefense, -1 });
2745
+ try log.boost(.{ foe_ident, .SpecialDefense, -1 });
2719
2746
  },
2720
2747
  .AccuracyDown1, .AccuracyDownChance => {
2721
2748
  assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
@@ -2727,7 +2754,7 @@ pub const Effects = struct {
2727
2754
  return options.log.miss(.{battle.active(player)});
2728
2755
  }
2729
2756
  boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
2730
- try log.boost(.{ foe_ident, Boost.Accuracy, -1 });
2757
+ try log.boost(.{ foe_ident, .Accuracy, -1 });
2731
2758
  },
2732
2759
  else => unreachable,
2733
2760
  }
@@ -2741,9 +2768,9 @@ fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
2741
2768
  }
2742
2769
 
2743
2770
  fn recomputeStats(battle: anytype, side: *Side) void {
2744
- var stats = unmodifiedStats(battle, side);
2771
+ const stats = unmodifiedStats(battle, side);
2745
2772
  inline for (STATS) |s| {
2746
- 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))];
2747
2774
  const val = @field(stats, s) * mod[0] / mod[1];
2748
2775
  @field(side.active.stats, s) = @max(1, @min(MAX_STAT_VALUE, val));
2749
2776
  }
@@ -2766,7 +2793,7 @@ fn boostStat(
2766
2793
  n: u2,
2767
2794
  ) !void {
2768
2795
  @field(boosts, s) = @intCast(@min(6, @as(i8, @field(boosts, s)) + n));
2769
- 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))];
2770
2797
  const stat = @field(unmodifiedStats(battle, side), s);
2771
2798
  @field(stats, s) = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2772
2799
  if (comptime std.mem.eql(u8, s, "atk")) {
@@ -2789,7 +2816,7 @@ fn unboostStat(
2789
2816
  n: u2,
2790
2817
  ) !void {
2791
2818
  @field(boosts, s) = @intCast(@max(-6, @as(i8, @field(boosts, s)) - n));
2792
- 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))];
2793
2820
  const stat = @field(unmodifiedStats(battle, side), s);
2794
2821
  @field(stats, s) = @max(1, stat * mod[0] / mod[1]);
2795
2822
  if (comptime std.mem.eql(u8, s, "atk")) {
@@ -2817,7 +2844,7 @@ fn convertible(active: ActivePokemon, m: Move) bool {
2817
2844
 
2818
2845
  pub const Rolls = struct {
2819
2846
  fn speedTie(battle: anytype, options: anytype) !bool {
2820
- const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
2847
+ const p1 = if (options.calc.overridden(.P1, 0, .speed_tie)) |player|
2821
2848
  player == .P1
2822
2849
  else if (showdown)
2823
2850
  battle.rng.range(u8, 0, 2) == 0
@@ -2829,7 +2856,7 @@ pub const Rolls = struct {
2829
2856
  }
2830
2857
 
2831
2858
  fn quickClaw(battle: anytype, player: Player, options: anytype) !bool {
2832
- const qkc = if (options.calc.overridden(player, .quick_claw)) |val|
2859
+ const qkc = if (options.calc.overridden(player, 0, .quick_claw)) |val|
2833
2860
  val == .true
2834
2861
  else if (showdown)
2835
2862
  battle.rng.chance(u8, 60, 256)
@@ -2841,7 +2868,7 @@ pub const Rolls = struct {
2841
2868
  }
2842
2869
 
2843
2870
  fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) !bool {
2844
- const ok = if (options.calc.overridden(player, .hit)) |val|
2871
+ const ok = if (options.calc.overridden(player, 0, .hit)) |val|
2845
2872
  val == .true
2846
2873
  else if (showdown)
2847
2874
  battle.rng.chance(u8, accuracy, 256)
@@ -2852,20 +2879,22 @@ pub const Rolls = struct {
2852
2879
  return ok;
2853
2880
  }
2854
2881
 
2855
- fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2856
- 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|
2857
2885
  val == .true
2858
2886
  else if (showdown)
2859
2887
  battle.rng.chance(u8, rate, 256)
2860
2888
  else
2861
2889
  battle.rng.next() < rate;
2862
2890
 
2863
- try options.chance.criticalHit(player, crit, rate);
2891
+ try options.chance.criticalHit(player, i, crit, rate);
2864
2892
  return crit;
2865
2893
  }
2866
2894
 
2867
- fn damage(battle: anytype, player: Player, options: anytype) !u8 {
2868
- 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|
2869
2898
  val
2870
2899
  else roll: {
2871
2900
  if (showdown) break :roll battle.rng.range(u8, 217, 256);
@@ -2876,12 +2905,12 @@ pub const Rolls = struct {
2876
2905
  };
2877
2906
 
2878
2907
  assert(roll >= 217 and roll <= 255);
2879
- try options.chance.damage(player, roll);
2908
+ try options.chance.damage(player, i, roll);
2880
2909
  return roll;
2881
2910
  }
2882
2911
 
2883
2912
  fn confused(battle: anytype, player: Player, options: anytype) !bool {
2884
- const cfz = if (options.calc.overridden(player, .confused)) |val|
2913
+ const cfz = if (options.calc.overridden(player, 0, .confused)) |val|
2885
2914
  val == .true
2886
2915
  else if (showdown)
2887
2916
  !battle.rng.chance(u8, 128, 256)
@@ -2893,7 +2922,7 @@ pub const Rolls = struct {
2893
2922
  }
2894
2923
 
2895
2924
  fn attract(battle: anytype, player: Player, options: anytype) !bool {
2896
- const cant = if (options.calc.overridden(player, .attract)) |val|
2925
+ const cant = if (options.calc.overridden(player, 0, .attract)) |val|
2897
2926
  val == .true
2898
2927
  else if (showdown)
2899
2928
  battle.rng.chance(u8, 1, 2)
@@ -2905,7 +2934,7 @@ pub const Rolls = struct {
2905
2934
  }
2906
2935
 
2907
2936
  fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2908
- const par = if (options.calc.overridden(player, .paralyzed)) |val|
2937
+ const par = if (options.calc.overridden(player, 0, .paralyzed)) |val|
2909
2938
  val == .true
2910
2939
  else if (showdown)
2911
2940
  battle.rng.chance(u8, 63, 256)
@@ -2917,7 +2946,7 @@ pub const Rolls = struct {
2917
2946
  }
2918
2947
 
2919
2948
  fn defrost(battle: anytype, player: Player, options: anytype) !bool {
2920
- const thaw = if (options.calc.overridden(player, .defrost)) |val|
2949
+ const thaw = if (options.calc.overridden(player, 0, .defrost)) |val|
2921
2950
  val == .true
2922
2951
  else if (showdown)
2923
2952
  battle.rng.chance(u8, 25, 256)
@@ -2930,7 +2959,7 @@ pub const Rolls = struct {
2930
2959
 
2931
2960
  fn secondaryChance(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2932
2961
  // GLITCH: 100% secondary chance will still fail 1/256 of the time
2933
- const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2962
+ const proc = if (options.calc.overridden(player, 0, .secondary_chance)) |val|
2934
2963
  val == .true
2935
2964
  else if (showdown)
2936
2965
  battle.rng.chance(u8, rate, 256)
@@ -2942,7 +2971,7 @@ pub const Rolls = struct {
2942
2971
  }
2943
2972
 
2944
2973
  fn item(battle: anytype, player: Player, options: anytype) !bool {
2945
- const proc = if (options.calc.overridden(player, .item)) |val|
2974
+ const proc = if (options.calc.overridden(player, 0, .item)) |val|
2946
2975
  val == .true
2947
2976
  else if (showdown)
2948
2977
  battle.rng.chance(u8, 30, 256)
@@ -2954,7 +2983,7 @@ pub const Rolls = struct {
2954
2983
  }
2955
2984
 
2956
2985
  fn triAttack(battle: anytype, player: Player, options: anytype) !TriAttack {
2957
- const status: TriAttack = if (options.calc.overridden(player, .tri_attack)) |val|
2986
+ const status: TriAttack = if (options.calc.overridden(player, 0, .tri_attack)) |val|
2958
2987
  @enumFromInt(@intFromEnum(val) - 1)
2959
2988
  else if (showdown)
2960
2989
  @enumFromInt(battle.rng.range(u2, 0, 2))
@@ -2970,7 +2999,7 @@ pub const Rolls = struct {
2970
2999
  }
2971
3000
 
2972
3001
  fn present(battle: anytype, player: Player, options: anytype) !u8 {
2973
- const power = if (options.calc.overridden(player, .present)) |val|
3002
+ const power = if (options.calc.overridden(player, 0, .present)) |val|
2974
3003
  (val - 1) * 40
2975
3004
  else if (showdown) power: {
2976
3005
  const r = battle.rng.range(u8, 0, 10);
@@ -2992,7 +3021,7 @@ pub const Rolls = struct {
2992
3021
  }
2993
3022
 
2994
3023
  fn magnitude(battle: anytype, player: Player, options: anytype) !u8 {
2995
- const num: u8 = if (options.calc.overridden(player, .magnitude)) |val|
3024
+ const num: u8 = if (options.calc.overridden(player, 0, .magnitude)) |val|
2996
3025
  val + 3
2997
3026
  else if (showdown) num: {
2998
3027
  const r = battle.rng.range(u8, 0, 100);
@@ -3020,7 +3049,7 @@ pub const Rolls = struct {
3020
3049
  }
3021
3050
 
3022
3051
  fn tripleKick(battle: anytype, player: Player, options: anytype) !u2 {
3023
- const hits = if (options.calc.overridden(player, .triple_kick)) |val|
3052
+ const hits = if (options.calc.overridden(player, 0, .triple_kick)) |val|
3024
3053
  val
3025
3054
  else if (showdown)
3026
3055
  battle.rng.range(u2, 1, 4)
@@ -3036,13 +3065,12 @@ pub const Rolls = struct {
3036
3065
  return hits;
3037
3066
  }
3038
3067
 
3039
- fn spite(battle: anytype, player: Player, options: anytype) !u8 {
3040
- 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|
3041
3071
  val + 1
3042
- else if (showdown)
3043
- battle.rng.range(u8, 2, 6)
3044
3072
  else
3045
- (battle.rng.next() & 3) + 2;
3073
+ @intCast((battle.rng.next() & 3) + 2);
3046
3074
 
3047
3075
  assert(pp >= 2 and pp <= 5);
3048
3076
  try options.chance.spite(player, pp);
@@ -3056,7 +3084,7 @@ pub const Rolls = struct {
3056
3084
  var i: u8 = 0;
3057
3085
  const neutral = @intFromEnum(Effectiveness.Neutral);
3058
3086
  assert(mtype != .@"???");
3059
- const ty: Type = if (options.calc.overridden(player, .conversion_2)) |val|
3087
+ const ty: Type = if (options.calc.overridden(player, 0, .conversion_2)) |val|
3060
3088
  @enumFromInt(@intFromEnum(val) - 1)
3061
3089
  else if (showdown) ty: {
3062
3090
  for (Type.SHOWDOWN) |t| {
@@ -3090,7 +3118,7 @@ pub const Rolls = struct {
3090
3118
  assert(count <= 8);
3091
3119
  const num = if (count == 8) 0 else @as(u8, 255) >> @intCast(count);
3092
3120
 
3093
- const ok = if (options.calc.overridden(player, .protect)) |val|
3121
+ const ok = if (options.calc.overridden(player, 0, .protect)) |val|
3094
3122
  val == .true
3095
3123
  else if (showdown)
3096
3124
  battle.rng.chance(u8, num, 255)
@@ -3112,7 +3140,7 @@ pub const Rolls = struct {
3112
3140
  const moves = active.moves;
3113
3141
 
3114
3142
  // TODO: consider throwing error instead of rerolling?
3115
- const overridden = if (options.calc.overridden(player, .move_slot)) |val|
3143
+ const overridden = if (options.calc.overridden(player, 0, .move_slot)) |val|
3116
3144
  if (convertible(active, moves[val - 1].id)) val else null
3117
3145
  else
3118
3146
  null;
@@ -3149,7 +3177,7 @@ pub const Rolls = struct {
3149
3177
  const moves = active.moves;
3150
3178
 
3151
3179
  // TODO: consider throwing error instead of rerolling?
3152
- const overridden = if (options.calc.overridden(player, .move_slot)) |val|
3180
+ const overridden = if (options.calc.overridden(player, 0, .move_slot)) |val|
3153
3181
  if (val != mslot and Move.get(moves[val - 1].id).extra.sleep_talk) val else null
3154
3182
  else
3155
3183
  null;
@@ -3193,7 +3221,7 @@ pub const Rolls = struct {
3193
3221
  assert(n > 0 and n <= 6);
3194
3222
 
3195
3223
  // TODO: consider throwing error instead of rerolling?
3196
- const overridden = if (options.calc.overridden(player, .force_switch)) |val| val: {
3224
+ const overridden = if (options.calc.overridden(player, 0, .force_switch)) |val| val: {
3197
3225
  for (slots) |slot| if (slot == val) break :val if (foe.get(slot).hp > 0) val else null;
3198
3226
  break :val null;
3199
3227
  } else null;
@@ -3219,7 +3247,7 @@ pub const Rolls = struct {
3219
3247
  const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
3220
3248
 
3221
3249
  fn multiHit(battle: anytype, player: Player, options: anytype) !u3 {
3222
- const n: u3 = if (options.calc.overridden(player, .multi_hit)) |val|
3250
+ const n: u3 = if (options.calc.overridden(player, 0, .multi_hit)) |val|
3223
3251
  @intCast(val)
3224
3252
  else if (showdown)
3225
3253
  DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
@@ -3234,7 +3262,7 @@ pub const Rolls = struct {
3234
3262
  }
3235
3263
 
3236
3264
  fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
3237
- const power = if (options.calc.overridden(player, .psywave)) |val|
3265
+ const power = if (options.calc.overridden(player, 0, .psywave)) |val|
3238
3266
  val
3239
3267
  else if (showdown)
3240
3268
  battle.rng.range(u8, 1, max)
@@ -3260,7 +3288,7 @@ pub const Rolls = struct {
3260
3288
  }
3261
3289
 
3262
3290
  // TODO: consider throwing error instead of rerolling?
3263
- const overridden = if (options.calc.overridden(player, .metronome)) |val| val: {
3291
+ const overridden = if (options.calc.overridden(player, 0, .metronome)) |val| val: {
3264
3292
  for (moves) |m| if (m.id == val) break :val null;
3265
3293
  break :val val;
3266
3294
  } else null;
@@ -3292,7 +3320,7 @@ pub const Rolls = struct {
3292
3320
  }
3293
3321
 
3294
3322
  fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
3295
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
3323
+ const duration: u3 = if (options.calc.overridden(player, 0, .duration)) |val|
3296
3324
  @intCast(val)
3297
3325
  else if (showdown)
3298
3326
  battle.rng.range(u3, 1, 7)
@@ -3304,12 +3332,12 @@ pub const Rolls = struct {
3304
3332
  };
3305
3333
 
3306
3334
  assert(duration >= 1 and duration <= 6);
3307
- options.chance.duration(.sleep, player, player.foe(), duration);
3335
+ options.chance.duration(player, duration);
3308
3336
  return duration + 1;
3309
3337
  }
3310
3338
 
3311
3339
  fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
3312
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
3340
+ const duration: u3 = if (options.calc.overridden(player, 0, .duration)) |val|
3313
3341
  @intCast(val)
3314
3342
  else if (showdown)
3315
3343
  // Pokémon Showdown incorrectly uses the same duration for self-confusion
@@ -3318,16 +3346,16 @@ pub const Rolls = struct {
3318
3346
  @intCast((battle.rng.next() & if (self) 1 else 3) + 2);
3319
3347
 
3320
3348
  assert(duration >= 2 and duration <= if (!showdown and self) 3 else 5);
3321
- options.chance.duration(.confusion, player, if (self) player else player.foe(), duration);
3349
+ options.chance.duration(player, duration);
3322
3350
  return duration;
3323
3351
  }
3324
3352
 
3325
3353
  fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
3326
- const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
3354
+ const duration: u3 = if (options.calc.overridden(player, 0, .duration)) |val|
3327
3355
  @intCast(val)
3328
3356
  else if (showdown)
3329
3357
  // Pokémon Showdown incorrectly inherits Generation III's Disable duration
3330
- battle.rng.range(u4, 2, 6)
3358
+ battle.rng.range(u3, 2, 6)
3331
3359
  else duration: {
3332
3360
  while (true) {
3333
3361
  const r = battle.rng.next() & 7;
@@ -3336,12 +3364,12 @@ pub const Rolls = struct {
3336
3364
  };
3337
3365
 
3338
3366
  assert(duration >= 2 and duration <= (if (showdown) 5 else 7));
3339
- options.chance.duration(.disable, player, player.foe(), duration);
3367
+ options.chance.duration(player, duration);
3340
3368
  return duration;
3341
3369
  }
3342
3370
 
3343
3371
  fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
3344
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
3372
+ const duration: u3 = if (options.calc.overridden(player, 0, .duration)) |val|
3345
3373
  @intCast(val)
3346
3374
  else if (showdown)
3347
3375
  battle.rng.range(u3, 2, 4)
@@ -3349,12 +3377,12 @@ pub const Rolls = struct {
3349
3377
  @intCast((battle.rng.next() & 1) + 2); // BUG: thrash is + 1 not +2
3350
3378
 
3351
3379
  assert(duration >= 2 and duration <= 3);
3352
- options.chance.duration(.attacking, player, player, duration);
3380
+ options.chance.duration(player, duration);
3353
3381
  return duration;
3354
3382
  }
3355
3383
 
3356
3384
  fn bindingDuration(battle: anytype, player: Player, options: anytype) u3 {
3357
- const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
3385
+ const duration: u3 = (if (options.calc.overridden(player, 0, .duration)) |val|
3358
3386
  @intCast(val)
3359
3387
  else if (showdown)
3360
3388
  // TODO: PS does range(3, 6) here which after subtracting 1 is actually 2-4 not 2-5!
@@ -3363,13 +3391,13 @@ pub const Rolls = struct {
3363
3391
  @intCast(battle.rng.next() & 3 + 2));
3364
3392
 
3365
3393
  assert(duration >= 2 and duration <= 5);
3366
- options.chance.duration(.binding, player, player.foe(), duration);
3394
+ options.chance.duration(player, duration);
3367
3395
  return duration + 1;
3368
3396
  }
3369
3397
 
3370
3398
  // TODO: consider sharing implementation with bindingDuration
3371
3399
  fn encoreDuration(battle: anytype, player: Player, options: anytype) u3 {
3372
- const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
3400
+ const duration: u3 = (if (options.calc.overridden(player, 0, .duration)) |val|
3373
3401
  @intCast(val)
3374
3402
  else if (showdown)
3375
3403
  battle.rng.range(u3, 2, 6)
@@ -3377,7 +3405,7 @@ pub const Rolls = struct {
3377
3405
  @intCast(battle.rng.next() & 3 + 2));
3378
3406
 
3379
3407
  assert(duration >= 2 and duration <= 5);
3380
- options.chance.duration(.encore, player, player.foe(), duration);
3408
+ options.chance.duration(player, duration);
3381
3409
  return duration + 1;
3382
3410
  }
3383
3411
  };
@@ -3490,7 +3518,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3490
3518
  .Move => {
3491
3519
  const side = battle.side(player);
3492
3520
 
3493
- var active = &side.active;
3521
+ const active = &side.active;
3494
3522
  const stored = side.stored();
3495
3523
 
3496
3524
  // While players are not given any input options on the cartridge in these cases,