@pkmn/engine 0.1.0-dev.dfd46bbb → 0.1.0-dev.e0a786f8

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 (83) hide show
  1. package/LICENSE +1 -3
  2. package/README.md +51 -23
  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 +25 -3
  13. package/build/pkg/addon.js +39 -71
  14. package/build/pkg/addon.js.map +1 -1
  15. package/build/pkg/data/layout.json +15 -2
  16. package/build/pkg/data/protocol.json +23 -9
  17. package/build/pkg/data.d.ts +27 -5
  18. package/build/pkg/data.js.map +1 -1
  19. package/build/pkg/gen1.d.ts +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 +20 -1
  26. package/build/pkg/protocol.js +109 -107
  27. package/build/pkg/protocol.js.map +1 -1
  28. package/build/tools/debug.js +53 -76
  29. package/build/tools/debug.js.map +1 -1
  30. package/build/tools/display/debug.jsx +152 -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 +359 -0
  35. package/build/tools/display/ui.jsx.map +1 -0
  36. package/build/tools/display/util.js +33 -0
  37. package/build/tools/display/util.js.map +1 -0
  38. package/build.zig +330 -93
  39. package/package.json +59 -132
  40. package/src/bin/install-pkmn-engine +31 -20
  41. package/src/bin/pkmn-debug +1 -1
  42. package/src/lib/bindings/c.zig +255 -0
  43. package/src/lib/{binding → bindings}/node.zig +8 -21
  44. package/src/lib/bindings/wasm.zig +59 -0
  45. package/src/lib/common/array.zig +77 -0
  46. package/src/lib/common/data.zig +4 -4
  47. package/src/lib/common/debug.zig +39 -8
  48. package/src/lib/common/js.zig +4 -2
  49. package/src/lib/common/optional.zig +14 -10
  50. package/src/lib/common/options.zig +6 -0
  51. package/src/lib/common/protocol.zig +396 -399
  52. package/src/lib/common/rational.zig +327 -158
  53. package/src/lib/common/rng.zig +7 -9
  54. package/src/lib/common/util.zig +60 -1
  55. package/src/lib/gen1/calc.zig +696 -160
  56. package/src/lib/gen1/chance.zig +405 -280
  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 +1 -2
  60. package/src/lib/gen1/data.zig +24 -27
  61. package/src/lib/gen1/helpers.zig +92 -548
  62. package/src/lib/gen1/mechanics.zig +575 -461
  63. package/src/lib/gen2/calc.zig +37 -27
  64. package/src/lib/gen2/chance.zig +84 -496
  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 +1 -2
  69. package/src/lib/gen2/data.zig +20 -23
  70. package/src/lib/gen2/generated.zig +24 -37
  71. package/src/lib/gen2/helpers.zig +46 -25
  72. package/src/lib/gen2/mechanics.zig +721 -453
  73. package/src/lib/node.zig +18 -0
  74. package/src/lib/pkmn.zig +37 -15
  75. package/src/lib/wasm.zig +17 -0
  76. package/src/tools/display/dom.ts +93 -0
  77. package/src/tools/display/ui.css +354 -0
  78. package/build/test/display.js +0 -400
  79. package/build/test/display.js.map +0 -1
  80. package/src/lib/binding/wasm.zig +0 -12
  81. package/src/lib/gen1/README.md +0 -894
  82. package/src/lib/gen2/README.md +0 -330
  83. package/src/test/display/pkmn.html.tmpl +0 -368
@@ -1,49 +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 Effectiveness = data.Effectiveness;
26
+ const Heal = protocol.Heal;
27
+ const ID = common.ID;
41
28
  const Item = data.Item;
42
29
  const Move = data.Move;
43
- const MoveSlot = data.MoveSlot;
44
- const Pokemon = data.Pokemon;
30
+ const Player = common.Player;
31
+ const Result = common.Result;
32
+ const showdown = pkmn.options.showdown;
45
33
  const Side = data.Side;
46
- const Species = data.Species;
34
+ const Start = protocol.Start;
47
35
  const Stats = data.Stats;
48
36
  const Status = data.Status;
49
37
  const TriAttack = data.TriAttack;
@@ -96,12 +84,16 @@ const CRITICAL_HITS = &[_]u8{
96
84
 
97
85
  const MAX_STAT_VALUE = 999;
98
86
 
87
+ const STATS = [_][]const u8{ "atk", "def", "spe", "spa", "spd" };
88
+
99
89
  pub const State = struct {
100
90
  damage: u16 = 0, // wCurDamage
101
91
  effectiveness: u16 = Effectiveness.neutral, // wTypeModifier
102
92
  bp: u8, // wPlayerMoveStructPower
93
+ type: Type, // BATTLE_VARS_MOVE_TYPE
103
94
  accuracy: u8, // wEnemyMoveStruct + MOVE_ACC
104
95
  move: Move, // wCurPlayerMove
96
+ hit: u3 = 0,
105
97
  from: Move = .None,
106
98
  mslot: u4, // wCurMoveNum
107
99
  first: bool, // wEnemyGoesFirst
@@ -111,7 +103,7 @@ pub const State = struct {
111
103
  proc: bool = true, // wEffectFailed
112
104
 
113
105
  comptime {
114
- assert(@sizeOf(State) == 14);
106
+ assert(@sizeOf(State) == 16);
115
107
  }
116
108
 
117
109
  pub fn immune(self: *State) bool {
@@ -136,11 +128,11 @@ fn start(battle: anytype, options: anytype) !Result {
136
128
  const p1 = battle.side(.P1);
137
129
  const p2 = battle.side(.P2);
138
130
 
139
- var p1_slot = findFirstAlive(p1);
131
+ const p1_slot = findFirstAlive(p1);
140
132
  assert(!showdown or p1_slot == 1);
141
133
  if (p1_slot == 0) return if (findFirstAlive(p2) == 0) Result.Tie else Result.Lose;
142
134
 
143
- var p2_slot = findFirstAlive(p2);
135
+ const p2_slot = findFirstAlive(p2);
144
136
  assert(!showdown or p2_slot == 1);
145
137
  if (p2_slot == 0) return Result.Win;
146
138
 
@@ -248,15 +240,14 @@ fn switchIn(battle: anytype, player: Player, slot: u8, kind: SwitchIn, options:
248
240
  statusModify(incoming.status, &active.stats);
249
241
 
250
242
  if (kind == .Drag) {
251
- try options.log.drag(battle.active(player), incoming);
243
+ try options.log.drag(.{ battle.active(player), incoming });
252
244
  } else {
253
- try options.log.switched(battle.active(player), incoming);
245
+ try options.log.switched(.{ battle.active(player), incoming });
254
246
  }
255
- options.chance.switched(player, side.pokemon[0].position, out);
256
247
 
257
248
  if (side.conditions.Spikes and !active.types.includes(.Flying)) {
258
249
  incoming.hp -|= @max(incoming.stats.hp / 8, 1);
259
- try options.log.damage(battle.active(player), incoming, .Spikes);
250
+ try options.log.damage(.{ battle.active(player), incoming, .Spikes });
260
251
  }
261
252
  }
262
253
 
@@ -399,14 +390,22 @@ fn executeMove(
399
390
 
400
391
  assert(choice.type == .Move);
401
392
  var mslot: u4 = @intCast(choice.data);
393
+ _ = &mslot;
402
394
  var move = Move.Pound; // TODO
395
+ _ = &move;
403
396
 
404
397
  if (!try beforeMove(battle, player, move, options)) return null;
405
398
 
406
399
  // FIXME: need damage from doTurn
407
400
  const m = Move.get(move);
408
- var state: State =
409
- .{ .move = move, .mslot = mslot, .first = first, .accuracy = m.accuracy, .bp = m.bp };
401
+ var state: State = .{
402
+ .move = move,
403
+ .mslot = mslot,
404
+ .first = first,
405
+ .accuracy = m.accuracy,
406
+ .bp = m.bp,
407
+ .type = m.type,
408
+ };
410
409
  try generated.runMove(battle, player, &state, options);
411
410
  return null;
412
411
  }
@@ -423,31 +422,22 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
423
422
  if (volatiles.Recharging) {
424
423
  volatiles.Recharging = false;
425
424
  resetCant(volatiles);
426
- try log.cant(ident, .Recharge);
425
+ try log.cant(.{ ident, .Recharge });
427
426
  return false;
428
427
  }
429
428
 
430
429
  if (Status.is(stored.status, .SLP)) slp: {
431
430
  const before = stored.status;
432
431
  // Even if the EXT bit is set this will still correctly modify the sleep duration
433
- // if (options.calc.modify(player, .sleep)) |extend| {
434
- // if (!extend) stored.status = 0;
435
- // } else {
436
432
  stored.status -= 1;
437
- // }
438
433
 
439
- const duration = Status.duration(stored.status);
440
- if (!Status.is(stored.status, .EXT)) {
441
- try options.chance.sleep(player, duration);
442
- }
443
-
444
- if (duration == 0) {
445
- try log.curestatus(ident, before, .Message);
434
+ if (Status.duration(stored.status) == 0) {
435
+ try log.curestatus(.{ ident, before, .Message });
446
436
  stored.status = 0; // clears EXT if present
447
437
  resetCant(volatiles);
448
438
  volatiles.Nightmare = false;
449
439
  } else {
450
- try log.cant(ident, .Sleep);
440
+ try log.cant(.{ ident, .Sleep });
451
441
  if (move == .Snore or move == .SleepTalk) break :slp;
452
442
  resetCant(volatiles);
453
443
  return false;
@@ -456,7 +446,7 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
456
446
 
457
447
  if (Status.is(stored.status, .FRZ) and !(move == .FlameWheel or move == .SacredFire)) {
458
448
  resetCant(volatiles);
459
- try log.cant(ident, .Freeze);
449
+ try log.cant(.{ ident, .Freeze });
460
450
  return false;
461
451
  }
462
452
 
@@ -466,39 +456,28 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
466
456
  // if (!showdown) volatiles.Flinch = false;
467
457
  volatiles.Flinch = false;
468
458
  resetCant(volatiles);
469
- try log.cant(ident, .Flinch);
459
+ try log.cant(.{ ident, .Flinch });
470
460
  return false;
471
461
  }
472
462
 
473
463
  if (volatiles.disable.duration > 0) {
474
- // if (options.calc.modify(player, .disable)) |extend| {
475
- // if (!extend) volatiles.disable.duration = 0;
476
- // } else {
477
464
  volatiles.disable.duration -= 1;
478
- // }
479
- try options.chance.disable(player, volatiles.disable.duration);
480
465
 
481
466
  if (volatiles.disable.duration == 0) {
482
467
  volatiles.disable.move = 0;
483
- try log.end(ident, .Disable);
468
+ try log.end(.{ ident, .Disable });
484
469
  }
485
470
  }
486
471
 
487
472
  if (volatiles.Confusion) {
488
473
  assert(volatiles.confusion > 0);
489
-
490
- // if (options.calc.modify(player, .confusion)) |extend| {
491
- // if (!extend) volatiles.confusion = 0;
492
- // } else {
493
474
  volatiles.confusion -= 1;
494
- // }
495
- try options.chance.confusion(player, volatiles.confusion);
496
475
 
497
476
  if (volatiles.confusion == 0) {
498
477
  volatiles.Confusion = false;
499
- try log.end(ident, .Confusion);
478
+ try log.end(.{ ident, .Confusion });
500
479
  } else {
501
- try log.activate(ident, .Confusion);
480
+ try log.activate(.{ ident, .Confusion });
502
481
 
503
482
  if (try Rolls.confused(battle, player, options)) {
504
483
  volatiles.BeatUp = false;
@@ -513,11 +492,11 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
513
492
  }
514
493
 
515
494
  if (volatiles.Attract) {
516
- try log.activate(ident, .Attract);
495
+ try log.activate(.{ ident, .Attract });
517
496
 
518
497
  if (try Rolls.attract(battle, player, options)) {
519
498
  resetCant(volatiles);
520
- try log.cant(ident, .Attract);
499
+ try log.cant(.{ ident, .Attract });
521
500
  return false;
522
501
  }
523
502
  }
@@ -528,14 +507,14 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
528
507
  if (m != .None and m == move) {
529
508
  side.active.volatiles.Charging = false;
530
509
  resetCant(volatiles);
531
- try options.log.disabled(ident, move);
510
+ try options.log.cant(.{ ident, .Disable, move });
532
511
  return false;
533
512
  }
534
513
  }
535
514
 
536
515
  if (Status.is(stored.status, .PAR) and try Rolls.paralyzed(battle, player, options)) {
537
516
  resetCant(volatiles);
538
- try log.cant(ident, .Paralysis);
517
+ try log.cant(.{ ident, .Paralysis });
539
518
  return false;
540
519
  }
541
520
 
@@ -555,11 +534,11 @@ fn resetCant(volatiles: *align(8) Volatiles) void {
555
534
 
556
535
  pub fn canMove(battle: anytype, player: Player, state: *State, options: anytype) !bool {
557
536
  var side = battle.side(player);
558
- var volatiles = &side.active.volatiles;
537
+ const volatiles = &side.active.volatiles;
559
538
 
560
539
  // FIXME: pass in target to avoid lookup?
561
540
  const target = if (Move.get(state.move).target == .Self) player else player.foe();
562
- try options.log.moveFrom(battle.active(player), state.move, battle.active(target), state.from);
541
+ try options.log.move(.{ battle.active(player), state.move, battle.active(target), state.from });
563
542
 
564
543
  const charging = false; // TODO charging uses doUse
565
544
  const skip_pp = charging or state.move == .Struggle or
@@ -638,7 +617,7 @@ pub fn afterMove(
638
617
 
639
618
  pub fn checkCriticalHit(battle: anytype, player: Player, state: *State, options: anytype) !void {
640
619
  const move = Move.get(state.move);
641
- if (move.bp == 0) return;
620
+ if (state.bp == 0) return;
642
621
 
643
622
  const side = battle.side(player);
644
623
  const pokemon = side.stored();
@@ -653,7 +632,7 @@ pub fn checkCriticalHit(battle: anytype, player: Player, state: *State, options:
653
632
  }
654
633
  assert(stage <= 4);
655
634
 
656
- 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);
657
636
  }
658
637
 
659
638
  pub fn calcDamage(
@@ -676,31 +655,30 @@ pub fn adjustDamage(battle: anytype, player: Player, state: *State, _: anytype)
676
655
  const side = battle.side(player);
677
656
  const foe = battle.foe(player);
678
657
  const types = foe.active.types;
679
- const move = Move.get(state.move);
680
658
 
681
659
  var d = state.damage;
682
- if ((move.type == .Water and battle.field.weather == .Rain) or
683
- (move.type == .Fire and battle.field.weather == .Sun))
660
+ if ((state.type == .Water and battle.field.weather == .Rain) or
661
+ (state.type == .Fire and battle.field.weather == .Sun))
684
662
  {
685
663
  d = d *% @intFromEnum(Effectiveness.Super) / 10;
686
- } else if ((move.type == .Water and battle.field.weather == .Sun) or
687
- ((move.type == .Fire or state.move == .SolarBeam) and battle.field.weather == .Rain))
664
+ } else if ((state.type == .Water and battle.field.weather == .Sun) or
665
+ ((state.type == .Fire or state.move == .SolarBeam) and battle.field.weather == .Rain))
688
666
  {
689
667
  d = d *% @intFromEnum(Effectiveness.Resisted) / 10;
690
668
  }
691
669
 
692
- if (side.active.types.includes(move.type)) d +%= d / 2;
670
+ if (side.active.types.includes(state.type)) d +%= d / 2;
693
671
 
694
672
  const neutral = @intFromEnum(Effectiveness.Neutral);
695
- const foresight = foe.active.volatiles.Foresight and FORESIGHT.includes(move.type);
673
+ const foresight = foe.active.volatiles.Foresight and FORESIGHT.includes(state.type);
696
674
  const eff1: u16 = if (foresight and types.type1 == .Ghost)
697
675
  neutral
698
676
  else
699
- @intFromEnum(move.type.effectiveness(types.type1));
677
+ @intFromEnum(state.type.effectiveness(types.type1));
700
678
  const eff2: u16 = if (foresight and types.type2 == .Ghost)
701
679
  neutral
702
680
  else
703
- @intFromEnum(move.type.effectiveness(types.type2));
681
+ @intFromEnum(state.type.effectiveness(types.type2));
704
682
 
705
683
  // Type effectiveness matchup precedence only matters with (NVE, SE)
706
684
  if (!showdown and (eff1 + eff2) == Effectiveness.mismatch and
@@ -722,7 +700,7 @@ pub fn adjustDamage(battle: anytype, player: Player, state: *State, _: anytype)
722
700
 
723
701
  pub fn randomizeDamage(battle: anytype, player: Player, state: *State, options: anytype) !void {
724
702
  if (state.damage <= 1) return;
725
- const random = try Rolls.damage(battle, player, options);
703
+ const random = try Rolls.damage(battle, player, state.hit, options);
726
704
  options.calc.base(player.foe(), state.damage);
727
705
  state.damage = @intCast(@as(u32, state.damage) *% random / 255);
728
706
  options.calc.final(player.foe(), state.damage);
@@ -840,13 +818,13 @@ fn checkFaint(
840
818
  if (foe_fainted) try faint(battle, player.foe(), !more, options);
841
819
 
842
820
  if (player_out and foe_out) {
843
- try options.log.tie();
821
+ try options.log.tie(.{});
844
822
  return Result.Tie;
845
823
  } else if (player_out) {
846
- try options.log.win(player.foe());
824
+ try options.log.win(.{player.foe()});
847
825
  return if (player == .P1) Result.Lose else Result.Win;
848
826
  } else if (foe_out) {
849
- try options.log.win(player);
827
+ try options.log.win(.{player});
850
828
  return if (player == .P1) Result.Win else Result.Lose;
851
829
  }
852
830
 
@@ -876,19 +854,19 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !void {
876
854
  side.stored().stats.spe;
877
855
  }
878
856
 
879
- try options.log.faint(battle.active(player), done);
857
+ try options.log.faint(.{ battle.active(player), done });
880
858
  options.calc.capped(player);
881
859
  }
882
860
 
883
861
  // TODO: see if we can move *before* applyDamage and include failuretext logic
884
862
  pub fn reportOutcome(battle: anytype, player: Player, state: *State, options: anytype) !void {
885
863
  const foe_ident = battle.active(player.foe());
886
- if (state.crit) try options.log.crit(foe_ident);
864
+ if (state.crit) try options.log.crit(.{foe_ident});
887
865
  if (!state.immune()) {
888
866
  if (state.effectiveness > Effectiveness.neutral) {
889
- try options.log.supereffective(foe_ident);
867
+ try options.log.supereffective(.{foe_ident});
890
868
  } else if (state.effectiveness < Effectiveness.neutral) {
891
- try options.log.resisted(foe_ident);
869
+ try options.log.resisted(.{foe_ident});
892
870
  }
893
871
  }
894
872
  }
@@ -921,8 +899,8 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !bool {
921
899
  }
922
900
 
923
901
  stored.hp -|= damage;
924
- // Pokémon Showdown uses damageOf here but its not relevant in Generation II
925
- try options.log.damage(ident, stored, if (brn) Damage.Burn else Damage.Poison);
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 });
926
904
  if (stored.hp == 0) return true;
927
905
  }
928
906
 
@@ -938,27 +916,27 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !bool {
938
916
 
939
917
  const damage = @max(stored.stats.hp / 8, 1);
940
918
  stored.hp -|= damage;
941
- // As above, Pokémon Showdown uses damageOf but its not relevant
942
- 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 });
943
921
 
944
922
  const before = foe_stored.hp;
945
923
  foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
946
924
  // Pokémon Showdown uses the less specific heal here instead of drain... because reasons?
947
- if (foe_stored.hp > before) try options.log.heal(foe_ident, foe_stored, .Silent);
925
+ if (foe_stored.hp > before) try options.log.heal(.{ foe_ident, foe_stored, .Silent });
948
926
  if (stored.hp == 0) return true;
949
927
  }
950
928
 
951
929
  if (volatiles.Nightmare) {
952
930
  stored.hp -= @max(stored.stats.hp / 4, 1);
953
931
  // ibid
954
- try options.log.damage(ident, stored, .Nightmare);
932
+ try options.log.damage(.{ ident, stored, .Nightmare });
955
933
  if (stored.hp == 0) return true;
956
934
  }
957
935
 
958
936
  if (volatiles.Curse) {
959
937
  stored.hp -= @max(stored.stats.hp / 4, 1);
960
938
  // ibid
961
- try options.log.damage(ident, stored, .Curse);
939
+ try options.log.damage(.{ ident, stored, .Curse });
962
940
  if (stored.hp == 0) return true;
963
941
  }
964
942
 
@@ -979,7 +957,7 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
979
957
  future_sight.count -= 1;
980
958
  assert(future_sight.count != 0);
981
959
  if (future_sight.count == 1) {
982
- try options.log.end(battle.active(player), .FutureSight);
960
+ try options.log.end(.{ battle.active(player), .FutureSight });
983
961
  // TODO doMove
984
962
  }
985
963
  }
@@ -988,21 +966,24 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
988
966
 
989
967
  // Weather
990
968
  if (battle.field.weather != .None) {
991
- assert(battle.field.weather_duration > 0);
992
969
  battle.field.weather_duration -= 1;
993
970
 
994
971
  if (battle.field.weather_duration == 0) {
995
972
  battle.field.weather = .None;
996
- try options.log.weather(battle.field.weather, .None);
973
+ try options.log.weather(.{ battle.field.weather, protocol.Weather.None });
997
974
  } else {
998
- try options.log.weather(battle.field.weather, .Upkeep);
975
+ try options.log.weather(.{ battle.field.weather, protocol.Weather.Upkeep });
999
976
  if (battle.field.weather == .Sandstorm) {
1000
977
  inline for (players) |player| {
1001
978
  var side = battle.side(player);
1002
979
  const active = side.active;
1003
980
  if (!(active.volatiles.Underground or active.types.sandstormImmune())) {
1004
981
  side.stored().hp -|= @max(side.stored().stats.hp / 8, 1);
1005
- 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
+ });
1006
987
  }
1007
988
  }
1008
989
  if (try checkFaint(battle, .P1, options)) |r| return r;
@@ -1014,19 +995,24 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1014
995
  inline for (players) |player| {
1015
996
  var side = battle.side(player);
1016
997
  var volatiles = &side.active.volatiles;
998
+ const ident = battle.active(player);
1017
999
 
1018
1000
  if (volatiles.bind.duration > 0 and !volatiles.Substitute) {
1019
1001
  volatiles.bind.duration -= 1;
1020
1002
  if (volatiles.bind.duration == 0) {
1021
1003
  const reason = @intFromEnum(End.Bind) + volatiles.bind.reason - 1;
1022
- try options.log.end(battle.active(player), @enumFromInt(reason));
1004
+ try options.log.end(.{ ident, @as(End, @enumFromInt(reason)) });
1023
1005
  } else {
1024
1006
  var reason = @intFromEnum(Activate.Bind) + volatiles.bind.reason - 1;
1025
- try options.log.activate(battle.active(player), @enumFromInt(reason));
1007
+ try options.log.activate(.{ ident, @as(Activate, @enumFromInt(reason)) });
1026
1008
 
1027
1009
  reason = @intFromEnum(Damage.Bind) + volatiles.bind.reason - 1;
1028
1010
  side.stored().hp -|= @max(side.stored().stats.hp / 16, 1);
1029
- 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
+ });
1030
1016
  }
1031
1017
  }
1032
1018
  }
@@ -1038,12 +1024,11 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1038
1024
  var volatiles = &side.active.volatiles;
1039
1025
 
1040
1026
  if (volatiles.PerishSong) {
1041
- assert(volatiles.perish_song > 0);
1042
1027
  volatiles.perish_song -= 1;
1043
1028
 
1044
1029
  assert(volatiles.perish_song < 3);
1045
1030
  const reason = @intFromEnum(Start.PerishSong0) + volatiles.perish_song;
1046
- try options.log.start(battle.active(player), @enumFromInt(reason));
1031
+ try options.log.start(.{ battle.active(player), @as(Start, @enumFromInt(reason)) });
1047
1032
 
1048
1033
  if (volatiles.perish_song == 0) {
1049
1034
  volatiles.PerishSong = false;
@@ -1060,13 +1045,15 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1060
1045
  if (stored.item == .Leftovers) {
1061
1046
  const before = stored.hp;
1062
1047
  stored.hp = @min(stored.hp + @max(stored.stats.hp / 16, 1), stored.stats.hp);
1063
- if (stored.hp > before) try options.log.heal(battle.active(player), stored, .Leftovers);
1048
+ if (stored.hp > before) {
1049
+ try options.log.heal(.{ battle.active(player), stored, .Leftovers });
1050
+ }
1064
1051
  }
1065
1052
  }
1066
1053
 
1067
1054
  // Mystery Berry
1068
1055
  inline for (players) |player| {
1069
- var stored = battle.side(player).stored();
1056
+ const stored = battle.side(player).stored();
1070
1057
 
1071
1058
  if (stored.item == .MysteryBerry) {
1072
1059
  // TODO stored and active moves, transform, etc
@@ -1081,7 +1068,7 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1081
1068
  !side.active.volatiles.frozen and
1082
1069
  try Rolls.defrost(battle, player, options))
1083
1070
  {
1084
- try options.log.curestatus(battle.active(player), side.stored().status, .Message);
1071
+ try options.log.curestatus(.{ battle.active(player), side.stored().status, .Message });
1085
1072
  side.stored().status = 0;
1086
1073
  }
1087
1074
  }
@@ -1122,14 +1109,14 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1122
1109
  stored.hp < stored.stats.hp;
1123
1110
  if (proc) {
1124
1111
  assert(stored.hp != 0);
1125
- try options.log.enditem(ident, stored.item, .Eat);
1112
+ try options.log.enditem(.{ ident, stored.item, .Eat });
1126
1113
  stored.item = .None;
1127
1114
 
1128
1115
  const offset = num - @intFromEnum(Item.Berry);
1129
1116
  assert(offset < 3);
1130
1117
  stored.hp = @min(stored.hp + ((offset + 1) * 10), stored.stats.hp);
1131
1118
  const reason: Heal = @enumFromInt(@intFromEnum(Heal.Berry) + offset);
1132
- try options.log.heal(ident, stored, reason);
1119
+ try options.log.heal(.{ ident, stored, reason });
1133
1120
  }
1134
1121
  } else if (num < @intFromEnum(Item.BitterBerry)) {
1135
1122
  const proc = if (stored.item == .MiracleBerry)
@@ -1139,7 +1126,7 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1139
1126
 
1140
1127
  if (proc) {
1141
1128
  assert(Status.any(stored.status));
1142
- try options.log.enditem(ident, stored.item, .Eat);
1129
+ try options.log.enditem(.{ ident, stored.item, .Eat });
1143
1130
  stored.item = .None;
1144
1131
 
1145
1132
  // FIXME wtf?
@@ -1147,21 +1134,21 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1147
1134
  // side.active.volatiles.Nightmare = false;
1148
1135
  if (stored.item == .MiracleBerry) {
1149
1136
  side.active.volatiles.Confusion = false;
1150
- try options.log.end(ident, .Confusion);
1137
+ try options.log.end(.{ ident, .Confusion });
1151
1138
  }
1152
1139
  stored.status = 0;
1153
1140
 
1154
1141
  // FIXME recalc stats
1155
1142
 
1156
- try options.log.curestatus(ident, stored.status, .Message);
1143
+ try options.log.curestatus(.{ ident, stored.status, .Message });
1157
1144
  }
1158
1145
  } else if (num == @intFromEnum(Item.BitterBerry)) {
1159
1146
  if (side.active.volatiles.Confusion) {
1160
- try options.log.enditem(ident, stored.item, .Eat);
1147
+ try options.log.enditem(.{ ident, stored.item, .Eat });
1161
1148
  stored.item = .None;
1162
1149
 
1163
1150
  side.active.volatiles.Confusion = false;
1164
- try options.log.end(ident, .Confusion);
1151
+ try options.log.end(.{ ident, .Confusion });
1165
1152
  }
1166
1153
  }
1167
1154
  }
@@ -1173,12 +1160,11 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1173
1160
  var volatiles = &side.active.volatiles;
1174
1161
 
1175
1162
  if (volatiles.Encore) {
1176
- assert(volatiles.encore > 0);
1177
1163
  volatiles.encore -= 1;
1178
1164
 
1179
1165
  if (volatiles.encore == 0 or side.active.move(mslot).pp == 0) {
1180
1166
  volatiles.Encore = false;
1181
- try options.log.end(battle.active(player), .Encore);
1167
+ try options.log.end(.{ battle.active(player), .Encore });
1182
1168
  }
1183
1169
  }
1184
1170
  }
@@ -1195,12 +1181,11 @@ fn updateSideCondition(
1195
1181
  ) !void {
1196
1182
  var side = battle.side(player);
1197
1183
  if (@field(side.conditions, key)) {
1198
- assert(@field(side.conditions, value) > 0);
1199
1184
  @field(side.conditions, value) -= 1;
1200
1185
 
1201
1186
  if (@field(side.conditions, value) == 0) {
1202
1187
  @field(side.conditions, key) = false;
1203
- try options.log.sideend(player, @field(protocol.Side, key));
1188
+ try options.log.sideend(.{ player, @field(protocol.Side, key) });
1204
1189
  }
1205
1190
  }
1206
1191
  }
@@ -1208,14 +1193,14 @@ fn updateSideCondition(
1208
1193
  fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result {
1209
1194
  battle.turn += 1;
1210
1195
 
1211
- if (showdown and battle.turn >= 1000) {
1212
- try options.log.tie();
1196
+ if (pkmn.options.mod and battle.turn >= 1000) {
1197
+ try options.log.tie(.{});
1213
1198
  return Result.Tie;
1214
1199
  } else if (battle.turn >= 65535) {
1215
1200
  return Result.Error;
1216
1201
  }
1217
1202
 
1218
- try options.log.turn(battle.turn);
1203
+ try options.log.turn(.{battle.turn});
1219
1204
 
1220
1205
  return Result.Default;
1221
1206
  }
@@ -1231,7 +1216,7 @@ pub fn buildRage(battle: anytype, player: Player, state: *State, options: anytyp
1231
1216
  }
1232
1217
  } else {
1233
1218
  foe.active.volatiles.rage +|= 1;
1234
- try options.log.activate(battle.active(player), .Rage);
1219
+ try options.log.activate(.{ battle.active(player), .Rage });
1235
1220
  }
1236
1221
  }
1237
1222
  }
@@ -1254,7 +1239,7 @@ pub fn destinyBond(battle: anytype, player: Player, _: *State, options: anytype)
1254
1239
 
1255
1240
  if (foe.active.volatiles.DestinyBond) {
1256
1241
  battle.side(player).stored().hp = 0;
1257
- try options.log.activate(battle.active(player.foe()), .DestinyBond);
1242
+ try options.log.activate(.{ battle.active(player.foe()), .DestinyBond });
1258
1243
  }
1259
1244
  return true;
1260
1245
  }
@@ -1267,14 +1252,14 @@ pub const Effects = struct {
1267
1252
  const gender: u8 = @intFromEnum(side.stored().dvs.gender);
1268
1253
  const foe_gender: u8 = @intFromEnum(foe.stored().dvs.gender);
1269
1254
  if (gender + foe_gender != 1) {
1270
- return options.log.immune(battle.active(player.foe()), .None);
1255
+ return options.log.immune(.{ battle.active(player.foe()), .None });
1271
1256
  } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
1272
- try options.log.lastmiss();
1273
- return options.log.miss(battle.active(player));
1257
+ try options.log.lastmiss(.{});
1258
+ return options.log.miss(.{battle.active(player)});
1274
1259
  }
1275
1260
 
1276
1261
  foe.active.volatiles.Attract = true;
1277
- try options.log.start(battle.active(player.foe()), .Attract);
1262
+ try options.log.start(.{ battle.active(player.foe()), .Attract });
1278
1263
  }
1279
1264
 
1280
1265
  pub fn batonPass(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1286,7 +1271,32 @@ pub const Effects = struct {
1286
1271
  }
1287
1272
 
1288
1273
  pub fn bellyDrum(battle: anytype, player: Player, state: *State, options: anytype) !void {
1289
- _ = .{ battle, player, state, options }; // TODO
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
+ }
1290
1300
  }
1291
1301
 
1292
1302
  pub const bide = struct {
@@ -1298,18 +1308,11 @@ pub const Effects = struct {
1298
1308
 
1299
1309
  if (!volatiles.Bide) return;
1300
1310
 
1301
- assert(volatiles.attacks > 0);
1302
- // if (options.calc.modify(player, .attacking)) |extend| {
1303
- // if (!extend) volatiles.attacks = 0;
1304
- // } else {
1305
1311
  volatiles.attacks -= 1;
1306
- // }
1307
- try options.chance.attacking(player, volatiles.attacks);
1308
-
1309
- if (volatiles.attacks != 0) return options.log.activate(ident, .Bide);
1312
+ if (volatiles.attacks != 0) return options.log.activate(.{ ident, .Bide });
1310
1313
 
1311
1314
  volatiles.Bide = false;
1312
- try options.log.end(ident, .Bide);
1315
+ try options.log.end(.{ ident, .Bide });
1313
1316
 
1314
1317
  state.damage = volatiles.bide *% 2;
1315
1318
  volatiles.bide = 0;
@@ -1323,7 +1326,16 @@ pub const Effects = struct {
1323
1326
  };
1324
1327
 
1325
1328
  pub fn binding(battle: anytype, player: Player, state: *State, options: anytype) !void {
1326
- _ = .{ battle, player, state, options }; // TODO
1329
+ var foe = battle.foe(player);
1330
+
1331
+ if (state.miss) return;
1332
+ if (foe.active.volatiles.bind.duration > 0) return;
1333
+ if (foe.active.volatiles.Substitute) return;
1334
+
1335
+ foe.active.volatiles.bind = .{
1336
+ .duration = Rolls.bindingDuration(battle, player, options),
1337
+ .reason = Move.get(state.move).extra.protocol,
1338
+ };
1327
1339
  }
1328
1340
 
1329
1341
  pub fn burnChance(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1334,11 +1346,11 @@ pub const Effects = struct {
1334
1346
  if (Status.any(foe_stored.status)) {
1335
1347
  if (Status.is(foe_stored.status, .FRZ)) {
1336
1348
  assert(Move.get(state.move).type == .Fire or state.move == .TriAttack);
1337
- try options.log.curestatus(
1349
+ try options.log.curestatus(.{
1338
1350
  battle.active(player.foe()),
1339
1351
  foe_stored.status,
1340
1352
  .Message,
1341
- );
1353
+ });
1342
1354
  foe_stored.status = 0;
1343
1355
  }
1344
1356
  return;
@@ -1353,7 +1365,8 @@ pub const Effects = struct {
1353
1365
 
1354
1366
  foe_stored.status = Status.init(.BRN);
1355
1367
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
1356
- 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 });
1357
1370
 
1358
1371
  // TODO: check for status berry
1359
1372
  }
@@ -1373,7 +1386,7 @@ pub const Effects = struct {
1373
1386
  for (side.active.moves) |m| {
1374
1387
  if (convertible(side.active, m.id)) n += 1;
1375
1388
  }
1376
- if (n == 0) return options.log.fail(battle.active(player), .None);
1389
+ if (n == 0) return options.log.fail(.{ battle.active(player), .None });
1377
1390
 
1378
1391
  side.active.types.type1 = try Rolls.conversion(battle, player, n, options);
1379
1392
  side.active.types.type2 = side.active.types.type1;
@@ -1383,20 +1396,88 @@ pub const Effects = struct {
1383
1396
  var side = battle.side(player);
1384
1397
 
1385
1398
  const last = battle.foe(player).lastMove(false);
1386
- if (last == .None) return options.log.fail(battle.active(player), .None);
1399
+ if (last == .None) return options.log.fail(.{ battle.active(player), .None });
1387
1400
  const move = Move.get(last);
1388
- if (move.type == .@"???") return options.log.fail(battle.active(player), .None);
1401
+ if (move.type == .@"???") return options.log.fail(.{ battle.active(player), .None });
1389
1402
 
1390
1403
  side.active.types.type1 = try Rolls.conversion2(battle, player, move.type, options);
1391
1404
  side.active.types.type2 = side.active.types.type1;
1392
1405
  }
1393
1406
 
1394
- pub fn counter(battle: anytype, player: Player, state: *State, options: anytype) !void {
1395
- _ = .{ battle, player, state, options }; // TODO
1407
+ pub fn counter(battle: anytype, player: Player, state: *State, _: anytype) !void {
1408
+ assert(state.move == .Counter or state.move == .MirrorCoat);
1409
+
1410
+ const foe = battle.foe(player);
1411
+
1412
+ state.miss = true;
1413
+ const last = foe.lastMove(false);
1414
+
1415
+ if (last == .None) return;
1416
+ if (last == state.move) return;
1417
+
1418
+ const immune = state.immune();
1419
+ state.effectiveness = Effectiveness.neutral;
1420
+ if (immune) {
1421
+ state.damage = 0;
1422
+ return;
1423
+ }
1424
+
1425
+ const move = Move.get(last);
1426
+ if (state.bp == 0) return;
1427
+ if (move.type.special() != (last == .MirrorCoat)) return;
1428
+ if (state.damage == 0) return;
1429
+
1430
+ // NOTE: llvm/llvm-project#58557
1431
+ state.damage = smul(state.damage, 2);
1432
+ state.miss = false;
1396
1433
  }
1397
1434
 
1398
1435
  pub fn curse(battle: anytype, player: Player, state: *State, options: anytype) !void {
1399
- _ = .{ battle, player, state, options }; // TODO
1436
+ var side = battle.side(player);
1437
+ var foe = battle.foe(player);
1438
+
1439
+ const ident = battle.active(player);
1440
+ const foe_ident = battle.active(player.foe());
1441
+
1442
+ if (side.active.types.includes(.Ghost)) {
1443
+ if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
1444
+ try options.log.lastmiss(.{});
1445
+ return options.log.miss(.{battle.active(player)});
1446
+ } else if (foe.active.volatiles.Substitute) {
1447
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
1448
+ } else if (foe.active.volatiles.Curse) {
1449
+ return options.log.fail(.{ foe_ident, .None });
1450
+ }
1451
+
1452
+ foe.active.volatiles.Curse = true;
1453
+ try options.log.start(.{ foe_ident, .Curse });
1454
+
1455
+ side.stored().hp -= @max(side.stored().stats.hp / 2, 1);
1456
+ try options.log.damage(.{ ident, side.stored(), .Curse });
1457
+ } else {
1458
+ const stats = &side.active.stats;
1459
+ const boosts = &side.active.boosts;
1460
+
1461
+ const max_atk = boosts.atk == 6 or stats.atk == MAX_STAT_VALUE;
1462
+ const max_def = boosts.def == 6 or stats.def == MAX_STAT_VALUE;
1463
+ if (max_atk and max_def) return options.log.fail(.{ ident, .None });
1464
+
1465
+ if (!(boosts.spe == -6 or stats.spe == 1)) {
1466
+ assert(boosts.spe >= -6 and boosts.spe <= 6);
1467
+ try unboostStat(battle, side, stats, boosts, "spe", 1);
1468
+ try options.log.boost(.{ ident, .Speed, @as(i8, -1) });
1469
+ }
1470
+ if (!max_atk) {
1471
+ assert(boosts.atk >= -6 and boosts.atk <= 6);
1472
+ try boostStat(battle, side, stats, boosts, "atk", 1);
1473
+ try options.log.boost(.{ ident, .Attack, @as(i8, 1) });
1474
+ }
1475
+ if (!max_def) {
1476
+ assert(boosts.def >= -6 and boosts.def <= 6);
1477
+ try boostStat(battle, side, stats, boosts, "def", 1);
1478
+ try options.log.boost(.{ ident, .Defense, @as(i8, 1) });
1479
+ }
1480
+ }
1400
1481
  }
1401
1482
 
1402
1483
  pub fn defenseCurl(battle: anytype, player: Player, _: *State, _: anytype) !void {
@@ -1406,14 +1487,13 @@ pub const Effects = struct {
1406
1487
  pub fn defrost(battle: anytype, player: Player, _: *State, options: anytype) !void {
1407
1488
  var side = battle.side(player);
1408
1489
  assert(Status.is(side.stored().status, .FRZ));
1409
-
1410
- try options.log.curestatus(battle.active(player), side.stored().status, .Message);
1490
+ try options.log.curestatus(.{ battle.active(player), side.stored().status, .Message });
1411
1491
  side.stored().status = 0;
1412
1492
  }
1413
1493
 
1414
1494
  pub fn destinyBond(battle: anytype, player: Player, _: *State, options: anytype) !void {
1415
1495
  battle.side(player).active.volatiles.DestinyBond = true;
1416
- try options.log.singlemove(battle.active(player), Move.DestinyBond);
1496
+ try options.log.singlemove(.{ battle.active(player), Move.DestinyBond });
1417
1497
  }
1418
1498
 
1419
1499
  pub fn disable(battle: anytype, player: Player, _: *State, options: anytype) !void {
@@ -1421,14 +1501,10 @@ pub const Effects = struct {
1421
1501
  var volatiles = &foe.active.volatiles;
1422
1502
  const foe_ident = battle.active(player.foe());
1423
1503
 
1424
- if (volatiles.disable.move != 0) {
1425
- return options.log.fail(foe_ident, .None);
1426
- }
1504
+ if (volatiles.disable.move != 0) return options.log.fail(.{ foe_ident, .None });
1427
1505
 
1428
1506
  const last = battle.foe(player).lastMove(false);
1429
- if (last == .None or last == .Struggle) {
1430
- return options.log.fail(foe_ident, .None);
1431
- }
1507
+ if (last == .None or last == .Struggle) return options.log.fail(.{ foe_ident, .None });
1432
1508
 
1433
1509
  var slot: u3 = 0;
1434
1510
  for (foe.active.moves) |m| {
@@ -1437,37 +1513,41 @@ pub const Effects = struct {
1437
1513
  if (m.id == last) break;
1438
1514
  }
1439
1515
  const move = foe.active.move(slot);
1440
- if (move.pp == 0) return options.log.fail(foe_ident, .None);
1516
+ if (move.pp == 0) return options.log.fail(.{ foe_ident, .None });
1441
1517
 
1442
1518
  volatiles.disable.move = slot;
1443
1519
  volatiles.disable.duration = Rolls.disableDuration(battle, player, options);
1444
1520
 
1445
- try options.log.startEffect(foe_ident, .Disable, move.id);
1521
+ try options.log.start(.{ foe_ident, .Disable, move.id });
1446
1522
  }
1447
1523
 
1448
1524
  pub const double = struct {
1449
1525
  pub fn flying(battle: anytype, player: Player, state: *State, _: anytype) !void {
1450
- 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);
1451
1528
  }
1452
1529
 
1453
1530
  pub fn underground(battle: anytype, player: Player, state: *State, _: anytype) !void {
1454
- 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);
1455
1533
  }
1456
1534
 
1457
1535
  pub fn minimize(battle: anytype, player: Player, state: *State, _: anytype) !void {
1458
- 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);
1459
1538
  }
1460
1539
  };
1461
1540
 
1462
1541
  pub fn drainHP(battle: anytype, player: Player, state: *State, options: anytype) !void {
1463
1542
  var stored = battle.side(player).stored();
1543
+ const ident = battle.active(player);
1464
1544
 
1465
1545
  assert(state.damage != 0);
1466
1546
 
1467
1547
  if (stored.hp == stored.stats.hp) return;
1468
1548
  stored.hp = @min(stored.stats.hp, stored.hp + @max(state.damage / 2, 1));
1469
1549
 
1470
- try options.log.drain(battle.active(player), stored, battle.active(player.foe()));
1550
+ try options.log.heal(.{ ident, stored, .Drain, battle.active(player.foe()) });
1471
1551
  }
1472
1552
 
1473
1553
  pub fn encore(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1479,7 +1559,7 @@ pub const Effects = struct {
1479
1559
  const failed = volatiles.Encore or
1480
1560
  (last == .None or last == .Struggle or
1481
1561
  last == .Encore or .last == .MirrorMove);
1482
- if (failed) return options.log.fail(foe_ident, .None);
1562
+ if (failed) return options.log.fail(.{ foe_ident, .None });
1483
1563
 
1484
1564
  var slot: u3 = 0;
1485
1565
  for (foe.active.moves) |m| {
@@ -1488,7 +1568,7 @@ pub const Effects = struct {
1488
1568
  if (m.id == last) break;
1489
1569
  }
1490
1570
  const move = foe.active.move(slot);
1491
- if (move.pp == 0) return options.log.fail(foe_ident, .None);
1571
+ if (move.pp == 0) return options.log.fail(.{ foe_ident, .None });
1492
1572
 
1493
1573
  volatiles.Encore = true;
1494
1574
  volatiles.encore = Rolls.encoreDuration(battle, player, options);
@@ -1496,11 +1576,19 @@ pub const Effects = struct {
1496
1576
  // TODO ???
1497
1577
  _ = state;
1498
1578
 
1499
- try options.log.startEffect(foe_ident, .Encore, move.id);
1579
+ try options.log.start(.{ foe_ident, .Encore, move.id });
1500
1580
  }
1501
1581
 
1502
- pub fn explode(battle: anytype, player: Player, state: *State, options: anytype) !void {
1503
- _ = .{ 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;
1504
1592
  }
1505
1593
 
1506
1594
  pub fn falseSwipe(battle: anytype, player: Player, state: *State, _: anytype) !void {
@@ -1550,12 +1638,11 @@ pub const Effects = struct {
1550
1638
  var side = battle.side(player);
1551
1639
 
1552
1640
  if (side.active.volatiles.FocusEnergy) {
1553
- if (!showdown) try options.log.fail(battle.active(player), .None);
1554
- return;
1641
+ return if (!showdown) try options.log.fail(.{ battle.active(player), .None });
1555
1642
  }
1556
1643
 
1557
1644
  side.active.volatiles.FocusEnergy = true;
1558
- try options.log.start(battle.active(player), .FocusEnergy);
1645
+ try options.log.start(.{ battle.active(player), .FocusEnergy });
1559
1646
  }
1560
1647
 
1561
1648
  var SLOTS: [5]u4 = .{ 0, 0, 0, 0, 0 };
@@ -1565,7 +1652,7 @@ pub const Effects = struct {
1565
1652
 
1566
1653
  const foe = battle.foe(player);
1567
1654
 
1568
- if (state.first) return options.log.fail(battle.active(player), .None);
1655
+ if (state.first) return options.log.fail(.{ battle.active(player), .None });
1569
1656
 
1570
1657
  var n: u4 = 0;
1571
1658
  var i: u4 = 0;
@@ -1579,7 +1666,7 @@ pub const Effects = struct {
1579
1666
  }
1580
1667
  }
1581
1668
 
1582
- if (n == 0) return options.log.fail(battle.active(player), .None);
1669
+ if (n == 0) return options.log.fail(.{ battle.active(player), .None });
1583
1670
 
1584
1671
  const slot = try Rolls.forceSwitch(battle, player, SLOTS[0..i], n, options);
1585
1672
  try switchIn(battle, player.foe(), slot, .Drag, options);
@@ -1589,14 +1676,14 @@ pub const Effects = struct {
1589
1676
  var foe = battle.foe(player);
1590
1677
 
1591
1678
  if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
1592
- try options.log.lastmiss();
1593
- return options.log.miss(battle.active(player));
1679
+ try options.log.lastmiss(.{});
1680
+ return options.log.miss(.{battle.active(player)});
1594
1681
  } else if (foe.active.volatiles.Foresight) {
1595
- return options.log.fail(battle.active(player), .None);
1682
+ return options.log.fail(.{ battle.active(player), .None });
1596
1683
  }
1597
1684
 
1598
1685
  foe.active.volatiles.Foresight = true;
1599
- try options.log.start(battle.active(player.foe()), .FocusEnergy);
1686
+ try options.log.start(.{ battle.active(player.foe()), .FocusEnergy });
1600
1687
  }
1601
1688
 
1602
1689
  pub fn freezeChance(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1616,10 +1703,10 @@ pub const Effects = struct {
1616
1703
  if (!state.proc) return;
1617
1704
  if (foe.conditions.Safeguard) return;
1618
1705
  // Freeze Clause Mod
1619
- 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;
1620
1707
 
1621
1708
  foe_stored.status = Status.init(.FRZ);
1622
- try options.log.status(foe_ident, foe_stored.status, .None);
1709
+ try options.log.status(.{ foe_ident, foe_stored.status, .None });
1623
1710
 
1624
1711
  // TODO: check for status berry
1625
1712
 
@@ -1627,8 +1714,26 @@ pub const Effects = struct {
1627
1714
  foe.active.volatiles.frozen = true;
1628
1715
  }
1629
1716
 
1630
- pub fn furyCutter(battle: anytype, player: Player, state: *State, options: anytype) !void {
1631
- _ = .{ 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
+ }
1632
1737
  }
1633
1738
 
1634
1739
  pub fn futureSight(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1641,42 +1746,131 @@ pub const Effects = struct {
1641
1746
  state.bp = ((if (frustration) 255 - val else val) * 10) / 25;
1642
1747
  }
1643
1748
 
1644
- pub fn haze(battle: anytype, player: Player, state: *State, options: anytype) !void {
1645
- _ = .{ battle, player, state, options }; // TODO
1749
+ pub fn haze(battle: anytype, player: Player, _: *State, options: anytype) !void {
1750
+ var side = battle.side(player);
1751
+ var stats = unmodifiedStats(battle, side);
1752
+ side.active.boosts = .{};
1753
+ inline for (STATS) |s| @field(side.active.stats, s) = @field(stats, s);
1754
+ statusModify(side.stored().status, &side.active.stats);
1755
+
1756
+ var foe = battle.foe(player);
1757
+ stats = unmodifiedStats(battle, foe);
1758
+ foe.active.boosts = .{};
1759
+ inline for (STATS) |s| @field(foe.active.stats, s) = @field(stats, s);
1760
+ statusModify(foe.stored().status, &foe.active.stats);
1761
+
1762
+ try options.log.clearallboost(.{});
1646
1763
  }
1647
1764
 
1648
1765
  pub fn heal(battle: anytype, player: Player, state: *State, options: anytype) !void {
1649
- _ = .{ battle, player, state, options }; // TODO
1766
+ var side = battle.side(player);
1767
+ var stored = side.stored();
1768
+ const ident = battle.active(player);
1769
+
1770
+ if (stored.stats.hp == stored.hp) return options.log.fail(.{ ident, .None });
1771
+
1772
+ const rest = state.move == .Rest;
1773
+ if (rest) {
1774
+ // Adding the sleep status runs the sleep condition handler to roll duration
1775
+ if (showdown) battle.rng.advance(1);
1776
+ side.active.volatiles.Toxic = false;
1777
+ stored.status = Status.slf(2);
1778
+ recomputeStats(battle, side);
1779
+ try options.log.status(.{ ident, stored.status, .From, Move.Rest });
1780
+ stored.hp = stored.stats.hp;
1781
+ } else {
1782
+ stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
1783
+ }
1784
+ try options.log.heal(.{ ident, stored, if (rest) Heal.Silent else Heal.None });
1650
1785
  }
1651
1786
 
1652
- pub fn healBell(battle: anytype, player: Player, state: *State, options: anytype) !void {
1653
- _ = .{ battle, player, state, options }; // TODO
1787
+ pub fn healBell(battle: anytype, player: Player, _: *State, options: anytype) !void {
1788
+ var side = battle.side(player);
1789
+ side.active.volatiles.Nightmare = false;
1790
+
1791
+ try options.log.cureteam(.{battle.active(player)});
1792
+ for (&side.pokemon) |*p| p.status = 0;
1793
+
1794
+ // We technically only need to recompute stats if the active Pokémon was burned/paralyzed,
1795
+ // but adding a branch to check for that is probably more expensive than just recomputing
1796
+ recomputeStats(battle, side);
1654
1797
  }
1655
1798
 
1656
- pub fn hiddenPower(battle: anytype, player: Player, state: *State, options: anytype) !void {
1657
- _ = .{ battle, player, state, options }; // TODO
1799
+ pub fn hiddenPower(battle: anytype, player: Player, state: *State, _: anytype) !void {
1800
+ const side = battle.side(player);
1801
+ state.bp = side.stored().dvs.power();
1802
+ state.type = side.stored().dvs.type;
1658
1803
  }
1659
1804
 
1660
- pub fn hyperBeam(battle: anytype, player: Player, state: *State, options: anytype) !void {
1661
- _ = .{ battle, player, state, options }; // TODO
1805
+ pub fn hyperBeam(battle: anytype, player: Player, _: *State, options: anytype) !void {
1806
+ battle.side(player).active.volatiles.Recharging = true;
1807
+ try options.log.mustrecharge(.{battle.active(player)});
1662
1808
  }
1663
1809
 
1664
1810
  pub fn leechSeed(battle: anytype, player: Player, state: *State, options: anytype) !void {
1665
- _ = .{ battle, player, state, options }; // TODO
1811
+ var foe = battle.foe(player);
1812
+
1813
+ if (showdown) {
1814
+ // Invulnerability trumps type immunity on Pokémon Showdown
1815
+ const invulnerable = foe.active.volatiles.Underground or foe.active.volatiles.Flying;
1816
+ if (!invulnerable and foe.active.types.includes(.Grass)) {
1817
+ return options.log.immune(.{ battle.active(player.foe()), .None });
1818
+ }
1819
+ try checkHit(battle, player, state, options);
1820
+ if (state.miss) {
1821
+ try options.log.lastmiss(.{});
1822
+ return options.log.miss(.{battle.active(player)});
1823
+ }
1824
+ if (foe.active.volatiles.LeechSeed) return;
1825
+ } else {
1826
+ try checkHit(battle, player, state, options);
1827
+ const miss =
1828
+ state.miss or foe.active.types.includes(.Grass) or foe.active.volatiles.LeechSeed;
1829
+ if (miss) {
1830
+ try options.log.lastmiss(.{});
1831
+ return options.log.miss(.{battle.active(player)});
1832
+ }
1833
+ }
1834
+
1835
+ foe.active.volatiles.LeechSeed = true;
1836
+
1837
+ try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
1666
1838
  }
1667
1839
 
1668
1840
  pub fn lockOn(battle: anytype, player: Player, state: *State, options: anytype) !void {
1669
- _ = .{ battle, player, state, options }; // TODO
1841
+ var foe = battle.foe(player);
1842
+ const foe_ident = battle.active(player.foe());
1843
+
1844
+ if (foe.active.volatiles.Substitute) {
1845
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
1846
+ } else if (state.miss) {
1847
+ return options.log.fail(.{ foe_ident, .None });
1848
+ }
1849
+
1850
+ foe.active.volatiles.LockOn = true;
1851
+ try options.log.activate(.{ battle.active(player), .LockOn });
1670
1852
  }
1671
1853
 
1672
1854
  const MAGNITUDE_POWER = [_]u8{ 10, 30, 50, 70, 90, 110, 150 };
1673
1855
 
1674
1856
  pub fn magnitude(battle: anytype, player: Player, state: *State, options: anytype) !void {
1675
- _ = .{ battle, player, state, options }; // TODO
1857
+ const num = try Rolls.magnitude(battle, player, options);
1858
+ try options.log.activate(.{ battle.active(player), .Magnitude, num });
1859
+ state.bp = MAGNITUDE_POWER[num - 4];
1676
1860
  }
1677
1861
 
1678
- pub fn meanLook(battle: anytype, player: Player, state: *State, options: anytype) !void {
1679
- _ = .{ battle, player, state, options }; // TODO
1862
+ pub fn meanLook(battle: anytype, player: Player, _: *State, options: anytype) !void {
1863
+ var foe = battle.foe(player);
1864
+
1865
+ if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
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)});
1869
+ } else if (foe.active.volatiles.Trapped) {
1870
+ return options.log.fail(.{ battle.active(player.foe()), .None });
1871
+ }
1872
+
1873
+ foe.active.volatiles.Trapped = true;
1680
1874
  }
1681
1875
 
1682
1876
  pub fn metronome(
@@ -1700,11 +1894,29 @@ pub const Effects = struct {
1700
1894
  }
1701
1895
 
1702
1896
  pub fn mimic(battle: anytype, player: Player, state: *State, options: anytype) !void {
1703
- _ = .{ battle, player, state, options }; // TODO
1704
- }
1897
+ var side = battle.side(player);
1705
1898
 
1706
- pub fn mirrorCoat(battle: anytype, player: Player, state: *State, options: anytype) !void {
1707
- _ = .{ battle, player, state, options }; // TODO
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 });
1708
1920
  }
1709
1921
 
1710
1922
  pub fn mirrorMove(
@@ -1721,11 +1933,11 @@ pub const Effects = struct {
1721
1933
 
1722
1934
  const last = foe.lastMove(false);
1723
1935
 
1724
- if (last == .None) return options.log.fail(battle.active(player), .None);
1936
+ if (last == .None) return options.log.fail(.{ battle.active(player), .None });
1725
1937
  const knows = knows: for (side.active.moves) |m| {
1726
1938
  if (last == m.id) break :knows true;
1727
1939
  } else false;
1728
- if (knows) return options.log.fail(battle.active(player), .None);
1940
+ if (knows) return options.log.fail(.{ battle.active(player), .None });
1729
1941
 
1730
1942
  // if (wPlayerCharging) ???
1731
1943
  // wPlayerCharging = 1
@@ -1739,17 +1951,17 @@ pub const Effects = struct {
1739
1951
  pub fn mist(battle: anytype, player: Player, _: *State, options: anytype) !void {
1740
1952
  var side = battle.side(player);
1741
1953
 
1742
- 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 });
1743
1955
  side.active.volatiles.Mist = true;
1744
1956
 
1745
- try options.log.start(battle.active(player), .Mist);
1957
+ try options.log.start(.{ battle.active(player), .Mist });
1746
1958
  }
1747
1959
 
1748
1960
  pub fn weatherHeal(battle: anytype, player: Player, _: *State, options: anytype) !void {
1749
1961
  var stored = battle.side(player).stored();
1750
1962
  const ident = battle.active(player);
1751
1963
 
1752
- 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 });
1753
1965
 
1754
1966
  stored.hp = switch (battle.field.weather) {
1755
1967
  .Sun => stored.stats.hp,
@@ -1757,7 +1969,7 @@ pub const Effects = struct {
1757
1969
  else => @min(stored.stats.hp, stored.hp + (stored.stats.hp / 4)),
1758
1970
  };
1759
1971
 
1760
- try options.log.heal(ident, stored, Heal.None);
1972
+ try options.log.heal(.{ ident, stored, .None });
1761
1973
  }
1762
1974
 
1763
1975
  pub fn multiHit(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1770,17 +1982,17 @@ pub const Effects = struct {
1770
1982
  const foe_ident = battle.active(player.foe());
1771
1983
 
1772
1984
  if (foe_volatiles.Flying or foe_volatiles.Underground) {
1773
- if (!showdown) return options.log.fail(battle.active(player.foe()), .None);
1774
- try options.log.lastmiss();
1775
- 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)});
1776
1988
  } else if (foe_volatiles.Substitute) {
1777
- return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
1989
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
1778
1990
  } else if (!Status.is(foe.stored().status, .SLP) or foe_volatiles.Nightmare) {
1779
- return options.log.fail(foe_ident, .None);
1991
+ return options.log.fail(.{ foe_ident, .None });
1780
1992
  }
1781
1993
 
1782
1994
  foe_volatiles.Nightmare = true;
1783
- try options.log.start(battle.active(player.foe()), .Nightmare);
1995
+ try options.log.start(.{ battle.active(player.foe()), .Nightmare });
1784
1996
  }
1785
1997
 
1786
1998
  pub const ohko = struct {
@@ -1804,9 +2016,9 @@ pub const Effects = struct {
1804
2016
  const foe_ident = battle.active(player.foe());
1805
2017
 
1806
2018
  state.damage = 0;
1807
- if (state.immune()) return options.log.immune(foe_ident, .None);
2019
+ if (state.immune()) return options.log.immune(.{ foe_ident, .None });
1808
2020
  const delta = side.stored().level -| foe.stored().level;
1809
- if (delta == 0) return options.log.immune(foe_ident, .OHKO);
2021
+ if (delta == 0) return options.log.immune(.{ foe_ident, .OHKO });
1810
2022
 
1811
2023
  state.accuracy = 2 * delta +| Gen12.percent(30);
1812
2024
  try checkHit(battle, player, state, options);
@@ -1822,37 +2034,39 @@ pub const Effects = struct {
1822
2034
  const ident = battle.active(player);
1823
2035
  const foe_ident = battle.active(player.foe());
1824
2036
 
1825
- if (state.miss) return options.log.fail(foe_ident, .None);
2037
+ if (state.miss) return options.log.fail(.{ foe_ident, .None });
1826
2038
  if (foe.active.volatiles.Substitute) {
1827
- return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
2039
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
1828
2040
  }
1829
2041
 
1830
2042
  const avg = @max((foe.stored().hp + side.stored().hp) / 2, 1);
1831
2043
  foe.stored().hp -= avg;
1832
2044
  side.stored().hp = avg;
1833
2045
 
1834
- try options.log.sethp(foe_ident, foe.stored(), .Silent);
1835
- 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 });
1836
2048
  }
1837
2049
 
1838
2050
  pub fn paralyze(battle: anytype, player: Player, state: *State, options: anytype) !void {
1839
2051
  var foe = battle.foe(player);
1840
2052
  const foe_ident = battle.active(player.foe());
1841
2053
 
1842
- if (Status.is(foe.stored().status, .PAR)) return options.log.fail(foe_ident, .Paralysis);
1843
- if (state.immune()) return options.log.immune(foe_ident, .None);
1844
- 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 });
1845
2059
  if (state.miss) {
1846
- try options.log.lastmiss();
1847
- return options.log.miss(battle.active(player));
2060
+ try options.log.lastmiss(.{});
2061
+ return options.log.miss(.{battle.active(player)});
1848
2062
  } else if (foe.active.volatiles.Substitute) {
1849
- return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
2063
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
1850
2064
  }
1851
2065
 
1852
2066
  foe.stored().status = Status.init(.PAR);
1853
2067
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
1854
2068
 
1855
- try options.log.status(foe_ident, foe.stored().status, .None);
2069
+ try options.log.status(.{ foe_ident, foe.stored().status, .None });
1856
2070
  }
1857
2071
 
1858
2072
  pub fn paralyzeChance(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1867,14 +2081,15 @@ pub const Effects = struct {
1867
2081
 
1868
2082
  foe_stored.status = Status.init(.PAR);
1869
2083
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
1870
- 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 });
1871
2086
 
1872
2087
  // TODO: check for status berry
1873
2088
  }
1874
2089
 
1875
2090
  pub fn payDay(battle: anytype, player: Player, _: *State, options: anytype) !void {
1876
2091
  if (!showdown or !battle.foe(player).active.volatiles.Substitute) {
1877
- try options.log.fieldactivate(); // FIXME .PayDay
2092
+ try options.log.fieldactivate(.{}); // FIXME .PayDay
1878
2093
  }
1879
2094
  }
1880
2095
 
@@ -1883,17 +2098,17 @@ pub const Effects = struct {
1883
2098
  var foe = battle.foe(player);
1884
2099
 
1885
2100
  if (foe.active.volatiles.PerishSong) {
1886
- return options.log.fail(battle.active(player.foe()), .None);
2101
+ return options.log.fail(.{ battle.active(player.foe()), .None });
1887
2102
  } else if (!side.active.volatiles.PerishSong) {
1888
2103
  side.active.volatiles.PerishSong = true;
1889
2104
  side.active.volatiles.perish_song = 4;
1890
- try options.log.start(battle.active(player), .PerishSong3Silent);
2105
+ try options.log.start(.{ battle.active(player), .PerishSong3Silent });
1891
2106
  }
1892
2107
  foe.active.volatiles.PerishSong = true;
1893
2108
  foe.active.volatiles.perish_song = 4;
1894
- try options.log.start(battle.active(player.foe()), .PerishSong3Silent);
2109
+ try options.log.start(.{ battle.active(player.foe()), .PerishSong3Silent });
1895
2110
 
1896
- try options.log.fieldactivate(); // FIXME .PerishSong
2111
+ try options.log.fieldactivate(.{}); // FIXME .PerishSong
1897
2112
  }
1898
2113
 
1899
2114
  pub fn poison(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1903,24 +2118,24 @@ pub const Effects = struct {
1903
2118
 
1904
2119
  const toxic = state.move == .Toxic;
1905
2120
  if (state.immune() or foe.active.types.includes(.Poison)) {
1906
- return options.log.immune(foe_ident, .None);
2121
+ return options.log.immune(.{ foe_ident, .None });
1907
2122
  } else if (Status.any(foe_stored.status)) {
1908
2123
  // Pokémon Showdown considers Toxic to be a status even in Generation II and so
1909
2124
  // will not include a fail reason for Toxic vs. Poison or vice-versa...
1910
- 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))
1911
2126
  if (!showdown)
1912
- .Poison
2127
+ Fail.Poison
1913
2128
  else if (toxic == (foe_stored.status == Status.TOX))
1914
- if (toxic) .Toxic else .Poison
2129
+ if (toxic) Fail.Toxic else Fail.Poison
1915
2130
  else
1916
- .None
2131
+ Fail.None
1917
2132
  else
1918
- .None);
2133
+ Fail.None });
1919
2134
  } else if (foe.active.volatiles.Substitute) {
1920
- return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
2135
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
1921
2136
  } else if (state.miss) {
1922
- try options.log.lastmiss();
1923
- return options.log.miss(battle.active(player));
2137
+ try options.log.lastmiss(.{});
2138
+ return options.log.miss(.{battle.active(player)});
1924
2139
  }
1925
2140
 
1926
2141
  foe_stored.status = Status.init(.PSN);
@@ -1930,7 +2145,7 @@ pub const Effects = struct {
1930
2145
  foe.active.volatiles.toxic = 0;
1931
2146
  }
1932
2147
 
1933
- try options.log.status(foe_ident, foe_stored.status, .None);
2148
+ try options.log.status(.{ foe_ident, foe_stored.status, .None });
1934
2149
 
1935
2150
  // TODO: check for status berry
1936
2151
  }
@@ -1946,7 +2161,8 @@ pub const Effects = struct {
1946
2161
  if (foe.conditions.Safeguard) return;
1947
2162
 
1948
2163
  foe_stored.status = Status.init(.PSN);
1949
- 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 });
1950
2166
 
1951
2167
  // TODO: check for status berry
1952
2168
  }
@@ -1961,9 +2177,7 @@ pub const Effects = struct {
1961
2177
 
1962
2178
  const failed = !state.first or volatiles.Substitute or
1963
2179
  !try Rolls.protect(battle, player, volatiles.protect, options);
1964
- if (failed) {
1965
- return options.log.fail(battle.active(player), .None);
1966
- }
2180
+ if (failed) return options.log.fail(.{ battle.active(player), .None });
1967
2181
 
1968
2182
  volatiles.protect = @max(8, volatiles.protect + 1);
1969
2183
  if (state.move == .Endure) {
@@ -1971,7 +2185,7 @@ pub const Effects = struct {
1971
2185
  } else {
1972
2186
  volatiles.Protect = true;
1973
2187
  }
1974
- try options.log.singleturn(battle.active(player), state.move);
2188
+ try options.log.singleturn(.{ battle.active(player), state.move });
1975
2189
  }
1976
2190
 
1977
2191
  pub fn psychUp(battle: anytype, player: Player, _: *State, options: anytype) !void {
@@ -1980,30 +2194,29 @@ pub const Effects = struct {
1980
2194
 
1981
2195
  // Pokémon Showdown should fail when the enemy has no boosts/unboosts but doesn't
1982
2196
  if (!showdown and @as(u32, @bitCast(foe.active.boosts)) == 0) {
1983
- return options.log.fail(battle.active(player), .None);
2197
+ return options.log.fail(.{ battle.active(player), .None });
1984
2198
  }
1985
2199
 
1986
2200
  side.active.boosts = foe.active.boosts;
1987
- try options.log.copyboost(battle.active(player), battle.active(player.foe()));
2201
+ try options.log.copyboost(.{ battle.active(player), battle.active(player.foe()) });
1988
2202
  }
1989
2203
 
1990
2204
  pub fn pursuit(battle: anytype, player: Player, state: *State, _: anytype) !void {
1991
2205
  // TODO: remove switching variable and just look at Choice saved in Battle struct?
1992
- if (battle.foe(player).active.volatiles.switching) state.damage *|= 2;
2206
+ if (battle.foe(player).active.volatiles.switching) state.damage = smul(state.damage, 2);
1993
2207
  }
1994
2208
 
1995
2209
  pub const rage = struct {
1996
2210
  pub fn start(battle: anytype, player: Player, _: *State, options: anytype) !void {
1997
2211
  battle.side(player).active.volatiles.Rage = true;
1998
- try options.log.singlemove(battle.active(player), Move.Rage);
2212
+ try options.log.singlemove(.{ battle.active(player), Move.Rage });
1999
2213
  }
2000
2214
 
2001
2215
  pub fn damage(battle: anytype, player: Player, state: *State, _: anytype) !void {
2002
- var side = battle.side(player);
2003
- var volatiles = &side.active.volatiles;
2216
+ const side = battle.side(player);
2004
2217
 
2005
- assert(volatiles.Rage);
2006
- state.damage *|= (volatiles.rage +| 1);
2218
+ assert(side.active.volatiles.Rage);
2219
+ state.damage = smul(state.damage, (side.active.volatiles.rage +| 1)); // TODO: |+
2007
2220
  }
2008
2221
  };
2009
2222
 
@@ -2015,18 +2228,18 @@ pub const Effects = struct {
2015
2228
  var side = battle.side(player);
2016
2229
 
2017
2230
  if (side.active.volatiles.LeechSeed) {
2018
- try options.log.end(battle.active(player), .LeechSeedFrom);
2231
+ try options.log.end(.{ battle.active(player), .LeechSeedFrom });
2019
2232
  side.active.volatiles.LeechSeed = false;
2020
2233
  }
2021
2234
 
2022
2235
  if (side.conditions.Spikes) {
2023
- try options.log.spikesend(battle.active(player));
2236
+ try options.log.sideend(.{ player, protocol.Side.Spikes, battle.active(player) });
2024
2237
  side.conditions.Spikes = false;
2025
2238
  }
2026
2239
 
2027
2240
  if (side.active.volatiles.bind.duration > 0) {
2028
2241
  const reason = @intFromEnum(End.Bind) + side.active.volatiles.bind.reason - 1;
2029
- try options.log.end(battle.active(player), @enumFromInt(reason));
2242
+ try options.log.end(.{ battle.active(player), @as(End, @enumFromInt(reason)) });
2030
2243
  side.active.volatiles.bind = .{};
2031
2244
  }
2032
2245
  }
@@ -2034,18 +2247,14 @@ pub const Effects = struct {
2034
2247
  pub fn recoil(battle: anytype, player: Player, state: *State, options: anytype) !void {
2035
2248
  var side = battle.side(player);
2036
2249
  var stored = side.stored();
2250
+ const ident = battle.active(player);
2037
2251
 
2038
2252
  assert(state.damage > 0);
2039
2253
 
2040
2254
  const damage: i16 = @intCast(@max(state.damage / 4, 1));
2041
2255
  stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
2042
2256
 
2043
- try options.log.damageOf(
2044
- battle.active(player),
2045
- stored,
2046
- .RecoilOf,
2047
- battle.active(player.foe()),
2048
- );
2257
+ try options.log.damage(.{ ident, stored, .RecoilOf, battle.active(player.foe()) });
2049
2258
  }
2050
2259
 
2051
2260
  pub fn reversal(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -2102,13 +2311,13 @@ pub const Effects = struct {
2102
2311
  side.active.volatiles.attacks += 1;
2103
2312
  side.active.volatiles.Rollout = side.active.volatiles.attacks != 5;
2104
2313
 
2105
- if (side.active.volatiles.DefenseCurl) state.damage *|= 2;
2314
+ if (side.active.volatiles.DefenseCurl) state.damage = smul(state.damage, 2);
2106
2315
  }
2107
2316
  };
2108
2317
 
2109
2318
  pub fn sandstorm(battle: anytype, player: Player, _: *State, options: anytype) !void {
2110
2319
  if (battle.field.weather != .Sandstorm) return weather(battle, .Sandstorm, options);
2111
- return options.log.fail(battle.active(player), .None);
2320
+ return options.log.fail(.{ battle.active(player), .None });
2112
2321
  }
2113
2322
 
2114
2323
  pub fn safeguard(battle: anytype, player: Player, _: *State, options: anytype) !void {
@@ -2132,11 +2341,13 @@ pub const Effects = struct {
2132
2341
  ) !void {
2133
2342
  var side = battle.side(player);
2134
2343
 
2135
- 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
+ }
2136
2347
 
2137
2348
  @field(side.conditions, key) = true;
2138
2349
  @field(side.conditions, value) = 5;
2139
- try options.log.sidestart(player, @field(protocol.Side, key));
2350
+ try options.log.sidestart(.{ player, @field(protocol.Side, key) });
2140
2351
  }
2141
2352
 
2142
2353
  pub fn sketch(battle: anytype, player: Player, _: *State, options: anytype) !void {
@@ -2147,9 +2358,9 @@ pub const Effects = struct {
2147
2358
 
2148
2359
  if (showdown) {
2149
2360
  // yes, actually "Splash". '|nothing' gets blindly mapped to '|-activate||move:Splash'
2150
- try options.log.activate(battle.active(player), .Splash);
2361
+ try options.log.activate(.{ battle.active(player), .Splash });
2151
2362
  } else {
2152
- try options.log.fail(battle.active(player), .None);
2363
+ try options.log.fail(.{ battle.active(player), .None });
2153
2364
  }
2154
2365
  }
2155
2366
 
@@ -2159,40 +2370,39 @@ pub const Effects = struct {
2159
2370
  const foe_ident = battle.active(player.foe());
2160
2371
 
2161
2372
  if (Status.any(foe_stored.status)) {
2162
- return options.log.fail(
2373
+ return options.log.fail(.{
2163
2374
  foe_ident,
2164
- if (Status.is(foe_stored.status, .SLP)) .Sleep else .None,
2165
- );
2375
+ if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
2376
+ });
2166
2377
  }
2167
2378
 
2168
2379
  if (foe.active.volatiles.Substitute) {
2169
- return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
2380
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
2170
2381
  }
2171
2382
 
2172
2383
  // Sleep Clause Mod
2173
- if (showdown) {
2384
+ if (pkmn.options.mod) {
2174
2385
  for (foe.pokemon) |p| {
2175
2386
  if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
2176
2387
  }
2177
2388
  }
2178
2389
 
2179
2390
  foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2180
- try options.log.statusFrom(foe_ident, foe_stored.status, state.move);
2391
+ try options.log.status(.{ foe_ident, foe_stored.status, .From, state.move });
2181
2392
  }
2182
2393
 
2183
2394
  pub fn sleepTalk(battle: anytype, player: Player, state: *State, options: anytype) !void {
2184
2395
  const side = battle.side(player);
2185
2396
 
2186
2397
  if (!Status.is(side.stored().status, .SLP)) {
2187
- if (!showdown) try options.log.fail(battle.active(player), .None);
2188
- return;
2398
+ return if (!showdown) try options.log.fail(.{ battle.active(player), .None });
2189
2399
  }
2190
2400
 
2191
2401
  var n: u3 = 0;
2192
2402
  for (side.active.moves, 0..) |m, i| {
2193
2403
  if (state.mslot - 1 != i and Move.get(m.id).extra.sleep_talk) n += 1;
2194
2404
  }
2195
- if (n == 0) return options.log.fail(battle.active(player), .None);
2405
+ if (n == 0) return options.log.fail(.{ battle.active(player), .None });
2196
2406
 
2197
2407
  _ = try Rolls.sleepTalk(battle, player, n, state.mslot, options); // TODO
2198
2408
  }
@@ -2201,55 +2411,75 @@ pub const Effects = struct {
2201
2411
  if (Status.is(battle.side(player).stored().status, .SLP)) return true;
2202
2412
  state.damage = 0;
2203
2413
  state.miss = true; // TODO is this necessary?
2204
- if (!showdown) try options.log.fail(battle.active(player), .None);
2414
+ if (!showdown) try options.log.fail(.{ battle.active(player), .None });
2205
2415
  return false;
2206
2416
  }
2207
2417
 
2208
2418
  pub fn spikes(battle: anytype, player: Player, _: *State, options: anytype) !void {
2209
2419
  var foe = battle.foe(player);
2210
- 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 });
2211
2421
 
2212
2422
  foe.conditions.Spikes = true;
2213
- try options.log.sidestart(player.foe(), .Spikes);
2423
+ try options.log.sidestart(.{ player.foe(), protocol.Side.Spikes });
2214
2424
  }
2215
2425
 
2216
2426
  pub fn spite(battle: anytype, player: Player, state: *State, options: anytype) !void {
2217
- _ = .{ battle, player, state, options }; // TODO
2218
- // var foe = battle.foe(player);
2427
+ var foe = battle.foe(player);
2219
2428
 
2220
- // 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
+ }
2221
2433
 
2222
- // const last = foe.lastMove(false);
2223
- // if (showdown) {
2224
- // const roll = try Rolls.spite(battle, player, options);
2225
- // } else {
2226
- // if (last != .None) {}
2227
- // }
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 });
2228
2458
  }
2229
2459
 
2230
2460
  pub fn splash(battle: anytype, player: Player, _: *State, options: anytype) !void {
2231
- try options.log.activate(battle.active(player), .Splash);
2461
+ try options.log.activate(.{ battle.active(player), .Splash });
2232
2462
  }
2233
2463
 
2234
2464
  pub fn substitute(battle: anytype, player: Player, _: *State, options: anytype) !void {
2235
2465
  var side = battle.side(player);
2236
2466
  if (side.active.volatiles.Substitute) {
2237
- return options.log.fail(battle.active(player), .Substitute);
2467
+ return options.log.fail(.{ battle.active(player), .Substitute });
2238
2468
  }
2239
2469
 
2240
2470
  assert(side.stored().stats.hp <= 1023);
2241
2471
  const hp: u8 = @intCast(side.stored().stats.hp / 4);
2242
2472
 
2243
2473
  if (hp == 0 or side.stored().hp <= hp) {
2244
- return options.log.fail(battle.active(player), .Weak);
2474
+ return options.log.fail(.{ battle.active(player), .Weak });
2245
2475
  }
2246
2476
 
2247
2477
  side.stored().hp -= hp;
2248
2478
  side.active.volatiles.substitute = hp;
2249
2479
  side.active.volatiles.Substitute = true;
2250
2480
  side.active.volatiles.bind = .{};
2251
- try options.log.start(battle.active(player), .Substitute);
2252
- 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 });
2253
2483
  }
2254
2484
 
2255
2485
  pub fn sunnyDay(battle: anytype, _: Player, _: *State, options: anytype) !void {
@@ -2259,14 +2489,14 @@ pub const Effects = struct {
2259
2489
  fn weather(battle: anytype, w: Weather, options: anytype) !void {
2260
2490
  battle.field.weather = w;
2261
2491
  battle.field.weather_duration = 5;
2262
- try options.log.weather(w, .None);
2492
+ try options.log.weather(.{ w, protocol.Weather.None });
2263
2493
  }
2264
2494
 
2265
2495
  pub fn teleport(battle: anytype, player: Player, _: *State, options: anytype) !void {
2266
2496
  if (showdown) return;
2267
2497
 
2268
- try options.log.fail(battle.active(player), .None);
2269
- try options.log.laststill();
2498
+ try options.log.fail(.{ battle.active(player), .None });
2499
+ try options.log.laststill(.{});
2270
2500
  }
2271
2501
 
2272
2502
  pub fn thief(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -2281,11 +2511,11 @@ pub const Effects = struct {
2281
2511
  side.stored().item = foe.stored().item;
2282
2512
  foe.stored().item = .None;
2283
2513
 
2284
- try options.log.item(
2514
+ try options.log.item(.{
2285
2515
  battle.active(player),
2286
2516
  side.stored().item,
2287
2517
  battle.active(player.foe()),
2288
- );
2518
+ });
2289
2519
  }
2290
2520
 
2291
2521
  pub fn thrashing(battle: anytype, player: Player, _: *State, options: anytype) !void {
@@ -2307,11 +2537,11 @@ pub const Effects = struct {
2307
2537
  side.active.volatiles.dirty = false;
2308
2538
 
2309
2539
  if (foe.active.volatiles.Transform) {
2310
- return options.log.fail(battle.active(player), .None);
2540
+ return options.log.fail(.{ battle.active(player), .None });
2311
2541
  } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2312
- if (!showdown) return options.log.fail(battle.active(player), .None);
2313
- try options.log.lastmiss();
2314
- 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)});
2315
2545
  }
2316
2546
 
2317
2547
  side.active.volatiles.Transform = true;
@@ -2333,7 +2563,7 @@ pub const Effects = struct {
2333
2563
  side.active.moves[i].pp = if (m.id != .None) if (m.id == .Sketch) 1 else 5 else 0;
2334
2564
  }
2335
2565
 
2336
- try options.log.transform(battle.active(player), foe_ident);
2566
+ try options.log.transform(.{ battle.active(player), foe_ident });
2337
2567
  }
2338
2568
 
2339
2569
  pub fn triAttack(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -2352,31 +2582,25 @@ pub const Effects = struct {
2352
2582
  _ = .{ battle, player, state, options }; // TODO
2353
2583
  }
2354
2584
 
2355
- const ALL_STAT_UP_STATS = [_][]const u8{ "atk", "def", "spe", "spa", "spd" };
2356
- const ALL_STAT_UP_REASONS =
2585
+ const STAT_UP_REASONS =
2357
2586
  [_]Boost{ .Attack, .Defense, .Speed, .SpecialAttack, .SpecialDefense };
2358
2587
 
2359
2588
  pub fn allStatUpChance(battle: anytype, player: Player, state: *State, options: anytype) !void {
2360
2589
  var side = battle.side(player);
2361
2590
 
2362
- var stats = &side.active.stats;
2363
- var boosts = &side.active.boosts;
2591
+ const stats = &side.active.stats;
2592
+ const boosts = &side.active.boosts;
2364
2593
 
2365
2594
  if (!state.proc) return;
2366
- inline for (ALL_STAT_UP_STATS, ALL_STAT_UP_REASONS) |s, r| {
2595
+ inline for (STATS, STAT_UP_REASONS) |s, r| {
2367
2596
  assert(@field(boosts, s) >= -6 and @field(boosts, s) <= 6);
2368
2597
  if (@field(boosts, s) == 6 or @field(stats, s) == MAX_STAT_VALUE) {
2369
- try options.log.boost(battle.active(player), r, 0);
2598
+ try options.log.boost(.{ battle.active(player), r, 0 });
2370
2599
  } else {
2371
- @field(boosts, s) = @intCast(@min(6, @as(i8, @field(boosts, s)) + 1));
2372
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, @field(boosts, s)) + 6))];
2373
- const stat = @field(unmodifiedStats(battle, side), s);
2374
- @field(stats, s) = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2375
- try options.log.boost(battle.active(player), r, 1);
2600
+ try boostStat(battle, side, stats, boosts, s, 1);
2601
+ try options.log.boost(.{ battle.active(player), r, 1 });
2376
2602
  }
2377
2603
  }
2378
-
2379
- statusModify(side.stored().status, stats);
2380
2604
  }
2381
2605
 
2382
2606
  pub fn boost(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -2384,72 +2608,65 @@ pub const Effects = struct {
2384
2608
  var side = battle.side(player);
2385
2609
  const ident = battle.active(player);
2386
2610
 
2387
- if (state.miss) return log.fail(ident, .None);
2611
+ if (state.miss) return log.fail(.{ ident, .None });
2388
2612
 
2389
- var stats = &side.active.stats;
2613
+ const stats = &side.active.stats;
2390
2614
  var boosts = &side.active.boosts;
2391
2615
 
2392
2616
  const move = Move.get(state.move);
2393
2617
  switch (move.effect) {
2394
2618
  .AttackUp1, .AttackUp2, .Rage, .Swagger => {
2395
2619
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2396
- 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
+ }
2397
2623
  const n: u2 = if (move.effect == .AttackUp2 or move.effect == .Swagger) 2 else 1;
2398
- boosts.atk = @as(i4, @intCast(@min(6, @as(i8, boosts.atk) + n)));
2624
+ try boostStat(battle, side, stats, boosts, "atk", n);
2399
2625
  const reason = if (showdown and move.effect == .Rage) Boost.Rage else Boost.Attack;
2400
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2401
- const stat = unmodifiedStats(battle, side).atk;
2402
- stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2403
- try log.boost(ident, reason, n);
2626
+ try log.boost(.{ ident, reason, n });
2404
2627
  },
2405
2628
  .DefenseUp1, .DefenseUp2, .SkullBash => {
2406
2629
  assert(boosts.def >= -6 and boosts.def <= 6);
2407
- 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
+ }
2408
2633
  const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
2409
- boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
2410
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2411
- const stat = unmodifiedStats(battle, side).def;
2412
- stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2413
- try log.boost(ident, .Defense, n);
2634
+ try boostStat(battle, side, stats, boosts, "def", n);
2635
+ try log.boost(.{ ident, .Defense, n });
2414
2636
  },
2415
2637
  .SpeedUp2 => {
2416
2638
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2417
- if (boosts.spe == 6 or stats.spe == MAX_STAT_VALUE) return log.fail(ident, .None);
2418
- boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
2419
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2420
- const stat = unmodifiedStats(battle, side).spe;
2421
- stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2422
- try log.boost(ident, .Speed, 2);
2639
+ if (boosts.spe == 6 or stats.spe == MAX_STAT_VALUE) {
2640
+ return log.fail(.{ ident, .None });
2641
+ }
2642
+ try boostStat(battle, side, stats, boosts, "spe", 2);
2643
+ try log.boost(.{ ident, .Speed, 2 });
2423
2644
  },
2424
2645
  .SpAtkUp1 => {
2425
2646
  assert(boosts.spa >= -6 and boosts.spa <= 6);
2426
- if (boosts.spa == 6 or stats.spa == MAX_STAT_VALUE) return log.fail(ident, .None);
2427
- boosts.spa = @intCast(@min(6, @as(i8, boosts.spa) + 1));
2428
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.spa) + 6))];
2429
- const stat = unmodifiedStats(battle, side).spa;
2430
- stats.spa = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2431
- try log.boost(ident, .SpecialAttack, 1);
2647
+ if (boosts.spa == 6 or stats.spa == MAX_STAT_VALUE) {
2648
+ return log.fail(.{ ident, .None });
2649
+ }
2650
+ try boostStat(battle, side, stats, boosts, "spa", 1);
2651
+ try log.boost(.{ ident, .SpecialAttack, 1 });
2432
2652
  },
2433
2653
  .SpDefUp2 => {
2434
2654
  assert(boosts.spd >= -6 and boosts.spd <= 6);
2435
- if (boosts.spd == 6 or stats.spd == MAX_STAT_VALUE) return log.fail(ident, .None);
2436
- boosts.spd = @intCast(@min(6, @as(i8, boosts.spd) + 2));
2437
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.spd) + 6))];
2438
- const stat = unmodifiedStats(battle, side).spd;
2439
- stats.spd = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2440
- try log.boost(ident, .SpecialDefense, 2);
2655
+ if (boosts.spd == 6 or stats.spd == MAX_STAT_VALUE) {
2656
+ return log.fail(.{ ident, .None });
2657
+ }
2658
+ try boostStat(battle, side, stats, boosts, "spd", 2);
2659
+ try log.boost(.{ ident, .SpecialDefense, 2 });
2441
2660
  },
2442
2661
  .EvasionUp1 => {
2443
2662
  assert(boosts.evasion >= -6 and boosts.evasion <= 6);
2444
- if (boosts.evasion == 6) return log.fail(ident, .None);
2663
+ if (boosts.evasion == 6) return log.fail(.{ ident, .None });
2445
2664
  boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
2446
- try log.boost(ident, .Evasion, 1);
2665
+ try log.boost(.{ ident, .Evasion, 1 });
2447
2666
  if (state.move == .Minimize) side.active.volatiles.minimized = true;
2448
2667
  },
2449
2668
  else => unreachable,
2450
2669
  }
2451
-
2452
- statusModify(side.stored().status, stats);
2453
2670
  }
2454
2671
 
2455
2672
  pub fn unboost(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -2458,97 +2675,86 @@ pub const Effects = struct {
2458
2675
  const foe_ident = battle.active(player.foe());
2459
2676
 
2460
2677
  if (foe.active.volatiles.Mist) {
2461
- try log.activate(foe_ident, .Mist);
2462
- return log.fail(foe_ident, .None);
2678
+ try log.activate(.{ foe_ident, .Mist });
2679
+ return log.fail(.{ foe_ident, .None });
2463
2680
  }
2464
2681
 
2465
2682
  const fail = foe.active.volatiles.Substitute or state.miss or !state.proc;
2466
2683
 
2467
- var stats = &foe.active.stats;
2684
+ const stats = &foe.active.stats;
2468
2685
  var boosts = &foe.active.boosts;
2469
2686
 
2470
2687
  const move = Move.get(state.move);
2688
+ const none: Fail = .None;
2471
2689
  switch (move.effect) {
2472
2690
  .AttackDown1, .AttackDown2, .AttackDownChance => {
2473
2691
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2474
2692
  if (fail or boosts.atk == -6) {
2475
- return if (move.effect != .AttackDownChance) try log.fail(foe_ident, .None);
2693
+ return if (move.effect != .AttackDownChance) try log.fail(.{ foe_ident, none });
2476
2694
  } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2477
- try options.log.lastmiss();
2478
- return options.log.miss(battle.active(player));
2695
+ try options.log.lastmiss(.{});
2696
+ return options.log.miss(.{battle.active(player)});
2479
2697
  } else if (stats.atk == 1) {
2480
- return if (move.effect != .AttackDownChance) try log.fail(foe_ident, .None);
2698
+ return if (move.effect != .AttackDownChance) try log.fail(.{ foe_ident, none });
2481
2699
  }
2482
2700
  const n: u2 = if (move.effect == .AttackDown2) 2 else 1;
2483
- boosts.atk = @intCast(@max(-6, @as(i8, boosts.atk) - n));
2484
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2485
- const stat = unmodifiedStats(battle, foe).atk;
2486
- stats.atk = @max(1, stat * mod[0] / mod[1]);
2487
- try log.boost(foe_ident, .Attack, -@as(i8, n));
2701
+ try unboostStat(battle, foe, stats, boosts, "atk", n);
2702
+ try log.boost(.{ foe_ident, .Attack, -@as(i8, n) });
2488
2703
  },
2489
2704
  .DefenseDown1, .DefenseDown2, .DefenseDownChance => {
2490
2705
  assert(boosts.def >= -6 and boosts.def <= 6);
2491
2706
  if (fail or boosts.def == -6) {
2492
- return if (move.effect != .DefenseDownChance) try log.fail(foe_ident, .None);
2707
+ if (move.effect != .DefenseDownChance) try log.fail(.{ foe_ident, none });
2708
+ return;
2493
2709
  } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2494
- try options.log.lastmiss();
2495
- return options.log.miss(battle.active(player));
2710
+ try options.log.lastmiss(.{});
2711
+ return options.log.miss(.{battle.active(player)});
2496
2712
  } else if (stats.def == 1) {
2497
- return if (move.effect != .DefenseDownChance) try log.fail(foe_ident, .None);
2713
+ if (move.effect != .DefenseDownChance) try log.fail(.{ foe_ident, none });
2714
+ return;
2498
2715
  }
2499
2716
  const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
2500
- boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
2501
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2502
- const stat = unmodifiedStats(battle, foe).def;
2503
- stats.def = @max(1, stat * mod[0] / mod[1]);
2504
- try log.boost(foe_ident, .Defense, -@as(i8, n));
2717
+ try unboostStat(battle, foe, stats, boosts, "def", n);
2718
+ try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
2505
2719
  },
2506
2720
  .SpeedDown1, .SpeedDown2, .SpeedDownChance => {
2507
2721
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2508
2722
  if (fail or boosts.spe == -6) {
2509
- return if (move.effect != .SpeedDownChance) try log.fail(foe_ident, .None);
2723
+ return if (move.effect != .SpeedDownChance) try log.fail(.{ foe_ident, none });
2510
2724
  } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2511
- try options.log.lastmiss();
2512
- return options.log.miss(battle.active(player));
2725
+ try options.log.lastmiss(.{});
2726
+ return options.log.miss(.{battle.active(player)});
2513
2727
  } else if (stats.spe == 1) {
2514
- return if (move.effect != .SpeedDownChance) try log.fail(foe_ident, .None);
2728
+ return if (move.effect != .SpeedDownChance) try log.fail(.{ foe_ident, none });
2515
2729
  }
2516
2730
  const n: u2 = if (move.effect == .SpeedDown2) 2 else 1;
2517
- boosts.spe = @intCast(@max(-6, @as(i8, boosts.spe) - 1));
2518
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2519
- const stat = unmodifiedStats(battle, foe).spe;
2520
- stats.spe = @max(1, stat * mod[0] / mod[1]);
2521
- try log.boost(foe_ident, .Speed, -@as(i8, n));
2522
- assert(boosts.spe >= -6);
2731
+ try unboostStat(battle, foe, stats, boosts, "spe", n);
2732
+ try log.boost(.{ foe_ident, .Speed, -@as(i8, n) });
2523
2733
  },
2524
2734
  .SpDefDownChance => {
2525
2735
  assert(boosts.spd >= -6 and boosts.spd <= 6);
2526
2736
  if (fail or boosts.spd == -6) return;
2527
2737
  if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2528
- try options.log.lastmiss();
2529
- return options.log.miss(battle.active(player));
2738
+ try options.log.lastmiss(.{});
2739
+ return options.log.miss(.{battle.active(player)});
2530
2740
  } else if (stats.spd == 1) return;
2531
- boosts.spd = @intCast(@max(-6, @as(i8, boosts.spd) - 1));
2532
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.spd) + 6))];
2533
- const stat = unmodifiedStats(battle, foe).spd;
2534
- stats.spd = @max(1, stat * mod[0] / mod[1]);
2535
- try log.boost(foe_ident, .SpecialDefense, -1);
2741
+ try unboostStat(battle, foe, stats, boosts, "spd", 1);
2742
+ try log.boost(.{ foe_ident, .SpecialDefense, -1 });
2536
2743
  },
2537
2744
  .AccuracyDown1, .AccuracyDownChance => {
2538
2745
  assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
2539
2746
  if (fail or boosts.accuracy == -6) {
2540
- return if (move.effect != .AccuracyDownChance) try log.fail(foe_ident, .None);
2747
+ if (move.effect != .AccuracyDownChance) try log.fail(.{ foe_ident, none });
2748
+ return;
2541
2749
  } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2542
- try options.log.lastmiss();
2543
- return options.log.miss(battle.active(player));
2750
+ try options.log.lastmiss(.{});
2751
+ return options.log.miss(.{battle.active(player)});
2544
2752
  }
2545
2753
  boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
2546
- try log.boost(foe_ident, .Accuracy, -1);
2754
+ try log.boost(.{ foe_ident, .Accuracy, -1 });
2547
2755
  },
2548
2756
  else => unreachable,
2549
2757
  }
2550
-
2551
- statusModify(foe.stored().status, stats);
2552
2758
  }
2553
2759
  };
2554
2760
 
@@ -2558,6 +2764,15 @@ fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
2558
2764
  return &battle.side(id.player).pokemon[id.id - 1].stats;
2559
2765
  }
2560
2766
 
2767
+ fn recomputeStats(battle: anytype, side: *Side) void {
2768
+ const stats = unmodifiedStats(battle, side);
2769
+ inline for (STATS) |s| {
2770
+ const mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, @field(side.active.boosts, s)) + 6))];
2771
+ const val = @field(stats, s) * mod[0] / mod[1];
2772
+ @field(side.active.stats, s) = @max(1, @min(MAX_STAT_VALUE, val));
2773
+ }
2774
+ }
2775
+
2561
2776
  fn statusModify(status: u8, stats: *Stats(u16)) void {
2562
2777
  if (Status.is(status, .PAR)) {
2563
2778
  stats.spe = @max(stats.spe / 4, 1);
@@ -2566,6 +2781,52 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
2566
2781
  }
2567
2782
  }
2568
2783
 
2784
+ fn boostStat(
2785
+ battle: anytype,
2786
+ side: *Side,
2787
+ stats: *Stats(u16),
2788
+ boosts: *Boosts,
2789
+ comptime s: []const u8,
2790
+ n: u2,
2791
+ ) !void {
2792
+ @field(boosts, s) = @intCast(@min(6, @as(i8, @field(boosts, s)) + n));
2793
+ const mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, @field(boosts, s)) + 6))];
2794
+ const stat = @field(unmodifiedStats(battle, side), s);
2795
+ @field(stats, s) = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2796
+ if (comptime std.mem.eql(u8, s, "atk")) {
2797
+ if (Status.is(side.stored().status, .BRN)) {
2798
+ @field(stats, s) = @max(@field(stats, s) / 2, 1);
2799
+ }
2800
+ } else if (comptime std.mem.eql(u8, s, "spe")) {
2801
+ if (Status.is(side.stored().status, .PAR)) {
2802
+ @field(stats, s) = @max(@field(stats, s) / 4, 1);
2803
+ }
2804
+ }
2805
+ }
2806
+
2807
+ fn unboostStat(
2808
+ battle: anytype,
2809
+ side: *Side,
2810
+ stats: *Stats(u16),
2811
+ boosts: *Boosts,
2812
+ comptime s: []const u8,
2813
+ n: u2,
2814
+ ) !void {
2815
+ @field(boosts, s) = @intCast(@max(-6, @as(i8, @field(boosts, s)) - n));
2816
+ const mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, @field(boosts, s)) + 6))];
2817
+ const stat = @field(unmodifiedStats(battle, side), s);
2818
+ @field(stats, s) = @max(1, stat * mod[0] / mod[1]);
2819
+ if (comptime std.mem.eql(u8, s, "atk")) {
2820
+ if (Status.is(side.stored().status, .BRN)) {
2821
+ @field(stats, s) = @max(@field(stats, s) / 2, 1);
2822
+ }
2823
+ } else if (comptime std.mem.eql(u8, s, "spe")) {
2824
+ if (Status.is(side.stored().status, .PAR)) {
2825
+ @field(stats, s) = @max(@field(stats, s) / 4, 1);
2826
+ }
2827
+ }
2828
+ }
2829
+
2569
2830
  fn isForced(active: anytype) bool {
2570
2831
  return active.volatiles.Recharging or active.volatiles.Rage or
2571
2832
  active.volatiles.Thrashing or active.volatiles.Charging or
@@ -2578,9 +2839,15 @@ fn convertible(active: ActivePokemon, m: Move) bool {
2578
2839
  return t != .@"???" and !active.types.includes(t);
2579
2840
  }
2580
2841
 
2842
+ // NOTE: llvm/llvm-project#58557
2843
+ fn smul(a: anytype, b: @TypeOf(a)) @TypeOf(a) {
2844
+ if (!comptime builtin.target.isWasm()) return a *| b;
2845
+ return @max(a *% b, std.math.maxInt(@TypeOf(a)));
2846
+ }
2847
+
2581
2848
  pub const Rolls = struct {
2582
2849
  fn speedTie(battle: anytype, options: anytype) !bool {
2583
- const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
2850
+ const p1 = if (options.calc.overridden(.P1, 0, .speed_tie)) |player|
2584
2851
  player == .P1
2585
2852
  else if (showdown)
2586
2853
  battle.rng.range(u8, 0, 2) == 0
@@ -2592,7 +2859,7 @@ pub const Rolls = struct {
2592
2859
  }
2593
2860
 
2594
2861
  fn quickClaw(battle: anytype, player: Player, options: anytype) !bool {
2595
- const qkc = if (options.calc.overridden(player, .quick_claw)) |val|
2862
+ const qkc = if (options.calc.overridden(player, 0, .quick_claw)) |val|
2596
2863
  val == .true
2597
2864
  else if (showdown)
2598
2865
  battle.rng.chance(u8, 60, 256)
@@ -2604,7 +2871,7 @@ pub const Rolls = struct {
2604
2871
  }
2605
2872
 
2606
2873
  fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) !bool {
2607
- const ok = if (options.calc.overridden(player, .hit)) |val|
2874
+ const ok = if (options.calc.overridden(player, 0, .hit)) |val|
2608
2875
  val == .true
2609
2876
  else if (showdown)
2610
2877
  battle.rng.chance(u8, accuracy, 256)
@@ -2615,20 +2882,22 @@ pub const Rolls = struct {
2615
2882
  return ok;
2616
2883
  }
2617
2884
 
2618
- fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2619
- 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|
2620
2888
  val == .true
2621
2889
  else if (showdown)
2622
2890
  battle.rng.chance(u8, rate, 256)
2623
2891
  else
2624
2892
  battle.rng.next() < rate;
2625
2893
 
2626
- try options.chance.criticalHit(player, crit, rate);
2894
+ try options.chance.criticalHit(player, i, crit, rate);
2627
2895
  return crit;
2628
2896
  }
2629
2897
 
2630
- fn damage(battle: anytype, player: Player, options: anytype) !u8 {
2631
- 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|
2632
2901
  val
2633
2902
  else roll: {
2634
2903
  if (showdown) break :roll battle.rng.range(u8, 217, 256);
@@ -2639,12 +2908,12 @@ pub const Rolls = struct {
2639
2908
  };
2640
2909
 
2641
2910
  assert(roll >= 217 and roll <= 255);
2642
- try options.chance.damage(player, roll);
2911
+ try options.chance.damage(player, i, roll);
2643
2912
  return roll;
2644
2913
  }
2645
2914
 
2646
2915
  fn confused(battle: anytype, player: Player, options: anytype) !bool {
2647
- const cfz = if (options.calc.overridden(player, .confused)) |val|
2916
+ const cfz = if (options.calc.overridden(player, 0, .confused)) |val|
2648
2917
  val == .true
2649
2918
  else if (showdown)
2650
2919
  !battle.rng.chance(u8, 128, 256)
@@ -2656,7 +2925,7 @@ pub const Rolls = struct {
2656
2925
  }
2657
2926
 
2658
2927
  fn attract(battle: anytype, player: Player, options: anytype) !bool {
2659
- const cant = if (options.calc.overridden(player, .attract)) |val|
2928
+ const cant = if (options.calc.overridden(player, 0, .attract)) |val|
2660
2929
  val == .true
2661
2930
  else if (showdown)
2662
2931
  battle.rng.chance(u8, 1, 2)
@@ -2668,7 +2937,7 @@ pub const Rolls = struct {
2668
2937
  }
2669
2938
 
2670
2939
  fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2671
- const par = if (options.calc.overridden(player, .paralyzed)) |val|
2940
+ const par = if (options.calc.overridden(player, 0, .paralyzed)) |val|
2672
2941
  val == .true
2673
2942
  else if (showdown)
2674
2943
  battle.rng.chance(u8, 63, 256)
@@ -2680,7 +2949,7 @@ pub const Rolls = struct {
2680
2949
  }
2681
2950
 
2682
2951
  fn defrost(battle: anytype, player: Player, options: anytype) !bool {
2683
- const thaw = if (options.calc.overridden(player, .defrost)) |val|
2952
+ const thaw = if (options.calc.overridden(player, 0, .defrost)) |val|
2684
2953
  val == .true
2685
2954
  else if (showdown)
2686
2955
  battle.rng.chance(u8, 25, 256)
@@ -2693,7 +2962,7 @@ pub const Rolls = struct {
2693
2962
 
2694
2963
  fn secondaryChance(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2695
2964
  // GLITCH: 100% secondary chance will still fail 1/256 of the time
2696
- const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2965
+ const proc = if (options.calc.overridden(player, 0, .secondary_chance)) |val|
2697
2966
  val == .true
2698
2967
  else if (showdown)
2699
2968
  battle.rng.chance(u8, rate, 256)
@@ -2705,7 +2974,7 @@ pub const Rolls = struct {
2705
2974
  }
2706
2975
 
2707
2976
  fn item(battle: anytype, player: Player, options: anytype) !bool {
2708
- const proc = if (options.calc.overridden(player, .item)) |val|
2977
+ const proc = if (options.calc.overridden(player, 0, .item)) |val|
2709
2978
  val == .true
2710
2979
  else if (showdown)
2711
2980
  battle.rng.chance(u8, 30, 256)
@@ -2717,7 +2986,7 @@ pub const Rolls = struct {
2717
2986
  }
2718
2987
 
2719
2988
  fn triAttack(battle: anytype, player: Player, options: anytype) !TriAttack {
2720
- const status: TriAttack = if (options.calc.overridden(player, .tri_attack)) |val|
2989
+ const status: TriAttack = if (options.calc.overridden(player, 0, .tri_attack)) |val|
2721
2990
  @enumFromInt(@intFromEnum(val) - 1)
2722
2991
  else if (showdown)
2723
2992
  @enumFromInt(battle.rng.range(u2, 0, 2))
@@ -2733,7 +3002,7 @@ pub const Rolls = struct {
2733
3002
  }
2734
3003
 
2735
3004
  fn present(battle: anytype, player: Player, options: anytype) !u8 {
2736
- const power = if (options.calc.overridden(player, .present)) |val|
3005
+ const power = if (options.calc.overridden(player, 0, .present)) |val|
2737
3006
  (val - 1) * 40
2738
3007
  else if (showdown) power: {
2739
3008
  const r = battle.rng.range(u8, 0, 10);
@@ -2755,7 +3024,7 @@ pub const Rolls = struct {
2755
3024
  }
2756
3025
 
2757
3026
  fn magnitude(battle: anytype, player: Player, options: anytype) !u8 {
2758
- const num = if (options.calc.overridden(player, .magnitude)) |val|
3027
+ const num: u8 = if (options.calc.overridden(player, 0, .magnitude)) |val|
2759
3028
  val + 3
2760
3029
  else if (showdown) num: {
2761
3030
  const r = battle.rng.range(u8, 0, 100);
@@ -2783,7 +3052,7 @@ pub const Rolls = struct {
2783
3052
  }
2784
3053
 
2785
3054
  fn tripleKick(battle: anytype, player: Player, options: anytype) !u2 {
2786
- const hits = if (options.calc.overridden(player, .triple_kick)) |val|
3055
+ const hits = if (options.calc.overridden(player, 0, .triple_kick)) |val|
2787
3056
  val
2788
3057
  else if (showdown)
2789
3058
  battle.rng.range(u2, 1, 4)
@@ -2799,13 +3068,12 @@ pub const Rolls = struct {
2799
3068
  return hits;
2800
3069
  }
2801
3070
 
2802
- fn spite(battle: anytype, player: Player, options: anytype) !u8 {
2803
- 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|
2804
3074
  val + 1
2805
- else if (showdown)
2806
- battle.rng.range(u8, 2, 6)
2807
3075
  else
2808
- (battle.rng.next() & 3) + 2;
3076
+ @intCast((battle.rng.next() & 3) + 2);
2809
3077
 
2810
3078
  assert(pp >= 2 and pp <= 5);
2811
3079
  try options.chance.spite(player, pp);
@@ -2819,7 +3087,7 @@ pub const Rolls = struct {
2819
3087
  var i: u8 = 0;
2820
3088
  const neutral = @intFromEnum(Effectiveness.Neutral);
2821
3089
  assert(mtype != .@"???");
2822
- const ty: Type = if (options.calc.overridden(player, .conversion_2)) |val|
3090
+ const ty: Type = if (options.calc.overridden(player, 0, .conversion_2)) |val|
2823
3091
  @enumFromInt(@intFromEnum(val) - 1)
2824
3092
  else if (showdown) ty: {
2825
3093
  for (Type.SHOWDOWN) |t| {
@@ -2853,7 +3121,7 @@ pub const Rolls = struct {
2853
3121
  assert(count <= 8);
2854
3122
  const num = if (count == 8) 0 else @as(u8, 255) >> @intCast(count);
2855
3123
 
2856
- const ok = if (options.calc.overridden(player, .protect)) |val|
3124
+ const ok = if (options.calc.overridden(player, 0, .protect)) |val|
2857
3125
  val == .true
2858
3126
  else if (showdown)
2859
3127
  battle.rng.chance(u8, num, 255)
@@ -2875,7 +3143,7 @@ pub const Rolls = struct {
2875
3143
  const moves = active.moves;
2876
3144
 
2877
3145
  // TODO: consider throwing error instead of rerolling?
2878
- const overridden = if (options.calc.overridden(player, .move_slot)) |val|
3146
+ const overridden = if (options.calc.overridden(player, 0, .move_slot)) |val|
2879
3147
  if (convertible(active, moves[val - 1].id)) val else null
2880
3148
  else
2881
3149
  null;
@@ -2912,7 +3180,7 @@ pub const Rolls = struct {
2912
3180
  const moves = active.moves;
2913
3181
 
2914
3182
  // TODO: consider throwing error instead of rerolling?
2915
- const overridden = if (options.calc.overridden(player, .move_slot)) |val|
3183
+ const overridden = if (options.calc.overridden(player, 0, .move_slot)) |val|
2916
3184
  if (val != mslot and Move.get(moves[val - 1].id).extra.sleep_talk) val else null
2917
3185
  else
2918
3186
  null;
@@ -2956,7 +3224,7 @@ pub const Rolls = struct {
2956
3224
  assert(n > 0 and n <= 6);
2957
3225
 
2958
3226
  // TODO: consider throwing error instead of rerolling?
2959
- const overridden = if (options.calc.overridden(player, .force_switch)) |val| val: {
3227
+ const overridden = if (options.calc.overridden(player, 0, .force_switch)) |val| val: {
2960
3228
  for (slots) |slot| if (slot == val) break :val if (foe.get(slot).hp > 0) val else null;
2961
3229
  break :val null;
2962
3230
  } else null;
@@ -2982,7 +3250,7 @@ pub const Rolls = struct {
2982
3250
  const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
2983
3251
 
2984
3252
  fn multiHit(battle: anytype, player: Player, options: anytype) !u3 {
2985
- const n: u3 = if (options.calc.overridden(player, .multi_hit)) |val|
3253
+ const n: u3 = if (options.calc.overridden(player, 0, .multi_hit)) |val|
2986
3254
  @intCast(val)
2987
3255
  else if (showdown)
2988
3256
  DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
@@ -2997,7 +3265,7 @@ pub const Rolls = struct {
2997
3265
  }
2998
3266
 
2999
3267
  fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
3000
- const power = if (options.calc.overridden(player, .psywave)) |val|
3268
+ const power = if (options.calc.overridden(player, 0, .psywave)) |val|
3001
3269
  val
3002
3270
  else if (showdown)
3003
3271
  battle.rng.range(u8, 1, max)
@@ -3023,14 +3291,14 @@ pub const Rolls = struct {
3023
3291
  }
3024
3292
 
3025
3293
  // TODO: consider throwing error instead of rerolling?
3026
- const overridden = if (options.calc.overridden(player, .metronome)) |val| val: {
3294
+ const overridden = if (options.calc.overridden(player, 0, .metronome)) |val| val: {
3027
3295
  for (moves) |m| if (m.id == val) break :val null;
3028
3296
  break :val val;
3029
3297
  } else null;
3030
3298
 
3031
3299
  const move: Move = overridden orelse move: {
3032
3300
  if (showdown) {
3033
- 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)));
3034
3302
  for (moves) |m| {
3035
3303
  if (m.id == .None) break;
3036
3304
  if (@intFromEnum(m.id) > @intFromEnum(Move.METRONOME[r])) break;
@@ -3055,7 +3323,7 @@ pub const Rolls = struct {
3055
3323
  }
3056
3324
 
3057
3325
  fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
3058
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
3326
+ const duration: u3 = if (options.calc.overridden(player, 0, .duration)) |val|
3059
3327
  @intCast(val)
3060
3328
  else if (showdown)
3061
3329
  battle.rng.range(u3, 1, 7)
@@ -3067,12 +3335,12 @@ pub const Rolls = struct {
3067
3335
  };
3068
3336
 
3069
3337
  assert(duration >= 1 and duration <= 6);
3070
- options.chance.duration(.sleep, player, player.foe(), duration);
3338
+ options.chance.duration(player, duration);
3071
3339
  return duration + 1;
3072
3340
  }
3073
3341
 
3074
3342
  fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
3075
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
3343
+ const duration: u3 = if (options.calc.overridden(player, 0, .duration)) |val|
3076
3344
  @intCast(val)
3077
3345
  else if (showdown)
3078
3346
  // Pokémon Showdown incorrectly uses the same duration for self-confusion
@@ -3081,16 +3349,16 @@ pub const Rolls = struct {
3081
3349
  @intCast((battle.rng.next() & if (self) 1 else 3) + 2);
3082
3350
 
3083
3351
  assert(duration >= 2 and duration <= if (!showdown and self) 3 else 5);
3084
- options.chance.duration(.confusion, player, if (self) player else player.foe(), duration);
3352
+ options.chance.duration(player, duration);
3085
3353
  return duration;
3086
3354
  }
3087
3355
 
3088
3356
  fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
3089
- const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
3357
+ const duration: u3 = if (options.calc.overridden(player, 0, .duration)) |val|
3090
3358
  @intCast(val)
3091
3359
  else if (showdown)
3092
3360
  // Pokémon Showdown incorrectly inherits Generation III's Disable duration
3093
- battle.rng.range(u4, 2, 6)
3361
+ battle.rng.range(u3, 2, 6)
3094
3362
  else duration: {
3095
3363
  while (true) {
3096
3364
  const r = battle.rng.next() & 7;
@@ -3099,12 +3367,12 @@ pub const Rolls = struct {
3099
3367
  };
3100
3368
 
3101
3369
  assert(duration >= 2 and duration <= (if (showdown) 5 else 7));
3102
- options.chance.duration(.disable, player, player.foe(), duration);
3370
+ options.chance.duration(player, duration);
3103
3371
  return duration;
3104
3372
  }
3105
3373
 
3106
3374
  fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
3107
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
3375
+ const duration: u3 = if (options.calc.overridden(player, 0, .duration)) |val|
3108
3376
  @intCast(val)
3109
3377
  else if (showdown)
3110
3378
  battle.rng.range(u3, 2, 4)
@@ -3112,12 +3380,12 @@ pub const Rolls = struct {
3112
3380
  @intCast((battle.rng.next() & 1) + 2); // BUG: thrash is + 1 not +2
3113
3381
 
3114
3382
  assert(duration >= 2 and duration <= 3);
3115
- options.chance.duration(.attacking, player, player, duration);
3383
+ options.chance.duration(player, duration);
3116
3384
  return duration;
3117
3385
  }
3118
3386
 
3119
3387
  fn bindingDuration(battle: anytype, player: Player, options: anytype) u3 {
3120
- const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
3388
+ const duration: u3 = (if (options.calc.overridden(player, 0, .duration)) |val|
3121
3389
  @intCast(val)
3122
3390
  else if (showdown)
3123
3391
  // TODO: PS does range(3, 6) here which after subtracting 1 is actually 2-4 not 2-5!
@@ -3126,13 +3394,13 @@ pub const Rolls = struct {
3126
3394
  @intCast(battle.rng.next() & 3 + 2));
3127
3395
 
3128
3396
  assert(duration >= 2 and duration <= 5);
3129
- options.chance.duration(.binding, player, player.foe(), duration);
3397
+ options.chance.duration(player, duration);
3130
3398
  return duration + 1;
3131
3399
  }
3132
3400
 
3133
3401
  // TODO: consider sharing implementation with bindingDuration
3134
3402
  fn encoreDuration(battle: anytype, player: Player, options: anytype) u3 {
3135
- const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
3403
+ const duration: u3 = (if (options.calc.overridden(player, 0, .duration)) |val|
3136
3404
  @intCast(val)
3137
3405
  else if (showdown)
3138
3406
  battle.rng.range(u3, 2, 6)
@@ -3140,7 +3408,7 @@ pub const Rolls = struct {
3140
3408
  @intCast(battle.rng.next() & 3 + 2));
3141
3409
 
3142
3410
  assert(duration >= 2 and duration <= 5);
3143
- options.chance.duration(.encore, player, player.foe(), duration);
3411
+ options.chance.duration(player, duration);
3144
3412
  return duration + 1;
3145
3413
  }
3146
3414
  };
@@ -3253,7 +3521,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3253
3521
  .Move => {
3254
3522
  const side = battle.side(player);
3255
3523
 
3256
- var active = &side.active;
3524
+ const active = &side.active;
3257
3525
  const stored = side.stored();
3258
3526
 
3259
3527
  // While players are not given any input options on the cartridge in these cases,
@@ -3273,7 +3541,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3273
3541
  }
3274
3542
 
3275
3543
  var slot: u4 = 2;
3276
- while (!active.volatiles.trapped and slot <= 6) : (slot += 1) {
3544
+ while (!active.volatiles.Trapped and slot <= 6) : (slot += 1) {
3277
3545
  const id = side.order[slot - 1];
3278
3546
  if (id == 0 or side.pokemon[id - 1].hp == 0) continue;
3279
3547
  out[n] = .{ .type = .Switch, .data = slot };