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

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