@pkmn/engine 0.1.0-dev.30c63015 → 0.1.0-dev.3158fa12

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 (82) hide show
  1. package/LICENSE +1 -3
  2. package/README.md +46 -18
  3. package/build/pkg/addon/browser.d.ts +3 -0
  4. package/build/pkg/addon/browser.js +44 -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 +97 -0
  8. package/build/pkg/addon/node.js.map +1 -0
  9. package/build/pkg/addon/wasm.d.ts +2 -0
  10. package/build/pkg/addon/wasm.js +53 -0
  11. package/build/pkg/addon/wasm.js.map +1 -0
  12. package/build/pkg/addon.d.ts +25 -3
  13. package/build/pkg/addon.js +39 -71
  14. package/build/pkg/addon.js.map +1 -1
  15. package/build/pkg/data/layout.json +15 -2
  16. package/build/pkg/data/protocol.json +23 -9
  17. package/build/pkg/data.d.ts +27 -5
  18. package/build/pkg/data.js.map +1 -1
  19. package/build/pkg/gen1.d.ts +13 -17
  20. package/build/pkg/gen1.js +6 -10
  21. package/build/pkg/gen1.js.map +1 -1
  22. package/build/pkg/index.d.ts +40 -91
  23. package/build/pkg/index.js +6 -63
  24. package/build/pkg/index.js.map +1 -1
  25. package/build/pkg/protocol.d.ts +20 -1
  26. package/build/pkg/protocol.js +109 -107
  27. package/build/pkg/protocol.js.map +1 -1
  28. package/build/tools/debug.js +36 -71
  29. package/build/tools/debug.js.map +1 -1
  30. package/build/tools/display/debug.jsx +141 -0
  31. package/build/tools/display/debug.jsx.map +1 -0
  32. package/build/tools/display/index.js +142 -0
  33. package/build/tools/display/index.js.map +1 -0
  34. package/build/tools/display/ui.jsx +349 -0
  35. package/build/tools/display/ui.jsx.map +1 -0
  36. package/build/tools/display/util.js +18 -0
  37. package/build/tools/display/util.js.map +1 -0
  38. package/build.zig +287 -89
  39. package/package.json +58 -132
  40. package/src/bin/install-pkmn-engine +30 -20
  41. package/src/bin/pkmn-debug +1 -1
  42. package/src/lib/{binding → bindings}/node.zig +8 -21
  43. package/src/lib/bindings/wasm.zig +59 -0
  44. package/src/lib/common/array.zig +77 -0
  45. package/src/lib/common/data.zig +4 -4
  46. package/src/lib/common/debug.zig +39 -8
  47. package/src/lib/common/js.zig +4 -2
  48. package/src/lib/common/optional.zig +14 -10
  49. package/src/lib/common/options.zig +6 -0
  50. package/src/lib/common/protocol.zig +223 -215
  51. package/src/lib/common/rational.zig +128 -32
  52. package/src/lib/common/rng.zig +7 -9
  53. package/src/lib/common/util.zig +50 -1
  54. package/src/lib/gen1/calc.zig +686 -159
  55. package/src/lib/gen1/chance.zig +405 -280
  56. package/src/lib/gen1/data/moves.zig +2 -4
  57. package/src/lib/gen1/data/species.zig +2 -4
  58. package/src/lib/gen1/data/types.zig +1 -2
  59. package/src/lib/gen1/data.zig +24 -27
  60. package/src/lib/gen1/helpers.zig +92 -548
  61. package/src/lib/gen1/mechanics.zig +517 -411
  62. package/src/lib/gen2/calc.zig +37 -27
  63. package/src/lib/gen2/chance.zig +84 -496
  64. package/src/lib/gen2/data/items.zig +1 -3
  65. package/src/lib/gen2/data/moves.zig +2 -4
  66. package/src/lib/gen2/data/species.zig +1 -3
  67. package/src/lib/gen2/data/types.zig +1 -2
  68. package/src/lib/gen2/data.zig +19 -22
  69. package/src/lib/gen2/generated.zig +19 -26
  70. package/src/lib/gen2/helpers.zig +46 -25
  71. package/src/lib/gen2/mechanics.zig +367 -336
  72. package/src/lib/node.zig +18 -0
  73. package/src/lib/pkmn.zig +37 -15
  74. package/src/lib/wasm.zig +17 -0
  75. package/src/tools/display/dom.ts +96 -0
  76. package/src/tools/display/ui.css +354 -0
  77. package/build/test/display.js +0 -400
  78. package/build/test/display.js.map +0 -1
  79. package/src/lib/binding/wasm.zig +0 -12
  80. package/src/lib/gen1/README.md +0 -894
  81. package/src/lib/gen2/README.md +0 -329
  82. package/src/test/display/pkmn.html.tmpl +0 -368
@@ -1,55 +1,37 @@
1
- const std = @import("std");
2
-
3
- const pkmn = @import("../pkmn.zig");
4
-
5
- const common = @import("../common/data.zig");
6
1
  const DEBUG = @import("../common/debug.zig").print;
7
- const protocol = @import("../common/protocol.zig");
8
- const rng = @import("../common/rng.zig");
9
- const util = @import("../common/util.zig");
10
2
 
3
+ const builtin = @import("builtin");
4
+ const common = @import("../common/data.zig");
11
5
  const data = @import("data.zig");
12
6
  const generated = @import("generated.zig");
13
-
14
- const assert = std.debug.assert;
15
-
16
- const expectEqual = std.testing.expectEqual;
17
- const expectEqualSlices = std.testing.expectEqualSlices;
18
- const expectEqualStrings = std.testing.expectEqualStrings;
19
-
20
- const showdown = pkmn.options.showdown;
21
-
22
- const Choice = common.Choice;
23
- const ID = common.ID;
24
- const Player = common.Player;
25
- const Result = common.Result;
7
+ const pkmn = @import("../pkmn.zig");
8
+ const protocol = @import("../common/protocol.zig");
9
+ const rng = @import("../common/rng.zig");
10
+ const std = @import("std");
26
11
 
27
12
  const Activate = protocol.Activate;
13
+ const ActivePokemon = data.ActivePokemon;
14
+ const assert = std.debug.assert;
28
15
  const Boost = protocol.Boost;
29
- const Cant = protocol.Cant;
30
- const CureStatus = protocol.CureStatus;
16
+ const Boosts = data.Boosts;
17
+ const Choice = common.Choice;
31
18
  const Damage = protocol.Damage;
19
+ const Effectiveness = data.Effectiveness;
32
20
  const End = protocol.End;
33
- const EndItem = protocol.EndItem;
21
+ const expectEqual = std.testing.expectEqual;
22
+ const expectEqualSlices = std.testing.expectEqualSlices;
23
+ const expectEqualStrings = std.testing.expectEqualStrings;
34
24
  const Fail = protocol.Fail;
35
- const Heal = protocol.Heal;
36
- const Immune = protocol.Immune;
37
- const SetHP = protocol.SetHP;
38
- const Start = protocol.Start;
39
-
40
25
  const Gen12 = rng.Gen12;
41
-
42
- const PointerType = util.PointerType;
43
-
44
- const ActivePokemon = data.ActivePokemon;
45
- const Boosts = data.Boosts;
46
- const Effectiveness = data.Effectiveness;
26
+ const Heal = protocol.Heal;
27
+ const ID = common.ID;
47
28
  const Item = data.Item;
48
29
  const Move = data.Move;
49
- const MoveSlot = data.MoveSlot;
50
- const Pokemon = data.Pokemon;
30
+ const Player = common.Player;
31
+ const Result = common.Result;
32
+ const showdown = pkmn.options.showdown;
51
33
  const Side = data.Side;
52
- const Species = data.Species;
34
+ const Start = protocol.Start;
53
35
  const Stats = data.Stats;
54
36
  const Status = data.Status;
55
37
  const TriAttack = data.TriAttack;
@@ -111,6 +93,7 @@ pub const State = struct {
111
93
  type: Type, // BATTLE_VARS_MOVE_TYPE
112
94
  accuracy: u8, // wEnemyMoveStruct + MOVE_ACC
113
95
  move: Move, // wCurPlayerMove
96
+ hit: u3 = 0,
114
97
  from: Move = .None,
115
98
  mslot: u4, // wCurMoveNum
116
99
  first: bool, // wEnemyGoesFirst
@@ -145,11 +128,11 @@ fn start(battle: anytype, options: anytype) !Result {
145
128
  const p1 = battle.side(.P1);
146
129
  const p2 = battle.side(.P2);
147
130
 
148
- var p1_slot = findFirstAlive(p1);
131
+ const p1_slot = findFirstAlive(p1);
149
132
  assert(!showdown or p1_slot == 1);
150
133
  if (p1_slot == 0) return if (findFirstAlive(p2) == 0) Result.Tie else Result.Lose;
151
134
 
152
- var p2_slot = findFirstAlive(p2);
135
+ const p2_slot = findFirstAlive(p2);
153
136
  assert(!showdown or p2_slot == 1);
154
137
  if (p2_slot == 0) return Result.Win;
155
138
 
@@ -261,11 +244,10 @@ fn switchIn(battle: anytype, player: Player, slot: u8, kind: SwitchIn, options:
261
244
  } else {
262
245
  try options.log.switched(.{ battle.active(player), incoming });
263
246
  }
264
- options.chance.switched(player, side.pokemon[0].position, out);
265
247
 
266
248
  if (side.conditions.Spikes and !active.types.includes(.Flying)) {
267
249
  incoming.hp -|= @max(incoming.stats.hp / 8, 1);
268
- try options.log.damage(.{ battle.active(player), incoming, Damage.Spikes });
250
+ try options.log.damage(.{ battle.active(player), incoming, .Spikes });
269
251
  }
270
252
  }
271
253
 
@@ -408,7 +390,9 @@ fn executeMove(
408
390
 
409
391
  assert(choice.type == .Move);
410
392
  var mslot: u4 = @intCast(choice.data);
393
+ _ = &mslot;
411
394
  var move = Move.Pound; // TODO
395
+ _ = &move;
412
396
 
413
397
  if (!try beforeMove(battle, player, move, options)) return null;
414
398
 
@@ -438,31 +422,22 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
438
422
  if (volatiles.Recharging) {
439
423
  volatiles.Recharging = false;
440
424
  resetCant(volatiles);
441
- try log.cant(.{ ident, Cant.Recharge });
425
+ try log.cant(.{ ident, .Recharge });
442
426
  return false;
443
427
  }
444
428
 
445
429
  if (Status.is(stored.status, .SLP)) slp: {
446
430
  const before = stored.status;
447
431
  // 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
432
  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
433
 
459
- if (duration == 0) {
460
- try log.curestatus(.{ ident, before, CureStatus.Message });
434
+ if (Status.duration(stored.status) == 0) {
435
+ try log.curestatus(.{ ident, before, .Message });
461
436
  stored.status = 0; // clears EXT if present
462
437
  resetCant(volatiles);
463
438
  volatiles.Nightmare = false;
464
439
  } else {
465
- try log.cant(.{ ident, Cant.Sleep });
440
+ try log.cant(.{ ident, .Sleep });
466
441
  if (move == .Snore or move == .SleepTalk) break :slp;
467
442
  resetCant(volatiles);
468
443
  return false;
@@ -471,7 +446,7 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
471
446
 
472
447
  if (Status.is(stored.status, .FRZ) and !(move == .FlameWheel or move == .SacredFire)) {
473
448
  resetCant(volatiles);
474
- try log.cant(.{ ident, Cant.Freeze });
449
+ try log.cant(.{ ident, .Freeze });
475
450
  return false;
476
451
  }
477
452
 
@@ -481,39 +456,28 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
481
456
  // if (!showdown) volatiles.Flinch = false;
482
457
  volatiles.Flinch = false;
483
458
  resetCant(volatiles);
484
- try log.cant(.{ ident, Cant.Flinch });
459
+ try log.cant(.{ ident, .Flinch });
485
460
  return false;
486
461
  }
487
462
 
488
463
  if (volatiles.disable.duration > 0) {
489
- // if (options.calc.modify(player, .disable)) |extend| {
490
- // if (!extend) volatiles.disable.duration = 0;
491
- // } else {
492
464
  volatiles.disable.duration -= 1;
493
- // }
494
- try options.chance.disable(player, volatiles.disable.duration);
495
465
 
496
466
  if (volatiles.disable.duration == 0) {
497
467
  volatiles.disable.move = 0;
498
- try log.end(.{ ident, End.Disable });
468
+ try log.end(.{ ident, .Disable });
499
469
  }
500
470
  }
501
471
 
502
472
  if (volatiles.Confusion) {
503
473
  assert(volatiles.confusion > 0);
504
-
505
- // if (options.calc.modify(player, .confusion)) |extend| {
506
- // if (!extend) volatiles.confusion = 0;
507
- // } else {
508
474
  volatiles.confusion -= 1;
509
- // }
510
- try options.chance.confusion(player, volatiles.confusion);
511
475
 
512
476
  if (volatiles.confusion == 0) {
513
477
  volatiles.Confusion = false;
514
- try log.end(.{ ident, End.Confusion });
478
+ try log.end(.{ ident, .Confusion });
515
479
  } else {
516
- try log.activate(ident, .Confusion);
480
+ try log.activate(.{ ident, .Confusion });
517
481
 
518
482
  if (try Rolls.confused(battle, player, options)) {
519
483
  volatiles.BeatUp = false;
@@ -528,11 +492,11 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
528
492
  }
529
493
 
530
494
  if (volatiles.Attract) {
531
- try log.activate(ident, .Attract);
495
+ try log.activate(.{ ident, .Attract });
532
496
 
533
497
  if (try Rolls.attract(battle, player, options)) {
534
498
  resetCant(volatiles);
535
- try log.cant(.{ ident, Cant.Attract });
499
+ try log.cant(.{ ident, .Attract });
536
500
  return false;
537
501
  }
538
502
  }
@@ -543,14 +507,14 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
543
507
  if (m != .None and m == move) {
544
508
  side.active.volatiles.Charging = false;
545
509
  resetCant(volatiles);
546
- try options.log.cant(.{ ident, Cant.Disable, move });
510
+ try options.log.cant(.{ ident, .Disable, move });
547
511
  return false;
548
512
  }
549
513
  }
550
514
 
551
515
  if (Status.is(stored.status, .PAR) and try Rolls.paralyzed(battle, player, options)) {
552
516
  resetCant(volatiles);
553
- try log.cant(.{ ident, Cant.Paralysis });
517
+ try log.cant(.{ ident, .Paralysis });
554
518
  return false;
555
519
  }
556
520
 
@@ -570,7 +534,7 @@ fn resetCant(volatiles: *align(8) Volatiles) void {
570
534
 
571
535
  pub fn canMove(battle: anytype, player: Player, state: *State, options: anytype) !bool {
572
536
  var side = battle.side(player);
573
- var volatiles = &side.active.volatiles;
537
+ const volatiles = &side.active.volatiles;
574
538
 
575
539
  // FIXME: pass in target to avoid lookup?
576
540
  const target = if (Move.get(state.move).target == .Self) player else player.foe();
@@ -668,7 +632,7 @@ pub fn checkCriticalHit(battle: anytype, player: Player, state: *State, options:
668
632
  }
669
633
  assert(stage <= 4);
670
634
 
671
- state.crit = try Rolls.criticalHit(battle, player, CRITICAL_HITS[stage], options);
635
+ state.crit = try Rolls.criticalHit(battle, player, state.hit, CRITICAL_HITS[stage], options);
672
636
  }
673
637
 
674
638
  pub fn calcDamage(
@@ -736,7 +700,7 @@ pub fn adjustDamage(battle: anytype, player: Player, state: *State, _: anytype)
736
700
 
737
701
  pub fn randomizeDamage(battle: anytype, player: Player, state: *State, options: anytype) !void {
738
702
  if (state.damage <= 1) return;
739
- const random = try Rolls.damage(battle, player, options);
703
+ const random = try Rolls.damage(battle, player, state.hit, options);
740
704
  options.calc.base(player.foe(), state.damage);
741
705
  state.damage = @intCast(@as(u32, state.damage) *% random / 255);
742
706
  options.calc.final(player.foe(), state.damage);
@@ -953,26 +917,26 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !bool {
953
917
  const damage = @max(stored.stats.hp / 8, 1);
954
918
  stored.hp -|= damage;
955
919
  // As above, Pokémon Showdown uses damage [of] but its not relevant
956
- try options.log.damage(.{ ident, stored, Damage.LeechSeed });
920
+ try options.log.damage(.{ ident, stored, .LeechSeed });
957
921
 
958
922
  const before = foe_stored.hp;
959
923
  foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
960
924
  // 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 });
925
+ if (foe_stored.hp > before) try options.log.heal(.{ foe_ident, foe_stored, .Silent });
962
926
  if (stored.hp == 0) return true;
963
927
  }
964
928
 
965
929
  if (volatiles.Nightmare) {
966
930
  stored.hp -= @max(stored.stats.hp / 4, 1);
967
931
  // ibid
968
- try options.log.damage(.{ ident, stored, Damage.Nightmare });
932
+ try options.log.damage(.{ ident, stored, .Nightmare });
969
933
  if (stored.hp == 0) return true;
970
934
  }
971
935
 
972
936
  if (volatiles.Curse) {
973
937
  stored.hp -= @max(stored.stats.hp / 4, 1);
974
938
  // ibid
975
- try options.log.damage(.{ ident, stored, Damage.Curse });
939
+ try options.log.damage(.{ ident, stored, .Curse });
976
940
  if (stored.hp == 0) return true;
977
941
  }
978
942
 
@@ -993,7 +957,7 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
993
957
  future_sight.count -= 1;
994
958
  assert(future_sight.count != 0);
995
959
  if (future_sight.count == 1) {
996
- try options.log.end(.{ battle.active(player), End.FutureSight });
960
+ try options.log.end(.{ battle.active(player), .FutureSight });
997
961
  // TODO doMove
998
962
  }
999
963
  }
@@ -1002,7 +966,6 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1002
966
 
1003
967
  // Weather
1004
968
  if (battle.field.weather != .None) {
1005
- assert(battle.field.weather_duration > 0);
1006
969
  battle.field.weather_duration -= 1;
1007
970
 
1008
971
  if (battle.field.weather_duration == 0) {
@@ -1019,7 +982,7 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1019
982
  try options.log.damage(.{
1020
983
  battle.active(player),
1021
984
  side.stored(),
1022
- Damage.Sandstorm,
985
+ .Sandstorm,
1023
986
  });
1024
987
  }
1025
988
  }
@@ -1032,20 +995,21 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1032
995
  inline for (players) |player| {
1033
996
  var side = battle.side(player);
1034
997
  var volatiles = &side.active.volatiles;
998
+ const ident = battle.active(player);
1035
999
 
1036
1000
  if (volatiles.bind.duration > 0 and !volatiles.Substitute) {
1037
1001
  volatiles.bind.duration -= 1;
1038
1002
  if (volatiles.bind.duration == 0) {
1039
1003
  const reason = @intFromEnum(End.Bind) + volatiles.bind.reason - 1;
1040
- try options.log.end(.{ battle.active(player), @as(End, @enumFromInt(reason)) });
1004
+ try options.log.end(.{ ident, @as(End, @enumFromInt(reason)) });
1041
1005
  } else {
1042
1006
  var reason = @intFromEnum(Activate.Bind) + volatiles.bind.reason - 1;
1043
- try options.log.activate(battle.active(player), @enumFromInt(reason));
1007
+ try options.log.activate(.{ ident, @as(Activate, @enumFromInt(reason)) });
1044
1008
 
1045
1009
  reason = @intFromEnum(Damage.Bind) + volatiles.bind.reason - 1;
1046
1010
  side.stored().hp -|= @max(side.stored().stats.hp / 16, 1);
1047
1011
  try options.log.damage(.{
1048
- battle.active(player),
1012
+ ident,
1049
1013
  side.stored(),
1050
1014
  @as(Damage, @enumFromInt(reason)),
1051
1015
  });
@@ -1060,12 +1024,11 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1060
1024
  var volatiles = &side.active.volatiles;
1061
1025
 
1062
1026
  if (volatiles.PerishSong) {
1063
- assert(volatiles.perish_song > 0);
1064
1027
  volatiles.perish_song -= 1;
1065
1028
 
1066
1029
  assert(volatiles.perish_song < 3);
1067
1030
  const reason = @intFromEnum(Start.PerishSong0) + volatiles.perish_song;
1068
- try options.log.start(battle.active(player), @enumFromInt(reason));
1031
+ try options.log.start(.{ battle.active(player), @as(Start, @enumFromInt(reason)) });
1069
1032
 
1070
1033
  if (volatiles.perish_song == 0) {
1071
1034
  volatiles.PerishSong = false;
@@ -1083,14 +1046,14 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1083
1046
  const before = stored.hp;
1084
1047
  stored.hp = @min(stored.hp + @max(stored.stats.hp / 16, 1), stored.stats.hp);
1085
1048
  if (stored.hp > before) {
1086
- try options.log.heal(.{ battle.active(player), stored, Heal.Leftovers });
1049
+ try options.log.heal(.{ battle.active(player), stored, .Leftovers });
1087
1050
  }
1088
1051
  }
1089
1052
  }
1090
1053
 
1091
1054
  // Mystery Berry
1092
1055
  inline for (players) |player| {
1093
- var stored = battle.side(player).stored();
1056
+ const stored = battle.side(player).stored();
1094
1057
 
1095
1058
  if (stored.item == .MysteryBerry) {
1096
1059
  // TODO stored and active moves, transform, etc
@@ -1105,8 +1068,7 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1105
1068
  !side.active.volatiles.frozen and
1106
1069
  try Rolls.defrost(battle, player, options))
1107
1070
  {
1108
- const reason: CureStatus = .Message;
1109
- try options.log.curestatus(.{ battle.active(player), side.stored().status, reason });
1071
+ try options.log.curestatus(.{ battle.active(player), side.stored().status, .Message });
1110
1072
  side.stored().status = 0;
1111
1073
  }
1112
1074
  }
@@ -1147,7 +1109,7 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1147
1109
  stored.hp < stored.stats.hp;
1148
1110
  if (proc) {
1149
1111
  assert(stored.hp != 0);
1150
- try options.log.enditem(.{ ident, stored.item, EndItem.Eat });
1112
+ try options.log.enditem(.{ ident, stored.item, .Eat });
1151
1113
  stored.item = .None;
1152
1114
 
1153
1115
  const offset = num - @intFromEnum(Item.Berry);
@@ -1164,7 +1126,7 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1164
1126
 
1165
1127
  if (proc) {
1166
1128
  assert(Status.any(stored.status));
1167
- try options.log.enditem(.{ ident, stored.item, EndItem.Eat });
1129
+ try options.log.enditem(.{ ident, stored.item, .Eat });
1168
1130
  stored.item = .None;
1169
1131
 
1170
1132
  // FIXME wtf?
@@ -1172,21 +1134,21 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1172
1134
  // side.active.volatiles.Nightmare = false;
1173
1135
  if (stored.item == .MiracleBerry) {
1174
1136
  side.active.volatiles.Confusion = false;
1175
- try options.log.end(.{ ident, End.Confusion });
1137
+ try options.log.end(.{ ident, .Confusion });
1176
1138
  }
1177
1139
  stored.status = 0;
1178
1140
 
1179
1141
  // FIXME recalc stats
1180
1142
 
1181
- try options.log.curestatus(.{ ident, stored.status, CureStatus.Message });
1143
+ try options.log.curestatus(.{ ident, stored.status, .Message });
1182
1144
  }
1183
1145
  } else if (num == @intFromEnum(Item.BitterBerry)) {
1184
1146
  if (side.active.volatiles.Confusion) {
1185
- try options.log.enditem(.{ ident, stored.item, EndItem.Eat });
1147
+ try options.log.enditem(.{ ident, stored.item, .Eat });
1186
1148
  stored.item = .None;
1187
1149
 
1188
1150
  side.active.volatiles.Confusion = false;
1189
- try options.log.end(.{ ident, End.Confusion });
1151
+ try options.log.end(.{ ident, .Confusion });
1190
1152
  }
1191
1153
  }
1192
1154
  }
@@ -1198,12 +1160,11 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1198
1160
  var volatiles = &side.active.volatiles;
1199
1161
 
1200
1162
  if (volatiles.Encore) {
1201
- assert(volatiles.encore > 0);
1202
1163
  volatiles.encore -= 1;
1203
1164
 
1204
1165
  if (volatiles.encore == 0 or side.active.move(mslot).pp == 0) {
1205
1166
  volatiles.Encore = false;
1206
- try options.log.end(.{ battle.active(player), End.Encore });
1167
+ try options.log.end(.{ battle.active(player), .Encore });
1207
1168
  }
1208
1169
  }
1209
1170
  }
@@ -1220,7 +1181,6 @@ fn updateSideCondition(
1220
1181
  ) !void {
1221
1182
  var side = battle.side(player);
1222
1183
  if (@field(side.conditions, key)) {
1223
- assert(@field(side.conditions, value) > 0);
1224
1184
  @field(side.conditions, value) -= 1;
1225
1185
 
1226
1186
  if (@field(side.conditions, value) == 0) {
@@ -1233,7 +1193,7 @@ fn updateSideCondition(
1233
1193
  fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result {
1234
1194
  battle.turn += 1;
1235
1195
 
1236
- if (showdown and battle.turn >= 1000) {
1196
+ if (pkmn.options.mod and battle.turn >= 1000) {
1237
1197
  try options.log.tie(.{});
1238
1198
  return Result.Tie;
1239
1199
  } else if (battle.turn >= 65535) {
@@ -1256,7 +1216,7 @@ pub fn buildRage(battle: anytype, player: Player, state: *State, options: anytyp
1256
1216
  }
1257
1217
  } else {
1258
1218
  foe.active.volatiles.rage +|= 1;
1259
- try options.log.activate(battle.active(player), .Rage);
1219
+ try options.log.activate(.{ battle.active(player), .Rage });
1260
1220
  }
1261
1221
  }
1262
1222
  }
@@ -1279,7 +1239,7 @@ pub fn destinyBond(battle: anytype, player: Player, _: *State, options: anytype)
1279
1239
 
1280
1240
  if (foe.active.volatiles.DestinyBond) {
1281
1241
  battle.side(player).stored().hp = 0;
1282
- try options.log.activate(battle.active(player.foe()), .DestinyBond);
1242
+ try options.log.activate(.{ battle.active(player.foe()), .DestinyBond });
1283
1243
  }
1284
1244
  return true;
1285
1245
  }
@@ -1292,14 +1252,14 @@ pub const Effects = struct {
1292
1252
  const gender: u8 = @intFromEnum(side.stored().dvs.gender);
1293
1253
  const foe_gender: u8 = @intFromEnum(foe.stored().dvs.gender);
1294
1254
  if (gender + foe_gender != 1) {
1295
- return options.log.immune(.{ battle.active(player.foe()), Immune.None });
1255
+ return options.log.immune(.{ battle.active(player.foe()), .None });
1296
1256
  } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
1297
1257
  try options.log.lastmiss(.{});
1298
1258
  return options.log.miss(.{battle.active(player)});
1299
1259
  }
1300
1260
 
1301
1261
  foe.active.volatiles.Attract = true;
1302
- try options.log.start(battle.active(player.foe()), .Attract);
1262
+ try options.log.start(.{ battle.active(player.foe()), .Attract });
1303
1263
  }
1304
1264
 
1305
1265
  pub fn batonPass(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1311,14 +1271,32 @@ pub const Effects = struct {
1311
1271
  }
1312
1272
 
1313
1273
  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
- //
1274
+ assert(!state.miss);
1275
+ var side = battle.side(player);
1276
+
1277
+ const ident = battle.active(player);
1278
+
1279
+ assert(side.active.boosts.atk >= -6 and side.active.boosts.atk <= 6);
1280
+ if (side.active.boosts.atk == 6 or side.active.boosts.atk == MAX_STAT_VALUE) {
1281
+ return options.log.fail(.{ ident, .None });
1282
+ }
1283
+
1284
+ const hp = @max(side.stored().stats.hp / 2, 1);
1285
+ if (side.stored().hp <= hp) {
1286
+ try boostStat(battle, side, &side.active.stats, &side.active.boosts, "atk", 2);
1287
+ try options.log.boost(.{ ident, .Attack, @as(i8, 2), .Silent });
1288
+ return options.log.fail(.{ ident, .None });
1289
+ } else {
1290
+ side.stored().hp -= hp;
1291
+ try options.log.damage(.{ ident, side.stored(), .None });
1292
+
1293
+ const before = side.active.boosts.atk;
1294
+ var i: usize = 0;
1295
+ while (i < 6 and side.active.stats.atk < MAX_STAT_VALUE) : (i += 1) {
1296
+ try boostStat(battle, side, &side.active.stats, &side.active.boosts, "atk", 2);
1297
+ }
1298
+ try options.log.boost(.{ ident, .Attack, @as(i8, side.active.boosts.atk - before) });
1299
+ }
1322
1300
  }
1323
1301
 
1324
1302
  pub const bide = struct {
@@ -1330,18 +1308,11 @@ pub const Effects = struct {
1330
1308
 
1331
1309
  if (!volatiles.Bide) return;
1332
1310
 
1333
- assert(volatiles.attacks > 0);
1334
- // if (options.calc.modify(player, .attacking)) |extend| {
1335
- // if (!extend) volatiles.attacks = 0;
1336
- // } else {
1337
1311
  volatiles.attacks -= 1;
1338
- // }
1339
- try options.chance.attacking(player, volatiles.attacks);
1340
-
1341
- if (volatiles.attacks != 0) return options.log.activate(ident, .Bide);
1312
+ if (volatiles.attacks != 0) return options.log.activate(.{ ident, .Bide });
1342
1313
 
1343
1314
  volatiles.Bide = false;
1344
- try options.log.end(.{ ident, End.Bide });
1315
+ try options.log.end(.{ ident, .Bide });
1345
1316
 
1346
1317
  state.damage = volatiles.bide *% 2;
1347
1318
  volatiles.bide = 0;
@@ -1378,7 +1349,7 @@ pub const Effects = struct {
1378
1349
  try options.log.curestatus(.{
1379
1350
  battle.active(player.foe()),
1380
1351
  foe_stored.status,
1381
- CureStatus.Message,
1352
+ .Message,
1382
1353
  });
1383
1354
  foe_stored.status = 0;
1384
1355
  }
@@ -1395,8 +1366,7 @@ pub const Effects = struct {
1395
1366
  foe_stored.status = Status.init(.BRN);
1396
1367
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
1397
1368
 
1398
- const reason: protocol.Status = .None;
1399
- try options.log.status(.{ battle.active(player.foe()), foe_stored.status, reason });
1369
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
1400
1370
 
1401
1371
  // TODO: check for status berry
1402
1372
  }
@@ -1416,7 +1386,7 @@ pub const Effects = struct {
1416
1386
  for (side.active.moves) |m| {
1417
1387
  if (convertible(side.active, m.id)) n += 1;
1418
1388
  }
1419
- if (n == 0) return options.log.fail(.{ battle.active(player), Fail.None });
1389
+ if (n == 0) return options.log.fail(.{ battle.active(player), .None });
1420
1390
 
1421
1391
  side.active.types.type1 = try Rolls.conversion(battle, player, n, options);
1422
1392
  side.active.types.type2 = side.active.types.type1;
@@ -1426,9 +1396,9 @@ pub const Effects = struct {
1426
1396
  var side = battle.side(player);
1427
1397
 
1428
1398
  const last = battle.foe(player).lastMove(false);
1429
- if (last == .None) return options.log.fail(.{ battle.active(player), Fail.None });
1399
+ if (last == .None) return options.log.fail(.{ battle.active(player), .None });
1430
1400
  const move = Move.get(last);
1431
- if (move.type == .@"???") return options.log.fail(.{ battle.active(player), Fail.None });
1401
+ if (move.type == .@"???") return options.log.fail(.{ battle.active(player), .None });
1432
1402
 
1433
1403
  side.active.types.type1 = try Rolls.conversion2(battle, player, move.type, options);
1434
1404
  side.active.types.type2 = side.active.types.type1;
@@ -1457,7 +1427,8 @@ pub const Effects = struct {
1457
1427
  if (move.type.special() != (last == .MirrorCoat)) return;
1458
1428
  if (state.damage == 0) return;
1459
1429
 
1460
- state.damage *|= 2;
1430
+ // NOTE: llvm/llvm-project#58557
1431
+ state.damage = smul(state.damage, 2);
1461
1432
  state.miss = false;
1462
1433
  }
1463
1434
 
@@ -1473,38 +1444,38 @@ pub const Effects = struct {
1473
1444
  try options.log.lastmiss(.{});
1474
1445
  return options.log.miss(.{battle.active(player)});
1475
1446
  } else if (foe.active.volatiles.Substitute) {
1476
- return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
1447
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
1477
1448
  } else if (foe.active.volatiles.Curse) {
1478
- return options.log.fail(.{ foe_ident, Fail.None });
1449
+ return options.log.fail(.{ foe_ident, .None });
1479
1450
  }
1480
1451
 
1481
1452
  foe.active.volatiles.Curse = true;
1482
- try options.log.start(foe_ident, .Curse);
1453
+ try options.log.start(.{ foe_ident, .Curse });
1483
1454
 
1484
1455
  side.stored().hp -= @max(side.stored().stats.hp / 2, 1);
1485
- try options.log.damage(.{ ident, side.stored(), Damage.Curse });
1456
+ try options.log.damage(.{ ident, side.stored(), .Curse });
1486
1457
  } else {
1487
- var stats = &side.active.stats;
1488
- var boosts = &side.active.boosts;
1458
+ const stats = &side.active.stats;
1459
+ const boosts = &side.active.boosts;
1489
1460
 
1490
1461
  const max_atk = boosts.atk == 6 or stats.atk == MAX_STAT_VALUE;
1491
1462
  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 });
1463
+ if (max_atk and max_def) return options.log.fail(.{ ident, .None });
1493
1464
 
1494
1465
  if (!(boosts.spe == -6 or stats.spe == 1)) {
1495
1466
  assert(boosts.spe >= -6 and boosts.spe <= 6);
1496
1467
  try unboostStat(battle, side, stats, boosts, "spe", 1);
1497
- try options.log.boost(.{ ident, Boost.Speed, @as(i8, -1) });
1468
+ try options.log.boost(.{ ident, .Speed, @as(i8, -1) });
1498
1469
  }
1499
1470
  if (!max_atk) {
1500
1471
  assert(boosts.atk >= -6 and boosts.atk <= 6);
1501
1472
  try boostStat(battle, side, stats, boosts, "atk", 1);
1502
- try options.log.boost(.{ ident, Boost.Attack, @as(i8, 1) });
1473
+ try options.log.boost(.{ ident, .Attack, @as(i8, 1) });
1503
1474
  }
1504
1475
  if (!max_def) {
1505
1476
  assert(boosts.def >= -6 and boosts.def <= 6);
1506
1477
  try boostStat(battle, side, stats, boosts, "def", 1);
1507
- try options.log.boost(.{ ident, Boost.Defense, @as(i8, 1) });
1478
+ try options.log.boost(.{ ident, .Defense, @as(i8, 1) });
1508
1479
  }
1509
1480
  }
1510
1481
  }
@@ -1516,9 +1487,7 @@ pub const Effects = struct {
1516
1487
  pub fn defrost(battle: anytype, player: Player, _: *State, options: anytype) !void {
1517
1488
  var side = battle.side(player);
1518
1489
  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 });
1490
+ try options.log.curestatus(.{ battle.active(player), side.stored().status, .Message });
1522
1491
  side.stored().status = 0;
1523
1492
  }
1524
1493
 
@@ -1532,14 +1501,10 @@ pub const Effects = struct {
1532
1501
  var volatiles = &foe.active.volatiles;
1533
1502
  const foe_ident = battle.active(player.foe());
1534
1503
 
1535
- if (volatiles.disable.move != 0) {
1536
- return options.log.fail(.{ foe_ident, Fail.None });
1537
- }
1504
+ if (volatiles.disable.move != 0) return options.log.fail(.{ foe_ident, .None });
1538
1505
 
1539
1506
  const last = battle.foe(player).lastMove(false);
1540
- if (last == .None or last == .Struggle) {
1541
- return options.log.fail(.{ foe_ident, Fail.None });
1542
- }
1507
+ if (last == .None or last == .Struggle) return options.log.fail(.{ foe_ident, .None });
1543
1508
 
1544
1509
  var slot: u3 = 0;
1545
1510
  for (foe.active.moves) |m| {
@@ -1548,25 +1513,28 @@ pub const Effects = struct {
1548
1513
  if (m.id == last) break;
1549
1514
  }
1550
1515
  const move = foe.active.move(slot);
1551
- if (move.pp == 0) return options.log.fail(.{ foe_ident, Fail.None });
1516
+ if (move.pp == 0) return options.log.fail(.{ foe_ident, .None });
1552
1517
 
1553
1518
  volatiles.disable.move = slot;
1554
1519
  volatiles.disable.duration = Rolls.disableDuration(battle, player, options);
1555
1520
 
1556
- try options.log.startEffect(foe_ident, .Disable, move.id);
1521
+ try options.log.start(.{ foe_ident, .Disable, move.id });
1557
1522
  }
1558
1523
 
1559
1524
  pub const double = struct {
1560
1525
  pub fn flying(battle: anytype, player: Player, state: *State, _: anytype) !void {
1561
- if (battle.foe(player).active.volatiles.Flying) state.damage *|= 2;
1526
+ if (!battle.foe(player).active.volatiles.Flying) return;
1527
+ state.damage = smul(state.damage, 2);
1562
1528
  }
1563
1529
 
1564
1530
  pub fn underground(battle: anytype, player: Player, state: *State, _: anytype) !void {
1565
- if (battle.foe(player).active.volatiles.Underground) state.damage *|= 2;
1531
+ if (!battle.foe(player).active.volatiles.Underground) return;
1532
+ state.damage = smul(state.damage, 2);
1566
1533
  }
1567
1534
 
1568
1535
  pub fn minimize(battle: anytype, player: Player, state: *State, _: anytype) !void {
1569
- if (battle.foe(player).active.volatiles.minimized) state.damage *|= 2;
1536
+ if (!battle.foe(player).active.volatiles.minimized) return;
1537
+ state.damage = smul(state.damage, 2);
1570
1538
  }
1571
1539
  };
1572
1540
 
@@ -1579,7 +1547,7 @@ pub const Effects = struct {
1579
1547
  if (stored.hp == stored.stats.hp) return;
1580
1548
  stored.hp = @min(stored.stats.hp, stored.hp + @max(state.damage / 2, 1));
1581
1549
 
1582
- try options.log.heal(.{ ident, stored, Heal.Drain, battle.active(player.foe()) });
1550
+ try options.log.heal(.{ ident, stored, .Drain, battle.active(player.foe()) });
1583
1551
  }
1584
1552
 
1585
1553
  pub fn encore(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1591,7 +1559,7 @@ pub const Effects = struct {
1591
1559
  const failed = volatiles.Encore or
1592
1560
  (last == .None or last == .Struggle or
1593
1561
  last == .Encore or .last == .MirrorMove);
1594
- if (failed) return options.log.fail(.{ foe_ident, Fail.None });
1562
+ if (failed) return options.log.fail(.{ foe_ident, .None });
1595
1563
 
1596
1564
  var slot: u3 = 0;
1597
1565
  for (foe.active.moves) |m| {
@@ -1600,7 +1568,7 @@ pub const Effects = struct {
1600
1568
  if (m.id == last) break;
1601
1569
  }
1602
1570
  const move = foe.active.move(slot);
1603
- if (move.pp == 0) return options.log.fail(.{ foe_ident, Fail.None });
1571
+ if (move.pp == 0) return options.log.fail(.{ foe_ident, .None });
1604
1572
 
1605
1573
  volatiles.Encore = true;
1606
1574
  volatiles.encore = Rolls.encoreDuration(battle, player, options);
@@ -1608,11 +1576,19 @@ pub const Effects = struct {
1608
1576
  // TODO ???
1609
1577
  _ = state;
1610
1578
 
1611
- try options.log.startEffect(foe_ident, .Encore, move.id);
1579
+ try options.log.start(.{ foe_ident, .Encore, move.id });
1612
1580
  }
1613
1581
 
1614
- pub fn explode(battle: anytype, player: Player, state: *State, options: anytype) !void {
1615
- _ = .{ battle, player, state, options }; // TODO
1582
+ pub fn explode(battle: anytype, player: Player, _: *State, _: anytype) !void {
1583
+ var side = battle.side(player);
1584
+ var stored = side.stored();
1585
+
1586
+ stored.hp = 0;
1587
+ // Pokémon Showdown sets the status to 0 on faint(), and we need to do the same to be able
1588
+ // to correctly implement Pokémon Showdown's dumb speed-based switch mechanics
1589
+ if (!showdown) stored.status = 0;
1590
+ side.active.volatiles.LeechSeed = false;
1591
+ side.active.volatiles.DestinyBond = false;
1616
1592
  }
1617
1593
 
1618
1594
  pub fn falseSwipe(battle: anytype, player: Player, state: *State, _: anytype) !void {
@@ -1662,12 +1638,11 @@ pub const Effects = struct {
1662
1638
  var side = battle.side(player);
1663
1639
 
1664
1640
  if (side.active.volatiles.FocusEnergy) {
1665
- if (!showdown) try options.log.fail(.{ battle.active(player), Fail.None });
1666
- return;
1641
+ return if (!showdown) try options.log.fail(.{ battle.active(player), .None });
1667
1642
  }
1668
1643
 
1669
1644
  side.active.volatiles.FocusEnergy = true;
1670
- try options.log.start(battle.active(player), .FocusEnergy);
1645
+ try options.log.start(.{ battle.active(player), .FocusEnergy });
1671
1646
  }
1672
1647
 
1673
1648
  var SLOTS: [5]u4 = .{ 0, 0, 0, 0, 0 };
@@ -1677,7 +1652,7 @@ pub const Effects = struct {
1677
1652
 
1678
1653
  const foe = battle.foe(player);
1679
1654
 
1680
- if (state.first) return options.log.fail(.{ battle.active(player), Fail.None });
1655
+ if (state.first) return options.log.fail(.{ battle.active(player), .None });
1681
1656
 
1682
1657
  var n: u4 = 0;
1683
1658
  var i: u4 = 0;
@@ -1691,7 +1666,7 @@ pub const Effects = struct {
1691
1666
  }
1692
1667
  }
1693
1668
 
1694
- if (n == 0) return options.log.fail(.{ battle.active(player), Fail.None });
1669
+ if (n == 0) return options.log.fail(.{ battle.active(player), .None });
1695
1670
 
1696
1671
  const slot = try Rolls.forceSwitch(battle, player, SLOTS[0..i], n, options);
1697
1672
  try switchIn(battle, player.foe(), slot, .Drag, options);
@@ -1704,11 +1679,11 @@ pub const Effects = struct {
1704
1679
  try options.log.lastmiss(.{});
1705
1680
  return options.log.miss(.{battle.active(player)});
1706
1681
  } else if (foe.active.volatiles.Foresight) {
1707
- return options.log.fail(.{ battle.active(player), Fail.None });
1682
+ return options.log.fail(.{ battle.active(player), .None });
1708
1683
  }
1709
1684
 
1710
1685
  foe.active.volatiles.Foresight = true;
1711
- try options.log.start(battle.active(player.foe()), .FocusEnergy);
1686
+ try options.log.start(.{ battle.active(player.foe()), .FocusEnergy });
1712
1687
  }
1713
1688
 
1714
1689
  pub fn freezeChance(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1728,10 +1703,10 @@ pub const Effects = struct {
1728
1703
  if (!state.proc) return;
1729
1704
  if (foe.conditions.Safeguard) return;
1730
1705
  // Freeze Clause Mod
1731
- if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1706
+ if (pkmn.options.mod) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1732
1707
 
1733
1708
  foe_stored.status = Status.init(.FRZ);
1734
- try options.log.status(.{ foe_ident, foe_stored.status, protocol.Status.None });
1709
+ try options.log.status(.{ foe_ident, foe_stored.status, .None });
1735
1710
 
1736
1711
  // TODO: check for status berry
1737
1712
 
@@ -1739,8 +1714,26 @@ pub const Effects = struct {
1739
1714
  foe.active.volatiles.frozen = true;
1740
1715
  }
1741
1716
 
1742
- pub fn furyCutter(battle: anytype, player: Player, state: *State, options: anytype) !void {
1743
- _ = .{ battle, player, state, options }; // TODO
1717
+ // TODO FIXME showdown calls this before calcDamage (after checkHit) and modifies state.bp!
1718
+ pub fn furyCutter(battle: anytype, player: Player, state: *State, _: anytype) !void {
1719
+ var side = battle.side(player);
1720
+ var volatiles = &side.active.volatiles;
1721
+
1722
+ if (state.miss) {
1723
+ volatiles.fury_cutter = 0;
1724
+ return;
1725
+ }
1726
+
1727
+ assert(volatiles.fury_cutter >= 0 and volatiles.fury_cutter <= 5);
1728
+ if (volatiles.fury_cutter == 5) return;
1729
+ volatiles.fury_cutter += 1;
1730
+
1731
+ if (showdown) {
1732
+ state.bp *= 2;
1733
+ assert(state.bp <= 160);
1734
+ } else {
1735
+ state.damage = smul(state.damage, 2);
1736
+ }
1744
1737
  }
1745
1738
 
1746
1739
  pub fn futureSight(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1774,7 +1767,7 @@ pub const Effects = struct {
1774
1767
  var stored = side.stored();
1775
1768
  const ident = battle.active(player);
1776
1769
 
1777
- if (stored.stats.hp == stored.hp) return options.log.fail(.{ ident, Fail.None });
1770
+ if (stored.stats.hp == stored.hp) return options.log.fail(.{ ident, .None });
1778
1771
 
1779
1772
  const rest = state.move == .Rest;
1780
1773
  if (rest) {
@@ -1783,7 +1776,7 @@ pub const Effects = struct {
1783
1776
  side.active.volatiles.Toxic = false;
1784
1777
  stored.status = Status.slf(2);
1785
1778
  recomputeStats(battle, side);
1786
- try options.log.status(.{ ident, stored.status, protocol.Status.From, Move.Rest });
1779
+ try options.log.status(.{ ident, stored.status, .From, Move.Rest });
1787
1780
  stored.hp = stored.stats.hp;
1788
1781
  } else {
1789
1782
  stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
@@ -1821,7 +1814,7 @@ pub const Effects = struct {
1821
1814
  // Invulnerability trumps type immunity on Pokémon Showdown
1822
1815
  const invulnerable = foe.active.volatiles.Underground or foe.active.volatiles.Flying;
1823
1816
  if (!invulnerable and foe.active.types.includes(.Grass)) {
1824
- return options.log.immune(.{ battle.active(player.foe()), Immune.None });
1817
+ return options.log.immune(.{ battle.active(player.foe()), .None });
1825
1818
  }
1826
1819
  try checkHit(battle, player, state, options);
1827
1820
  if (state.miss) {
@@ -1841,7 +1834,7 @@ pub const Effects = struct {
1841
1834
 
1842
1835
  foe.active.volatiles.LeechSeed = true;
1843
1836
 
1844
- try options.log.start(battle.active(player.foe()), .LeechSeed);
1837
+ try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
1845
1838
  }
1846
1839
 
1847
1840
  pub fn lockOn(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1849,20 +1842,20 @@ pub const Effects = struct {
1849
1842
  const foe_ident = battle.active(player.foe());
1850
1843
 
1851
1844
  if (foe.active.volatiles.Substitute) {
1852
- return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
1845
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
1853
1846
  } else if (state.miss) {
1854
- return options.log.fail(.{ foe_ident, Fail.None });
1847
+ return options.log.fail(.{ foe_ident, .None });
1855
1848
  }
1856
1849
 
1857
1850
  foe.active.volatiles.LockOn = true;
1858
- try options.log.activate(battle.active(player), .LockOn);
1851
+ try options.log.activate(.{ battle.active(player), .LockOn });
1859
1852
  }
1860
1853
 
1861
1854
  const MAGNITUDE_POWER = [_]u8{ 10, 30, 50, 70, 90, 110, 150 };
1862
1855
 
1863
1856
  pub fn magnitude(battle: anytype, player: Player, state: *State, options: anytype) !void {
1864
1857
  const num = try Rolls.magnitude(battle, player, options);
1865
- try options.log.magnitude(battle.active(player), num);
1858
+ try options.log.activate(.{ battle.active(player), .Magnitude, num });
1866
1859
  state.bp = MAGNITUDE_POWER[num - 4];
1867
1860
  }
1868
1861
 
@@ -1870,11 +1863,11 @@ pub const Effects = struct {
1870
1863
  var foe = battle.foe(player);
1871
1864
 
1872
1865
  if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
1873
- if (!showdown) return options.log.fail(.{ battle.active(player.foe()), Fail.None });
1866
+ if (!showdown) return options.log.fail(.{ battle.active(player.foe()), .None });
1874
1867
  try options.log.lastmiss(.{});
1875
1868
  return options.log.miss(.{battle.active(player)});
1876
1869
  } else if (foe.active.volatiles.Trapped) {
1877
- return options.log.fail(.{ battle.active(player.foe()), Fail.None });
1870
+ return options.log.fail(.{ battle.active(player.foe()), .None });
1878
1871
  }
1879
1872
 
1880
1873
  foe.active.volatiles.Trapped = true;
@@ -1901,7 +1894,29 @@ pub const Effects = struct {
1901
1894
  }
1902
1895
 
1903
1896
  pub fn mimic(battle: anytype, player: Player, state: *State, options: anytype) !void {
1904
- _ = .{ battle, player, state, options }; // TODO
1897
+ var side = battle.side(player);
1898
+
1899
+ if (state.miss) {
1900
+ if (!showdown) return options.log.fail(.{ battle.active(player.foe()), .None });
1901
+ try options.log.lastmiss(.{});
1902
+ return options.log.miss(.{battle.active(player)});
1903
+ }
1904
+
1905
+ const move = battle.foe(player).lastMove(false);
1906
+ if (move == .None or move == .Struggle) {
1907
+ return options.log.fail(.{ battle.active(player.foe()), .None });
1908
+ }
1909
+
1910
+ var slot: u4 = 0;
1911
+ for (side.active.moves, 0..) |m, i| {
1912
+ if (m.id == move) return options.log.fail(.{ battle.active(player.foe()), .None });
1913
+ if (m.id == .Mimic) slot = @intCast(i + 1);
1914
+ }
1915
+ assert(slot != 0 and slot == state.mslot);
1916
+
1917
+ side.active.move(slot).id = move;
1918
+ side.active.move(slot).pp = 5;
1919
+ try options.log.start(.{ battle.active(player), .Mimic, move });
1905
1920
  }
1906
1921
 
1907
1922
  pub fn mirrorMove(
@@ -1918,11 +1933,11 @@ pub const Effects = struct {
1918
1933
 
1919
1934
  const last = foe.lastMove(false);
1920
1935
 
1921
- if (last == .None) return options.log.fail(.{ battle.active(player), Fail.None });
1936
+ if (last == .None) return options.log.fail(.{ battle.active(player), .None });
1922
1937
  const knows = knows: for (side.active.moves) |m| {
1923
1938
  if (last == m.id) break :knows true;
1924
1939
  } else false;
1925
- if (knows) return options.log.fail(.{ battle.active(player), Fail.None });
1940
+ if (knows) return options.log.fail(.{ battle.active(player), .None });
1926
1941
 
1927
1942
  // if (wPlayerCharging) ???
1928
1943
  // wPlayerCharging = 1
@@ -1936,19 +1951,17 @@ pub const Effects = struct {
1936
1951
  pub fn mist(battle: anytype, player: Player, _: *State, options: anytype) !void {
1937
1952
  var side = battle.side(player);
1938
1953
 
1939
- if (side.active.volatiles.Mist) {
1940
- return options.log.fail(.{ battle.active(player), Fail.None });
1941
- }
1954
+ if (side.active.volatiles.Mist) return options.log.fail(.{ battle.active(player), .None });
1942
1955
  side.active.volatiles.Mist = true;
1943
1956
 
1944
- try options.log.start(battle.active(player), .Mist);
1957
+ try options.log.start(.{ battle.active(player), .Mist });
1945
1958
  }
1946
1959
 
1947
1960
  pub fn weatherHeal(battle: anytype, player: Player, _: *State, options: anytype) !void {
1948
1961
  var stored = battle.side(player).stored();
1949
1962
  const ident = battle.active(player);
1950
1963
 
1951
- if (stored.hp == stored.stats.hp) return options.log.fail(.{ ident, Fail.None });
1964
+ if (stored.hp == stored.stats.hp) return options.log.fail(.{ ident, .None });
1952
1965
 
1953
1966
  stored.hp = switch (battle.field.weather) {
1954
1967
  .Sun => stored.stats.hp,
@@ -1956,7 +1969,7 @@ pub const Effects = struct {
1956
1969
  else => @min(stored.stats.hp, stored.hp + (stored.stats.hp / 4)),
1957
1970
  };
1958
1971
 
1959
- try options.log.heal(.{ ident, stored, Heal.None });
1972
+ try options.log.heal(.{ ident, stored, .None });
1960
1973
  }
1961
1974
 
1962
1975
  pub fn multiHit(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1969,17 +1982,17 @@ pub const Effects = struct {
1969
1982
  const foe_ident = battle.active(player.foe());
1970
1983
 
1971
1984
  if (foe_volatiles.Flying or foe_volatiles.Underground) {
1972
- if (!showdown) return options.log.fail(.{ battle.active(player.foe()), Fail.None });
1985
+ if (!showdown) return options.log.fail(.{ battle.active(player.foe()), .None });
1973
1986
  try options.log.lastmiss(.{});
1974
1987
  return options.log.miss(.{battle.active(player)});
1975
1988
  } else if (foe_volatiles.Substitute) {
1976
- return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
1989
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
1977
1990
  } else if (!Status.is(foe.stored().status, .SLP) or foe_volatiles.Nightmare) {
1978
- return options.log.fail(.{ foe_ident, Fail.None });
1991
+ return options.log.fail(.{ foe_ident, .None });
1979
1992
  }
1980
1993
 
1981
1994
  foe_volatiles.Nightmare = true;
1982
- try options.log.start(battle.active(player.foe()), .Nightmare);
1995
+ try options.log.start(.{ battle.active(player.foe()), .Nightmare });
1983
1996
  }
1984
1997
 
1985
1998
  pub const ohko = struct {
@@ -2003,9 +2016,9 @@ pub const Effects = struct {
2003
2016
  const foe_ident = battle.active(player.foe());
2004
2017
 
2005
2018
  state.damage = 0;
2006
- if (state.immune()) return options.log.immune(.{ foe_ident, Immune.None });
2019
+ if (state.immune()) return options.log.immune(.{ foe_ident, .None });
2007
2020
  const delta = side.stored().level -| foe.stored().level;
2008
- if (delta == 0) return options.log.immune(.{ foe_ident, Immune.OHKO });
2021
+ if (delta == 0) return options.log.immune(.{ foe_ident, .OHKO });
2009
2022
 
2010
2023
  state.accuracy = 2 * delta +| Gen12.percent(30);
2011
2024
  try checkHit(battle, player, state, options);
@@ -2021,17 +2034,17 @@ pub const Effects = struct {
2021
2034
  const ident = battle.active(player);
2022
2035
  const foe_ident = battle.active(player.foe());
2023
2036
 
2024
- if (state.miss) return options.log.fail(.{ foe_ident, Fail.None });
2037
+ if (state.miss) return options.log.fail(.{ foe_ident, .None });
2025
2038
  if (foe.active.volatiles.Substitute) {
2026
- return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
2039
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
2027
2040
  }
2028
2041
 
2029
2042
  const avg = @max((foe.stored().hp + side.stored().hp) / 2, 1);
2030
2043
  foe.stored().hp -= avg;
2031
2044
  side.stored().hp = avg;
2032
2045
 
2033
- try options.log.sethp(.{ foe_ident, foe.stored(), SetHP.Silent });
2034
- try options.log.sethp(.{ ident, side.stored(), SetHP.None });
2046
+ try options.log.sethp(.{ foe_ident, foe.stored(), .Silent });
2047
+ try options.log.sethp(.{ ident, side.stored(), .None });
2035
2048
  }
2036
2049
 
2037
2050
  pub fn paralyze(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -2039,21 +2052,21 @@ pub const Effects = struct {
2039
2052
  const foe_ident = battle.active(player.foe());
2040
2053
 
2041
2054
  if (Status.is(foe.stored().status, .PAR)) {
2042
- return options.log.fail(.{ foe_ident, Fail.Paralysis });
2055
+ return options.log.fail(.{ foe_ident, .Paralysis });
2043
2056
  }
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 });
2057
+ if (state.immune()) return options.log.immune(.{ foe_ident, .None });
2058
+ if (Status.any(foe.stored().status)) return options.log.fail(.{ foe_ident, .None });
2046
2059
  if (state.miss) {
2047
2060
  try options.log.lastmiss(.{});
2048
2061
  return options.log.miss(.{battle.active(player)});
2049
2062
  } else if (foe.active.volatiles.Substitute) {
2050
- return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
2063
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
2051
2064
  }
2052
2065
 
2053
2066
  foe.stored().status = Status.init(.PAR);
2054
2067
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2055
2068
 
2056
- try options.log.status(.{ foe_ident, foe.stored().status, protocol.Status.None });
2069
+ try options.log.status(.{ foe_ident, foe.stored().status, .None });
2057
2070
  }
2058
2071
 
2059
2072
  pub fn paralyzeChance(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -2069,8 +2082,7 @@ pub const Effects = struct {
2069
2082
  foe_stored.status = Status.init(.PAR);
2070
2083
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2071
2084
 
2072
- const reason: protocol.Status = .None;
2073
- try options.log.status(.{ battle.active(player.foe()), foe_stored.status, reason });
2085
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
2074
2086
 
2075
2087
  // TODO: check for status berry
2076
2088
  }
@@ -2086,15 +2098,15 @@ pub const Effects = struct {
2086
2098
  var foe = battle.foe(player);
2087
2099
 
2088
2100
  if (foe.active.volatiles.PerishSong) {
2089
- return options.log.fail(.{ battle.active(player.foe()), Fail.None });
2101
+ return options.log.fail(.{ battle.active(player.foe()), .None });
2090
2102
  } else if (!side.active.volatiles.PerishSong) {
2091
2103
  side.active.volatiles.PerishSong = true;
2092
2104
  side.active.volatiles.perish_song = 4;
2093
- try options.log.start(battle.active(player), .PerishSong3Silent);
2105
+ try options.log.start(.{ battle.active(player), .PerishSong3Silent });
2094
2106
  }
2095
2107
  foe.active.volatiles.PerishSong = true;
2096
2108
  foe.active.volatiles.perish_song = 4;
2097
- try options.log.start(battle.active(player.foe()), .PerishSong3Silent);
2109
+ try options.log.start(.{ battle.active(player.foe()), .PerishSong3Silent });
2098
2110
 
2099
2111
  try options.log.fieldactivate(.{}); // FIXME .PerishSong
2100
2112
  }
@@ -2106,7 +2118,7 @@ pub const Effects = struct {
2106
2118
 
2107
2119
  const toxic = state.move == .Toxic;
2108
2120
  if (state.immune() or foe.active.types.includes(.Poison)) {
2109
- return options.log.immune(.{ foe_ident, Immune.None });
2121
+ return options.log.immune(.{ foe_ident, .None });
2110
2122
  } else if (Status.any(foe_stored.status)) {
2111
2123
  // Pokémon Showdown considers Toxic to be a status even in Generation II and so
2112
2124
  // will not include a fail reason for Toxic vs. Poison or vice-versa...
@@ -2120,7 +2132,7 @@ pub const Effects = struct {
2120
2132
  else
2121
2133
  Fail.None });
2122
2134
  } else if (foe.active.volatiles.Substitute) {
2123
- return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
2135
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
2124
2136
  } else if (state.miss) {
2125
2137
  try options.log.lastmiss(.{});
2126
2138
  return options.log.miss(.{battle.active(player)});
@@ -2133,7 +2145,7 @@ pub const Effects = struct {
2133
2145
  foe.active.volatiles.toxic = 0;
2134
2146
  }
2135
2147
 
2136
- try options.log.status(.{ foe_ident, foe_stored.status, protocol.Status.None });
2148
+ try options.log.status(.{ foe_ident, foe_stored.status, .None });
2137
2149
 
2138
2150
  // TODO: check for status berry
2139
2151
  }
@@ -2149,8 +2161,8 @@ pub const Effects = struct {
2149
2161
  if (foe.conditions.Safeguard) return;
2150
2162
 
2151
2163
  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 });
2164
+
2165
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
2154
2166
 
2155
2167
  // TODO: check for status berry
2156
2168
  }
@@ -2165,9 +2177,7 @@ pub const Effects = struct {
2165
2177
 
2166
2178
  const failed = !state.first or volatiles.Substitute or
2167
2179
  !try Rolls.protect(battle, player, volatiles.protect, options);
2168
- if (failed) {
2169
- return options.log.fail(.{ battle.active(player), Fail.None });
2170
- }
2180
+ if (failed) return options.log.fail(.{ battle.active(player), .None });
2171
2181
 
2172
2182
  volatiles.protect = @max(8, volatiles.protect + 1);
2173
2183
  if (state.move == .Endure) {
@@ -2184,7 +2194,7 @@ pub const Effects = struct {
2184
2194
 
2185
2195
  // Pokémon Showdown should fail when the enemy has no boosts/unboosts but doesn't
2186
2196
  if (!showdown and @as(u32, @bitCast(foe.active.boosts)) == 0) {
2187
- return options.log.fail(.{ battle.active(player), Fail.None });
2197
+ return options.log.fail(.{ battle.active(player), .None });
2188
2198
  }
2189
2199
 
2190
2200
  side.active.boosts = foe.active.boosts;
@@ -2193,7 +2203,7 @@ pub const Effects = struct {
2193
2203
 
2194
2204
  pub fn pursuit(battle: anytype, player: Player, state: *State, _: anytype) !void {
2195
2205
  // TODO: remove switching variable and just look at Choice saved in Battle struct?
2196
- if (battle.foe(player).active.volatiles.switching) state.damage *|= 2;
2206
+ if (battle.foe(player).active.volatiles.switching) state.damage = smul(state.damage, 2);
2197
2207
  }
2198
2208
 
2199
2209
  pub const rage = struct {
@@ -2203,11 +2213,10 @@ pub const Effects = struct {
2203
2213
  }
2204
2214
 
2205
2215
  pub fn damage(battle: anytype, player: Player, state: *State, _: anytype) !void {
2206
- var side = battle.side(player);
2207
- var volatiles = &side.active.volatiles;
2216
+ const side = battle.side(player);
2208
2217
 
2209
- assert(volatiles.Rage);
2210
- state.damage *|= (volatiles.rage +| 1);
2218
+ assert(side.active.volatiles.Rage);
2219
+ state.damage = smul(state.damage, (side.active.volatiles.rage +| 1)); // TODO: |+
2211
2220
  }
2212
2221
  };
2213
2222
 
@@ -2219,7 +2228,7 @@ pub const Effects = struct {
2219
2228
  var side = battle.side(player);
2220
2229
 
2221
2230
  if (side.active.volatiles.LeechSeed) {
2222
- try options.log.end(.{ battle.active(player), End.LeechSeedFrom });
2231
+ try options.log.end(.{ battle.active(player), .LeechSeedFrom });
2223
2232
  side.active.volatiles.LeechSeed = false;
2224
2233
  }
2225
2234
 
@@ -2238,18 +2247,14 @@ pub const Effects = struct {
2238
2247
  pub fn recoil(battle: anytype, player: Player, state: *State, options: anytype) !void {
2239
2248
  var side = battle.side(player);
2240
2249
  var stored = side.stored();
2250
+ const ident = battle.active(player);
2241
2251
 
2242
2252
  assert(state.damage > 0);
2243
2253
 
2244
2254
  const damage: i16 = @intCast(@max(state.damage / 4, 1));
2245
2255
  stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
2246
2256
 
2247
- try options.log.damage(.{
2248
- battle.active(player),
2249
- stored,
2250
- Damage.RecoilOf,
2251
- battle.active(player.foe()),
2252
- });
2257
+ try options.log.damage(.{ ident, stored, .RecoilOf, battle.active(player.foe()) });
2253
2258
  }
2254
2259
 
2255
2260
  pub fn reversal(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -2306,13 +2311,13 @@ pub const Effects = struct {
2306
2311
  side.active.volatiles.attacks += 1;
2307
2312
  side.active.volatiles.Rollout = side.active.volatiles.attacks != 5;
2308
2313
 
2309
- if (side.active.volatiles.DefenseCurl) state.damage *|= 2;
2314
+ if (side.active.volatiles.DefenseCurl) state.damage = smul(state.damage, 2);
2310
2315
  }
2311
2316
  };
2312
2317
 
2313
2318
  pub fn sandstorm(battle: anytype, player: Player, _: *State, options: anytype) !void {
2314
2319
  if (battle.field.weather != .Sandstorm) return weather(battle, .Sandstorm, options);
2315
- return options.log.fail(.{ battle.active(player), Fail.None });
2320
+ return options.log.fail(.{ battle.active(player), .None });
2316
2321
  }
2317
2322
 
2318
2323
  pub fn safeguard(battle: anytype, player: Player, _: *State, options: anytype) !void {
@@ -2337,7 +2342,7 @@ pub const Effects = struct {
2337
2342
  var side = battle.side(player);
2338
2343
 
2339
2344
  if (@field(side.conditions, key)) {
2340
- return options.log.fail(.{ battle.active(player), Fail.None });
2345
+ return options.log.fail(.{ battle.active(player), .None });
2341
2346
  }
2342
2347
 
2343
2348
  @field(side.conditions, key) = true;
@@ -2353,9 +2358,9 @@ pub const Effects = struct {
2353
2358
 
2354
2359
  if (showdown) {
2355
2360
  // yes, actually "Splash". '|nothing' gets blindly mapped to '|-activate||move:Splash'
2356
- try options.log.activate(battle.active(player), .Splash);
2361
+ try options.log.activate(.{ battle.active(player), .Splash });
2357
2362
  } else {
2358
- try options.log.fail(.{ battle.active(player), Fail.None });
2363
+ try options.log.fail(.{ battle.active(player), .None });
2359
2364
  }
2360
2365
  }
2361
2366
 
@@ -2372,33 +2377,32 @@ pub const Effects = struct {
2372
2377
  }
2373
2378
 
2374
2379
  if (foe.active.volatiles.Substitute) {
2375
- return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
2380
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
2376
2381
  }
2377
2382
 
2378
2383
  // Sleep Clause Mod
2379
- if (showdown) {
2384
+ if (pkmn.options.mod) {
2380
2385
  for (foe.pokemon) |p| {
2381
2386
  if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
2382
2387
  }
2383
2388
  }
2384
2389
 
2385
2390
  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 });
2391
+ try options.log.status(.{ foe_ident, foe_stored.status, .From, state.move });
2387
2392
  }
2388
2393
 
2389
2394
  pub fn sleepTalk(battle: anytype, player: Player, state: *State, options: anytype) !void {
2390
2395
  const side = battle.side(player);
2391
2396
 
2392
2397
  if (!Status.is(side.stored().status, .SLP)) {
2393
- if (!showdown) try options.log.fail(.{ battle.active(player), Fail.None });
2394
- return;
2398
+ return if (!showdown) try options.log.fail(.{ battle.active(player), .None });
2395
2399
  }
2396
2400
 
2397
2401
  var n: u3 = 0;
2398
2402
  for (side.active.moves, 0..) |m, i| {
2399
2403
  if (state.mslot - 1 != i and Move.get(m.id).extra.sleep_talk) n += 1;
2400
2404
  }
2401
- if (n == 0) return options.log.fail(.{ battle.active(player), Fail.None });
2405
+ if (n == 0) return options.log.fail(.{ battle.active(player), .None });
2402
2406
 
2403
2407
  _ = try Rolls.sleepTalk(battle, player, n, state.mslot, options); // TODO
2404
2408
  }
@@ -2407,55 +2411,75 @@ pub const Effects = struct {
2407
2411
  if (Status.is(battle.side(player).stored().status, .SLP)) return true;
2408
2412
  state.damage = 0;
2409
2413
  state.miss = true; // TODO is this necessary?
2410
- if (!showdown) try options.log.fail(.{ battle.active(player), Fail.None });
2414
+ if (!showdown) try options.log.fail(.{ battle.active(player), .None });
2411
2415
  return false;
2412
2416
  }
2413
2417
 
2414
2418
  pub fn spikes(battle: anytype, player: Player, _: *State, options: anytype) !void {
2415
2419
  var foe = battle.foe(player);
2416
- if (foe.conditions.Spikes) return options.log.fail(.{ battle.active(player), Fail.None });
2420
+ if (foe.conditions.Spikes) return options.log.fail(.{ battle.active(player), .None });
2417
2421
 
2418
2422
  foe.conditions.Spikes = true;
2419
2423
  try options.log.sidestart(.{ player.foe(), protocol.Side.Spikes });
2420
2424
  }
2421
2425
 
2422
2426
  pub fn spite(battle: anytype, player: Player, state: *State, options: anytype) !void {
2423
- _ = .{ battle, player, state, options }; // TODO
2424
- // var foe = battle.foe(player);
2427
+ var foe = battle.foe(player);
2425
2428
 
2426
- // if (state.miss) return options.log.fail(.{ battle.active(player), Fail.None });
2429
+ const last = foe.lastMove(false);
2430
+ if (state.miss or last == .None or last == .Struggle) {
2431
+ return options.log.fail(.{ battle.active(player.foe()), .None });
2432
+ }
2427
2433
 
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
- // }
2434
+ var slot: u4 = 0;
2435
+ for (foe.active.moves, 0..) |m, i| {
2436
+ if (m.id == last) slot = @intCast(i + 1);
2437
+ }
2438
+ // If the move was called via Metronome it might not exist
2439
+ if (slot == 0 or foe.active.move(slot).pp == 0) {
2440
+ return options.log.fail(.{ battle.active(player.foe()), .None });
2441
+ }
2442
+
2443
+ // Pokémon Showdown always deducts 4 instead of randomizing
2444
+ const pp = @min(
2445
+ foe.active.move(slot).pp,
2446
+ if (showdown) 4 else try Rolls.spite(battle, player, options),
2447
+ );
2448
+ foe.active.move(slot).pp -= pp;
2449
+ if (!foe.active.volatiles.Transform) {
2450
+ var move_slot = foe.stored().move(slot);
2451
+ assert(move_slot.pp > 0);
2452
+
2453
+ move_slot.pp = @as(u6, @intCast(move_slot.pp)) -% 1;
2454
+ assert(foe.active.move(slot).pp == foe.stored().move(slot).pp);
2455
+ }
2456
+
2457
+ try options.log.activate(.{ battle.active(player.foe()), .Spite, last, pp });
2434
2458
  }
2435
2459
 
2436
2460
  pub fn splash(battle: anytype, player: Player, _: *State, options: anytype) !void {
2437
- try options.log.activate(battle.active(player), .Splash);
2461
+ try options.log.activate(.{ battle.active(player), .Splash });
2438
2462
  }
2439
2463
 
2440
2464
  pub fn substitute(battle: anytype, player: Player, _: *State, options: anytype) !void {
2441
2465
  var side = battle.side(player);
2442
2466
  if (side.active.volatiles.Substitute) {
2443
- return options.log.fail(.{ battle.active(player), Fail.Substitute });
2467
+ return options.log.fail(.{ battle.active(player), .Substitute });
2444
2468
  }
2445
2469
 
2446
2470
  assert(side.stored().stats.hp <= 1023);
2447
2471
  const hp: u8 = @intCast(side.stored().stats.hp / 4);
2448
2472
 
2449
2473
  if (hp == 0 or side.stored().hp <= hp) {
2450
- return options.log.fail(.{ battle.active(player), Fail.Weak });
2474
+ return options.log.fail(.{ battle.active(player), .Weak });
2451
2475
  }
2452
2476
 
2453
2477
  side.stored().hp -= hp;
2454
2478
  side.active.volatiles.substitute = hp;
2455
2479
  side.active.volatiles.Substitute = true;
2456
2480
  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 });
2481
+ try options.log.start(.{ battle.active(player), .Substitute });
2482
+ try options.log.damage(.{ battle.active(player), side.stored(), .None });
2459
2483
  }
2460
2484
 
2461
2485
  pub fn sunnyDay(battle: anytype, _: Player, _: *State, options: anytype) !void {
@@ -2471,7 +2495,7 @@ pub const Effects = struct {
2471
2495
  pub fn teleport(battle: anytype, player: Player, _: *State, options: anytype) !void {
2472
2496
  if (showdown) return;
2473
2497
 
2474
- try options.log.fail(.{ battle.active(player), Fail.None });
2498
+ try options.log.fail(.{ battle.active(player), .None });
2475
2499
  try options.log.laststill(.{});
2476
2500
  }
2477
2501
 
@@ -2513,9 +2537,9 @@ pub const Effects = struct {
2513
2537
  side.active.volatiles.dirty = false;
2514
2538
 
2515
2539
  if (foe.active.volatiles.Transform) {
2516
- return options.log.fail(.{ battle.active(player), Fail.None });
2540
+ return options.log.fail(.{ battle.active(player), .None });
2517
2541
  } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2518
- if (!showdown) return options.log.fail(.{ battle.active(player), Fail.None });
2542
+ if (!showdown) return options.log.fail(.{ battle.active(player), .None });
2519
2543
  try options.log.lastmiss(.{});
2520
2544
  return options.log.miss(.{battle.active(player)});
2521
2545
  }
@@ -2564,8 +2588,8 @@ pub const Effects = struct {
2564
2588
  pub fn allStatUpChance(battle: anytype, player: Player, state: *State, options: anytype) !void {
2565
2589
  var side = battle.side(player);
2566
2590
 
2567
- var stats = &side.active.stats;
2568
- var boosts = &side.active.boosts;
2591
+ const stats = &side.active.stats;
2592
+ const boosts = &side.active.boosts;
2569
2593
 
2570
2594
  if (!state.proc) return;
2571
2595
  inline for (STATS, STAT_UP_REASONS) |s, r| {
@@ -2584,9 +2608,9 @@ pub const Effects = struct {
2584
2608
  var side = battle.side(player);
2585
2609
  const ident = battle.active(player);
2586
2610
 
2587
- if (state.miss) return log.fail(.{ ident, Fail.None });
2611
+ if (state.miss) return log.fail(.{ ident, .None });
2588
2612
 
2589
- var stats = &side.active.stats;
2613
+ const stats = &side.active.stats;
2590
2614
  var boosts = &side.active.boosts;
2591
2615
 
2592
2616
  const move = Move.get(state.move);
@@ -2594,7 +2618,7 @@ pub const Effects = struct {
2594
2618
  .AttackUp1, .AttackUp2, .Rage, .Swagger => {
2595
2619
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2596
2620
  if (boosts.atk == 6 or stats.atk == MAX_STAT_VALUE) {
2597
- return log.fail(.{ ident, Fail.None });
2621
+ return log.fail(.{ ident, .None });
2598
2622
  }
2599
2623
  const n: u2 = if (move.effect == .AttackUp2 or move.effect == .Swagger) 2 else 1;
2600
2624
  try boostStat(battle, side, stats, boosts, "atk", n);
@@ -2604,41 +2628,41 @@ pub const Effects = struct {
2604
2628
  .DefenseUp1, .DefenseUp2, .SkullBash => {
2605
2629
  assert(boosts.def >= -6 and boosts.def <= 6);
2606
2630
  if (boosts.def == 6 or stats.def == MAX_STAT_VALUE) {
2607
- return log.fail(.{ ident, Fail.None });
2631
+ return log.fail(.{ ident, .None });
2608
2632
  }
2609
2633
  const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
2610
2634
  try boostStat(battle, side, stats, boosts, "def", n);
2611
- try log.boost(.{ ident, Boost.Defense, n });
2635
+ try log.boost(.{ ident, .Defense, n });
2612
2636
  },
2613
2637
  .SpeedUp2 => {
2614
2638
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2615
2639
  if (boosts.spe == 6 or stats.spe == MAX_STAT_VALUE) {
2616
- return log.fail(.{ ident, Fail.None });
2640
+ return log.fail(.{ ident, .None });
2617
2641
  }
2618
2642
  try boostStat(battle, side, stats, boosts, "spe", 2);
2619
- try log.boost(.{ ident, Boost.Speed, 2 });
2643
+ try log.boost(.{ ident, .Speed, 2 });
2620
2644
  },
2621
2645
  .SpAtkUp1 => {
2622
2646
  assert(boosts.spa >= -6 and boosts.spa <= 6);
2623
2647
  if (boosts.spa == 6 or stats.spa == MAX_STAT_VALUE) {
2624
- return log.fail(.{ ident, Fail.None });
2648
+ return log.fail(.{ ident, .None });
2625
2649
  }
2626
2650
  try boostStat(battle, side, stats, boosts, "spa", 1);
2627
- try log.boost(.{ ident, Boost.SpecialAttack, 1 });
2651
+ try log.boost(.{ ident, .SpecialAttack, 1 });
2628
2652
  },
2629
2653
  .SpDefUp2 => {
2630
2654
  assert(boosts.spd >= -6 and boosts.spd <= 6);
2631
2655
  if (boosts.spd == 6 or stats.spd == MAX_STAT_VALUE) {
2632
- return log.fail(.{ ident, Fail.None });
2656
+ return log.fail(.{ ident, .None });
2633
2657
  }
2634
2658
  try boostStat(battle, side, stats, boosts, "spd", 2);
2635
- try log.boost(.{ ident, Boost.SpecialDefense, 2 });
2659
+ try log.boost(.{ ident, .SpecialDefense, 2 });
2636
2660
  },
2637
2661
  .EvasionUp1 => {
2638
2662
  assert(boosts.evasion >= -6 and boosts.evasion <= 6);
2639
- if (boosts.evasion == 6) return log.fail(.{ ident, Fail.None });
2663
+ if (boosts.evasion == 6) return log.fail(.{ ident, .None });
2640
2664
  boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
2641
- try log.boost(.{ ident, Boost.Evasion, 1 });
2665
+ try log.boost(.{ ident, .Evasion, 1 });
2642
2666
  if (state.move == .Minimize) side.active.volatiles.minimized = true;
2643
2667
  },
2644
2668
  else => unreachable,
@@ -2651,13 +2675,13 @@ pub const Effects = struct {
2651
2675
  const foe_ident = battle.active(player.foe());
2652
2676
 
2653
2677
  if (foe.active.volatiles.Mist) {
2654
- try log.activate(foe_ident, .Mist);
2655
- return log.fail(.{ foe_ident, Fail.None });
2678
+ try log.activate(.{ foe_ident, .Mist });
2679
+ return log.fail(.{ foe_ident, .None });
2656
2680
  }
2657
2681
 
2658
2682
  const fail = foe.active.volatiles.Substitute or state.miss or !state.proc;
2659
2683
 
2660
- var stats = &foe.active.stats;
2684
+ const stats = &foe.active.stats;
2661
2685
  var boosts = &foe.active.boosts;
2662
2686
 
2663
2687
  const move = Move.get(state.move);
@@ -2675,7 +2699,7 @@ pub const Effects = struct {
2675
2699
  }
2676
2700
  const n: u2 = if (move.effect == .AttackDown2) 2 else 1;
2677
2701
  try unboostStat(battle, foe, stats, boosts, "atk", n);
2678
- try log.boost(.{ foe_ident, Boost.Attack, -@as(i8, n) });
2702
+ try log.boost(.{ foe_ident, .Attack, -@as(i8, n) });
2679
2703
  },
2680
2704
  .DefenseDown1, .DefenseDown2, .DefenseDownChance => {
2681
2705
  assert(boosts.def >= -6 and boosts.def <= 6);
@@ -2691,7 +2715,7 @@ pub const Effects = struct {
2691
2715
  }
2692
2716
  const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
2693
2717
  try unboostStat(battle, foe, stats, boosts, "def", n);
2694
- try log.boost(.{ foe_ident, Boost.Defense, -@as(i8, n) });
2718
+ try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
2695
2719
  },
2696
2720
  .SpeedDown1, .SpeedDown2, .SpeedDownChance => {
2697
2721
  assert(boosts.spe >= -6 and boosts.spe <= 6);
@@ -2705,7 +2729,7 @@ pub const Effects = struct {
2705
2729
  }
2706
2730
  const n: u2 = if (move.effect == .SpeedDown2) 2 else 1;
2707
2731
  try unboostStat(battle, foe, stats, boosts, "spe", n);
2708
- try log.boost(.{ foe_ident, Boost.Speed, -@as(i8, n) });
2732
+ try log.boost(.{ foe_ident, .Speed, -@as(i8, n) });
2709
2733
  },
2710
2734
  .SpDefDownChance => {
2711
2735
  assert(boosts.spd >= -6 and boosts.spd <= 6);
@@ -2715,7 +2739,7 @@ pub const Effects = struct {
2715
2739
  return options.log.miss(.{battle.active(player)});
2716
2740
  } else if (stats.spd == 1) return;
2717
2741
  try unboostStat(battle, foe, stats, boosts, "spd", 1);
2718
- try log.boost(.{ foe_ident, Boost.SpecialDefense, -1 });
2742
+ try log.boost(.{ foe_ident, .SpecialDefense, -1 });
2719
2743
  },
2720
2744
  .AccuracyDown1, .AccuracyDownChance => {
2721
2745
  assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
@@ -2727,7 +2751,7 @@ pub const Effects = struct {
2727
2751
  return options.log.miss(.{battle.active(player)});
2728
2752
  }
2729
2753
  boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
2730
- try log.boost(.{ foe_ident, Boost.Accuracy, -1 });
2754
+ try log.boost(.{ foe_ident, .Accuracy, -1 });
2731
2755
  },
2732
2756
  else => unreachable,
2733
2757
  }
@@ -2741,9 +2765,9 @@ fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
2741
2765
  }
2742
2766
 
2743
2767
  fn recomputeStats(battle: anytype, side: *Side) void {
2744
- var stats = unmodifiedStats(battle, side);
2768
+ const stats = unmodifiedStats(battle, side);
2745
2769
  inline for (STATS) |s| {
2746
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, @field(side.active.boosts, s)) + 6))];
2770
+ const mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, @field(side.active.boosts, s)) + 6))];
2747
2771
  const val = @field(stats, s) * mod[0] / mod[1];
2748
2772
  @field(side.active.stats, s) = @max(1, @min(MAX_STAT_VALUE, val));
2749
2773
  }
@@ -2766,7 +2790,7 @@ fn boostStat(
2766
2790
  n: u2,
2767
2791
  ) !void {
2768
2792
  @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))];
2793
+ const mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, @field(boosts, s)) + 6))];
2770
2794
  const stat = @field(unmodifiedStats(battle, side), s);
2771
2795
  @field(stats, s) = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2772
2796
  if (comptime std.mem.eql(u8, s, "atk")) {
@@ -2789,7 +2813,7 @@ fn unboostStat(
2789
2813
  n: u2,
2790
2814
  ) !void {
2791
2815
  @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))];
2816
+ const mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, @field(boosts, s)) + 6))];
2793
2817
  const stat = @field(unmodifiedStats(battle, side), s);
2794
2818
  @field(stats, s) = @max(1, stat * mod[0] / mod[1]);
2795
2819
  if (comptime std.mem.eql(u8, s, "atk")) {
@@ -2815,9 +2839,15 @@ fn convertible(active: ActivePokemon, m: Move) bool {
2815
2839
  return t != .@"???" and !active.types.includes(t);
2816
2840
  }
2817
2841
 
2842
+ // NOTE: llvm/llvm-project#58557
2843
+ fn smul(a: anytype, b: @TypeOf(a)) @TypeOf(a) {
2844
+ if (!comptime builtin.target.isWasm()) return a *| b;
2845
+ return @max(a *% b, std.math.maxInt(@TypeOf(a)));
2846
+ }
2847
+
2818
2848
  pub const Rolls = struct {
2819
2849
  fn speedTie(battle: anytype, options: anytype) !bool {
2820
- const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
2850
+ const p1 = if (options.calc.overridden(.P1, 0, .speed_tie)) |player|
2821
2851
  player == .P1
2822
2852
  else if (showdown)
2823
2853
  battle.rng.range(u8, 0, 2) == 0
@@ -2829,7 +2859,7 @@ pub const Rolls = struct {
2829
2859
  }
2830
2860
 
2831
2861
  fn quickClaw(battle: anytype, player: Player, options: anytype) !bool {
2832
- const qkc = if (options.calc.overridden(player, .quick_claw)) |val|
2862
+ const qkc = if (options.calc.overridden(player, 0, .quick_claw)) |val|
2833
2863
  val == .true
2834
2864
  else if (showdown)
2835
2865
  battle.rng.chance(u8, 60, 256)
@@ -2841,7 +2871,7 @@ pub const Rolls = struct {
2841
2871
  }
2842
2872
 
2843
2873
  fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) !bool {
2844
- const ok = if (options.calc.overridden(player, .hit)) |val|
2874
+ const ok = if (options.calc.overridden(player, 0, .hit)) |val|
2845
2875
  val == .true
2846
2876
  else if (showdown)
2847
2877
  battle.rng.chance(u8, accuracy, 256)
@@ -2852,20 +2882,22 @@ pub const Rolls = struct {
2852
2882
  return ok;
2853
2883
  }
2854
2884
 
2855
- fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2856
- const crit = if (options.calc.overridden(player, .critical_hit)) |val|
2885
+ fn criticalHit(battle: anytype, player: Player, i: u3, rate: u8, options: anytype) !bool {
2886
+ _ = .{ player, options };
2887
+ const crit = if (options.calc.overridden(player, i, .critical_hits)) |val|
2857
2888
  val == .true
2858
2889
  else if (showdown)
2859
2890
  battle.rng.chance(u8, rate, 256)
2860
2891
  else
2861
2892
  battle.rng.next() < rate;
2862
2893
 
2863
- try options.chance.criticalHit(player, crit, rate);
2894
+ try options.chance.criticalHit(player, i, crit, rate);
2864
2895
  return crit;
2865
2896
  }
2866
2897
 
2867
- fn damage(battle: anytype, player: Player, options: anytype) !u8 {
2868
- const roll = if (options.calc.overridden(player, .damage)) |val|
2898
+ fn damage(battle: anytype, player: Player, i: u3, options: anytype) !u8 {
2899
+ _ = .{ player, options };
2900
+ const roll = if (options.calc.overridden(player, i, .damages)) |val|
2869
2901
  val
2870
2902
  else roll: {
2871
2903
  if (showdown) break :roll battle.rng.range(u8, 217, 256);
@@ -2876,12 +2908,12 @@ pub const Rolls = struct {
2876
2908
  };
2877
2909
 
2878
2910
  assert(roll >= 217 and roll <= 255);
2879
- try options.chance.damage(player, roll);
2911
+ try options.chance.damage(player, i, roll);
2880
2912
  return roll;
2881
2913
  }
2882
2914
 
2883
2915
  fn confused(battle: anytype, player: Player, options: anytype) !bool {
2884
- const cfz = if (options.calc.overridden(player, .confused)) |val|
2916
+ const cfz = if (options.calc.overridden(player, 0, .confused)) |val|
2885
2917
  val == .true
2886
2918
  else if (showdown)
2887
2919
  !battle.rng.chance(u8, 128, 256)
@@ -2893,7 +2925,7 @@ pub const Rolls = struct {
2893
2925
  }
2894
2926
 
2895
2927
  fn attract(battle: anytype, player: Player, options: anytype) !bool {
2896
- const cant = if (options.calc.overridden(player, .attract)) |val|
2928
+ const cant = if (options.calc.overridden(player, 0, .attract)) |val|
2897
2929
  val == .true
2898
2930
  else if (showdown)
2899
2931
  battle.rng.chance(u8, 1, 2)
@@ -2905,7 +2937,7 @@ pub const Rolls = struct {
2905
2937
  }
2906
2938
 
2907
2939
  fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2908
- const par = if (options.calc.overridden(player, .paralyzed)) |val|
2940
+ const par = if (options.calc.overridden(player, 0, .paralyzed)) |val|
2909
2941
  val == .true
2910
2942
  else if (showdown)
2911
2943
  battle.rng.chance(u8, 63, 256)
@@ -2917,7 +2949,7 @@ pub const Rolls = struct {
2917
2949
  }
2918
2950
 
2919
2951
  fn defrost(battle: anytype, player: Player, options: anytype) !bool {
2920
- const thaw = if (options.calc.overridden(player, .defrost)) |val|
2952
+ const thaw = if (options.calc.overridden(player, 0, .defrost)) |val|
2921
2953
  val == .true
2922
2954
  else if (showdown)
2923
2955
  battle.rng.chance(u8, 25, 256)
@@ -2930,7 +2962,7 @@ pub const Rolls = struct {
2930
2962
 
2931
2963
  fn secondaryChance(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2932
2964
  // GLITCH: 100% secondary chance will still fail 1/256 of the time
2933
- const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2965
+ const proc = if (options.calc.overridden(player, 0, .secondary_chance)) |val|
2934
2966
  val == .true
2935
2967
  else if (showdown)
2936
2968
  battle.rng.chance(u8, rate, 256)
@@ -2942,7 +2974,7 @@ pub const Rolls = struct {
2942
2974
  }
2943
2975
 
2944
2976
  fn item(battle: anytype, player: Player, options: anytype) !bool {
2945
- const proc = if (options.calc.overridden(player, .item)) |val|
2977
+ const proc = if (options.calc.overridden(player, 0, .item)) |val|
2946
2978
  val == .true
2947
2979
  else if (showdown)
2948
2980
  battle.rng.chance(u8, 30, 256)
@@ -2954,7 +2986,7 @@ pub const Rolls = struct {
2954
2986
  }
2955
2987
 
2956
2988
  fn triAttack(battle: anytype, player: Player, options: anytype) !TriAttack {
2957
- const status: TriAttack = if (options.calc.overridden(player, .tri_attack)) |val|
2989
+ const status: TriAttack = if (options.calc.overridden(player, 0, .tri_attack)) |val|
2958
2990
  @enumFromInt(@intFromEnum(val) - 1)
2959
2991
  else if (showdown)
2960
2992
  @enumFromInt(battle.rng.range(u2, 0, 2))
@@ -2970,7 +3002,7 @@ pub const Rolls = struct {
2970
3002
  }
2971
3003
 
2972
3004
  fn present(battle: anytype, player: Player, options: anytype) !u8 {
2973
- const power = if (options.calc.overridden(player, .present)) |val|
3005
+ const power = if (options.calc.overridden(player, 0, .present)) |val|
2974
3006
  (val - 1) * 40
2975
3007
  else if (showdown) power: {
2976
3008
  const r = battle.rng.range(u8, 0, 10);
@@ -2992,7 +3024,7 @@ pub const Rolls = struct {
2992
3024
  }
2993
3025
 
2994
3026
  fn magnitude(battle: anytype, player: Player, options: anytype) !u8 {
2995
- const num: u8 = if (options.calc.overridden(player, .magnitude)) |val|
3027
+ const num: u8 = if (options.calc.overridden(player, 0, .magnitude)) |val|
2996
3028
  val + 3
2997
3029
  else if (showdown) num: {
2998
3030
  const r = battle.rng.range(u8, 0, 100);
@@ -3020,7 +3052,7 @@ pub const Rolls = struct {
3020
3052
  }
3021
3053
 
3022
3054
  fn tripleKick(battle: anytype, player: Player, options: anytype) !u2 {
3023
- const hits = if (options.calc.overridden(player, .triple_kick)) |val|
3055
+ const hits = if (options.calc.overridden(player, 0, .triple_kick)) |val|
3024
3056
  val
3025
3057
  else if (showdown)
3026
3058
  battle.rng.range(u2, 1, 4)
@@ -3036,13 +3068,12 @@ pub const Rolls = struct {
3036
3068
  return hits;
3037
3069
  }
3038
3070
 
3039
- fn spite(battle: anytype, player: Player, options: anytype) !u8 {
3040
- const pp = if (options.calc.overridden(player, .spite)) |val|
3071
+ fn spite(battle: anytype, player: Player, options: anytype) !u3 {
3072
+ assert(!showdown);
3073
+ const pp: u3 = if (options.calc.overridden(player, 0, .spite)) |val|
3041
3074
  val + 1
3042
- else if (showdown)
3043
- battle.rng.range(u8, 2, 6)
3044
3075
  else
3045
- (battle.rng.next() & 3) + 2;
3076
+ @intCast((battle.rng.next() & 3) + 2);
3046
3077
 
3047
3078
  assert(pp >= 2 and pp <= 5);
3048
3079
  try options.chance.spite(player, pp);
@@ -3056,7 +3087,7 @@ pub const Rolls = struct {
3056
3087
  var i: u8 = 0;
3057
3088
  const neutral = @intFromEnum(Effectiveness.Neutral);
3058
3089
  assert(mtype != .@"???");
3059
- const ty: Type = if (options.calc.overridden(player, .conversion_2)) |val|
3090
+ const ty: Type = if (options.calc.overridden(player, 0, .conversion_2)) |val|
3060
3091
  @enumFromInt(@intFromEnum(val) - 1)
3061
3092
  else if (showdown) ty: {
3062
3093
  for (Type.SHOWDOWN) |t| {
@@ -3090,7 +3121,7 @@ pub const Rolls = struct {
3090
3121
  assert(count <= 8);
3091
3122
  const num = if (count == 8) 0 else @as(u8, 255) >> @intCast(count);
3092
3123
 
3093
- const ok = if (options.calc.overridden(player, .protect)) |val|
3124
+ const ok = if (options.calc.overridden(player, 0, .protect)) |val|
3094
3125
  val == .true
3095
3126
  else if (showdown)
3096
3127
  battle.rng.chance(u8, num, 255)
@@ -3112,7 +3143,7 @@ pub const Rolls = struct {
3112
3143
  const moves = active.moves;
3113
3144
 
3114
3145
  // TODO: consider throwing error instead of rerolling?
3115
- const overridden = if (options.calc.overridden(player, .move_slot)) |val|
3146
+ const overridden = if (options.calc.overridden(player, 0, .move_slot)) |val|
3116
3147
  if (convertible(active, moves[val - 1].id)) val else null
3117
3148
  else
3118
3149
  null;
@@ -3149,7 +3180,7 @@ pub const Rolls = struct {
3149
3180
  const moves = active.moves;
3150
3181
 
3151
3182
  // TODO: consider throwing error instead of rerolling?
3152
- const overridden = if (options.calc.overridden(player, .move_slot)) |val|
3183
+ const overridden = if (options.calc.overridden(player, 0, .move_slot)) |val|
3153
3184
  if (val != mslot and Move.get(moves[val - 1].id).extra.sleep_talk) val else null
3154
3185
  else
3155
3186
  null;
@@ -3193,7 +3224,7 @@ pub const Rolls = struct {
3193
3224
  assert(n > 0 and n <= 6);
3194
3225
 
3195
3226
  // TODO: consider throwing error instead of rerolling?
3196
- const overridden = if (options.calc.overridden(player, .force_switch)) |val| val: {
3227
+ const overridden = if (options.calc.overridden(player, 0, .force_switch)) |val| val: {
3197
3228
  for (slots) |slot| if (slot == val) break :val if (foe.get(slot).hp > 0) val else null;
3198
3229
  break :val null;
3199
3230
  } else null;
@@ -3219,7 +3250,7 @@ pub const Rolls = struct {
3219
3250
  const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
3220
3251
 
3221
3252
  fn multiHit(battle: anytype, player: Player, options: anytype) !u3 {
3222
- const n: u3 = if (options.calc.overridden(player, .multi_hit)) |val|
3253
+ const n: u3 = if (options.calc.overridden(player, 0, .multi_hit)) |val|
3223
3254
  @intCast(val)
3224
3255
  else if (showdown)
3225
3256
  DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
@@ -3234,7 +3265,7 @@ pub const Rolls = struct {
3234
3265
  }
3235
3266
 
3236
3267
  fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
3237
- const power = if (options.calc.overridden(player, .psywave)) |val|
3268
+ const power = if (options.calc.overridden(player, 0, .psywave)) |val|
3238
3269
  val
3239
3270
  else if (showdown)
3240
3271
  battle.rng.range(u8, 1, max)
@@ -3260,7 +3291,7 @@ pub const Rolls = struct {
3260
3291
  }
3261
3292
 
3262
3293
  // TODO: consider throwing error instead of rerolling?
3263
- const overridden = if (options.calc.overridden(player, .metronome)) |val| val: {
3294
+ const overridden = if (options.calc.overridden(player, 0, .metronome)) |val| val: {
3264
3295
  for (moves) |m| if (m.id == val) break :val null;
3265
3296
  break :val val;
3266
3297
  } else null;
@@ -3292,7 +3323,7 @@ pub const Rolls = struct {
3292
3323
  }
3293
3324
 
3294
3325
  fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
3295
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
3326
+ const duration: u3 = if (options.calc.overridden(player, 0, .duration)) |val|
3296
3327
  @intCast(val)
3297
3328
  else if (showdown)
3298
3329
  battle.rng.range(u3, 1, 7)
@@ -3304,12 +3335,12 @@ pub const Rolls = struct {
3304
3335
  };
3305
3336
 
3306
3337
  assert(duration >= 1 and duration <= 6);
3307
- options.chance.duration(.sleep, player, player.foe(), duration);
3338
+ options.chance.duration(player, duration);
3308
3339
  return duration + 1;
3309
3340
  }
3310
3341
 
3311
3342
  fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
3312
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
3343
+ const duration: u3 = if (options.calc.overridden(player, 0, .duration)) |val|
3313
3344
  @intCast(val)
3314
3345
  else if (showdown)
3315
3346
  // Pokémon Showdown incorrectly uses the same duration for self-confusion
@@ -3318,16 +3349,16 @@ pub const Rolls = struct {
3318
3349
  @intCast((battle.rng.next() & if (self) 1 else 3) + 2);
3319
3350
 
3320
3351
  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);
3352
+ options.chance.duration(player, duration);
3322
3353
  return duration;
3323
3354
  }
3324
3355
 
3325
3356
  fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
3326
- const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
3357
+ const duration: u3 = if (options.calc.overridden(player, 0, .duration)) |val|
3327
3358
  @intCast(val)
3328
3359
  else if (showdown)
3329
3360
  // Pokémon Showdown incorrectly inherits Generation III's Disable duration
3330
- battle.rng.range(u4, 2, 6)
3361
+ battle.rng.range(u3, 2, 6)
3331
3362
  else duration: {
3332
3363
  while (true) {
3333
3364
  const r = battle.rng.next() & 7;
@@ -3336,12 +3367,12 @@ pub const Rolls = struct {
3336
3367
  };
3337
3368
 
3338
3369
  assert(duration >= 2 and duration <= (if (showdown) 5 else 7));
3339
- options.chance.duration(.disable, player, player.foe(), duration);
3370
+ options.chance.duration(player, duration);
3340
3371
  return duration;
3341
3372
  }
3342
3373
 
3343
3374
  fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
3344
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
3375
+ const duration: u3 = if (options.calc.overridden(player, 0, .duration)) |val|
3345
3376
  @intCast(val)
3346
3377
  else if (showdown)
3347
3378
  battle.rng.range(u3, 2, 4)
@@ -3349,12 +3380,12 @@ pub const Rolls = struct {
3349
3380
  @intCast((battle.rng.next() & 1) + 2); // BUG: thrash is + 1 not +2
3350
3381
 
3351
3382
  assert(duration >= 2 and duration <= 3);
3352
- options.chance.duration(.attacking, player, player, duration);
3383
+ options.chance.duration(player, duration);
3353
3384
  return duration;
3354
3385
  }
3355
3386
 
3356
3387
  fn bindingDuration(battle: anytype, player: Player, options: anytype) u3 {
3357
- const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
3388
+ const duration: u3 = (if (options.calc.overridden(player, 0, .duration)) |val|
3358
3389
  @intCast(val)
3359
3390
  else if (showdown)
3360
3391
  // TODO: PS does range(3, 6) here which after subtracting 1 is actually 2-4 not 2-5!
@@ -3363,13 +3394,13 @@ pub const Rolls = struct {
3363
3394
  @intCast(battle.rng.next() & 3 + 2));
3364
3395
 
3365
3396
  assert(duration >= 2 and duration <= 5);
3366
- options.chance.duration(.binding, player, player.foe(), duration);
3397
+ options.chance.duration(player, duration);
3367
3398
  return duration + 1;
3368
3399
  }
3369
3400
 
3370
3401
  // TODO: consider sharing implementation with bindingDuration
3371
3402
  fn encoreDuration(battle: anytype, player: Player, options: anytype) u3 {
3372
- const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
3403
+ const duration: u3 = (if (options.calc.overridden(player, 0, .duration)) |val|
3373
3404
  @intCast(val)
3374
3405
  else if (showdown)
3375
3406
  battle.rng.range(u3, 2, 6)
@@ -3377,7 +3408,7 @@ pub const Rolls = struct {
3377
3408
  @intCast(battle.rng.next() & 3 + 2));
3378
3409
 
3379
3410
  assert(duration >= 2 and duration <= 5);
3380
- options.chance.duration(.encore, player, player.foe(), duration);
3411
+ options.chance.duration(player, duration);
3381
3412
  return duration + 1;
3382
3413
  }
3383
3414
  };
@@ -3490,7 +3521,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3490
3521
  .Move => {
3491
3522
  const side = battle.side(player);
3492
3523
 
3493
- var active = &side.active;
3524
+ const active = &side.active;
3494
3525
  const stored = side.stored();
3495
3526
 
3496
3527
  // While players are not given any input options on the cartridge in these cases,