@pkmn/engine 0.1.0-dev.1220fbc5 → 0.1.0-dev.130de754

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/LICENSE +1 -3
  2. package/README.md +46 -30
  3. package/build/pkg/addon/browser.d.ts +3 -0
  4. package/build/pkg/addon/browser.js +47 -0
  5. package/build/pkg/addon/browser.js.map +1 -0
  6. package/build/pkg/addon/node.d.ts +3 -0
  7. package/build/pkg/addon/node.js +110 -0
  8. package/build/pkg/addon/node.js.map +1 -0
  9. package/build/pkg/addon/wasm.d.ts +2 -0
  10. package/build/pkg/addon/wasm.js +53 -0
  11. package/build/pkg/addon/wasm.js.map +1 -0
  12. package/build/pkg/addon.d.ts +26 -4
  13. package/build/pkg/addon.js +39 -71
  14. package/build/pkg/addon.js.map +1 -1
  15. package/build/pkg/data/layout.json +15 -2
  16. package/build/pkg/data/protocol.json +23 -9
  17. package/build/pkg/data.d.ts +27 -5
  18. package/build/pkg/data.js.map +1 -1
  19. package/build/pkg/gen1.d.ts +15 -18
  20. package/build/pkg/gen1.js +28 -19
  21. package/build/pkg/gen1.js.map +1 -1
  22. package/build/pkg/index.d.ts +42 -91
  23. package/build/pkg/index.js +23 -70
  24. package/build/pkg/index.js.map +1 -1
  25. package/build/pkg/protocol.d.ts +29 -1
  26. package/build/pkg/protocol.js +109 -107
  27. package/build/pkg/protocol.js.map +1 -1
  28. package/build/tools/debug.js +84 -73
  29. package/build/tools/debug.js.map +1 -1
  30. package/build/tools/display/debug.jsx +66 -0
  31. package/build/tools/display/debug.jsx.map +1 -0
  32. package/build/tools/display/index.js +152 -0
  33. package/build/tools/display/index.js.map +1 -0
  34. package/build/tools/display/ui.jsx +360 -0
  35. package/build/tools/display/ui.jsx.map +1 -0
  36. package/build/tools/display/util.js +157 -0
  37. package/build/tools/display/util.js.map +1 -0
  38. package/build.zig +310 -203
  39. package/package.json +59 -132
  40. package/src/bin/install-pkmn-engine +70 -53
  41. package/src/bin/pkmn-debug +1 -1
  42. package/src/lib/bindings/c.zig +201 -0
  43. package/src/lib/{binding → bindings}/node.zig +15 -32
  44. package/src/lib/bindings/wasm.zig +51 -0
  45. package/src/lib/common/array.zig +70 -0
  46. package/src/lib/common/data.zig +4 -4
  47. package/src/lib/common/debug.zig +46 -30
  48. package/src/lib/common/js.zig +5 -9
  49. package/src/lib/common/optional.zig +21 -22
  50. package/src/lib/common/options.zig +6 -0
  51. package/src/lib/common/protocol.zig +573 -541
  52. package/src/lib/common/rational.zig +340 -254
  53. package/src/lib/common/rng.zig +11 -13
  54. package/src/lib/common/util.zig +53 -1
  55. package/src/lib/gen1/calc.zig +724 -175
  56. package/src/lib/gen1/chance.zig +494 -308
  57. package/src/lib/gen1/data/moves.zig +2 -4
  58. package/src/lib/gen1/data/species.zig +2 -4
  59. package/src/lib/gen1/data/types.zig +3 -4
  60. package/src/lib/gen1/data.zig +49 -49
  61. package/src/lib/gen1/helpers.zig +115 -562
  62. package/src/lib/gen1/mechanics.zig +603 -489
  63. package/src/lib/gen2/calc.zig +35 -28
  64. package/src/lib/gen2/chance.zig +122 -518
  65. package/src/lib/gen2/data/items.zig +1 -3
  66. package/src/lib/gen2/data/moves.zig +2 -4
  67. package/src/lib/gen2/data/species.zig +1 -3
  68. package/src/lib/gen2/data/types.zig +7 -5
  69. package/src/lib/gen2/data.zig +40 -39
  70. package/src/lib/gen2/generated.zig +30 -47
  71. package/src/lib/gen2/helpers.zig +68 -39
  72. package/src/lib/gen2/mechanics.zig +853 -533
  73. package/src/lib/napi.h +2 -0
  74. package/src/lib/node.zig +14 -0
  75. package/src/lib/pkmn.zig +123 -29
  76. package/src/lib/wasm.zig +12 -0
  77. package/src/tools/display/dom.ts +108 -0
  78. package/src/tools/display/ui.css +354 -0
  79. package/build/test/display.js +0 -400
  80. package/build/test/display.js.map +0 -1
  81. package/src/lib/binding/wasm.zig +0 -12
  82. package/src/lib/gen1/README.md +0 -894
  83. package/src/lib/gen2/README.md +0 -330
  84. package/src/test/display/pkmn.html.tmpl +0 -368
@@ -1,49 +1,36 @@
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 common = @import("../common/data.zig");
11
4
  const data = @import("data.zig");
12
5
  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;
6
+ const pkmn = @import("../pkmn.zig");
7
+ const protocol = @import("../common/protocol.zig");
8
+ const rng = @import("../common/rng.zig");
9
+ const std = @import("std");
26
10
 
27
11
  const Activate = protocol.Activate;
12
+ const ActivePokemon = data.ActivePokemon;
13
+ const assert = std.debug.assert;
28
14
  const Boost = protocol.Boost;
29
- const Cant = protocol.Cant;
15
+ const Boosts = data.Boosts;
16
+ const Choice = common.Choice;
30
17
  const Damage = protocol.Damage;
18
+ const Effectiveness = data.Effectiveness;
31
19
  const End = protocol.End;
32
- const Heal = protocol.Heal;
33
- const Start = protocol.Start;
34
-
20
+ const expectEqual = std.testing.expectEqual;
21
+ const expectEqualSlices = std.testing.expectEqualSlices;
22
+ const expectEqualStrings = std.testing.expectEqualStrings;
23
+ const Fail = protocol.Fail;
35
24
  const Gen12 = rng.Gen12;
36
-
37
- const PointerType = util.PointerType;
38
-
39
- const ActivePokemon = data.ActivePokemon;
40
- const Effectiveness = data.Effectiveness;
25
+ const Heal = protocol.Heal;
26
+ const ID = common.ID;
41
27
  const Item = data.Item;
42
28
  const Move = data.Move;
43
- const MoveSlot = data.MoveSlot;
44
- const Pokemon = data.Pokemon;
29
+ const Player = common.Player;
30
+ const Result = common.Result;
31
+ const showdown = pkmn.options.showdown;
45
32
  const Side = data.Side;
46
- const Species = data.Species;
33
+ const Start = protocol.Start;
47
34
  const Stats = data.Stats;
48
35
  const Status = data.Status;
49
36
  const TriAttack = data.TriAttack;
@@ -96,12 +83,16 @@ const CRITICAL_HITS = &[_]u8{
96
83
 
97
84
  const MAX_STAT_VALUE = 999;
98
85
 
86
+ const STATS = [_][]const u8{ "atk", "def", "spe", "spa", "spd" };
87
+
99
88
  pub const State = struct {
100
89
  damage: u16 = 0, // wCurDamage
101
90
  effectiveness: u16 = Effectiveness.neutral, // wTypeModifier
102
91
  bp: u8, // wPlayerMoveStructPower
92
+ type: Type, // BATTLE_VARS_MOVE_TYPE
103
93
  accuracy: u8, // wEnemyMoveStruct + MOVE_ACC
104
94
  move: Move, // wCurPlayerMove
95
+ hit: u3 = 0,
105
96
  from: Move = .None,
106
97
  mslot: u4, // wCurMoveNum
107
98
  first: bool, // wEnemyGoesFirst
@@ -111,7 +102,7 @@ pub const State = struct {
111
102
  proc: bool = true, // wEffectFailed
112
103
 
113
104
  comptime {
114
- assert(@sizeOf(State) == 14);
105
+ assert(@sizeOf(State) == 16);
115
106
  }
116
107
 
117
108
  pub fn immune(self: *State) bool {
@@ -136,13 +127,13 @@ fn start(battle: anytype, options: anytype) !Result {
136
127
  const p1 = battle.side(.P1);
137
128
  const p2 = battle.side(.P2);
138
129
 
139
- var p1_slot = findFirstAlive(p1);
130
+ const p1_slot = findFirstAlive(p1);
140
131
  assert(!showdown or p1_slot == 1);
141
- if (p1_slot == 0) return if (findFirstAlive(p2) == 0) Result.Tie else Result.Lose;
132
+ if (p1_slot == 0) return if (findFirstAlive(p2) == 0) .Tie else .Lose;
142
133
 
143
- var p2_slot = findFirstAlive(p2);
134
+ const p2_slot = findFirstAlive(p2);
144
135
  assert(!showdown or p2_slot == 1);
145
- if (p2_slot == 0) return Result.Win;
136
+ if (p2_slot == 0) return .Win;
146
137
 
147
138
  try switchIn(battle, .P1, p1_slot, .Initial, options);
148
139
  try switchIn(battle, .P2, p2_slot, .Initial, options);
@@ -248,15 +239,14 @@ fn switchIn(battle: anytype, player: Player, slot: u8, kind: SwitchIn, options:
248
239
  statusModify(incoming.status, &active.stats);
249
240
 
250
241
  if (kind == .Drag) {
251
- try options.log.drag(battle.active(player), incoming);
242
+ try options.log.drag(.{ battle.active(player), incoming });
252
243
  } else {
253
- try options.log.switched(battle.active(player), incoming);
244
+ try options.log.switched(.{ battle.active(player), incoming });
254
245
  }
255
- options.chance.switched(player, side.pokemon[0].position, out);
256
246
 
257
247
  if (side.conditions.Spikes and !active.types.includes(.Flying)) {
258
248
  incoming.hp -|= @max(incoming.stats.hp / 8, 1);
259
- try options.log.damage(battle.active(player), incoming, .Spikes);
249
+ try options.log.damage(.{ battle.active(player), incoming, .Spikes });
260
250
  }
261
251
  }
262
252
 
@@ -399,14 +389,22 @@ fn executeMove(
399
389
 
400
390
  assert(choice.type == .Move);
401
391
  var mslot: u4 = @intCast(choice.data);
392
+ _ = &mslot;
402
393
  var move = Move.Pound; // TODO
394
+ _ = &move;
403
395
 
404
396
  if (!try beforeMove(battle, player, move, options)) return null;
405
397
 
406
398
  // FIXME: need damage from doTurn
407
399
  const m = Move.get(move);
408
- var state: State =
409
- .{ .move = move, .mslot = mslot, .first = first, .accuracy = m.accuracy, .bp = m.bp };
400
+ var state: State = .{
401
+ .move = move,
402
+ .mslot = mslot,
403
+ .first = first,
404
+ .accuracy = m.accuracy,
405
+ .bp = m.bp,
406
+ .type = m.type,
407
+ };
410
408
  try generated.runMove(battle, player, &state, options);
411
409
  return null;
412
410
  }
@@ -423,31 +421,22 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
423
421
  if (volatiles.Recharging) {
424
422
  volatiles.Recharging = false;
425
423
  resetCant(volatiles);
426
- try log.cant(ident, .Recharge);
424
+ try log.cant(.{ ident, .Recharge });
427
425
  return false;
428
426
  }
429
427
 
430
428
  if (Status.is(stored.status, .SLP)) slp: {
431
429
  const before = stored.status;
432
430
  // 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
431
  stored.status -= 1;
437
- // }
438
-
439
- const duration = Status.duration(stored.status);
440
- if (!Status.is(stored.status, .EXT)) {
441
- try options.chance.sleep(player, duration);
442
- }
443
432
 
444
- if (duration == 0) {
445
- try log.curestatus(ident, before, .Message);
433
+ if (Status.duration(stored.status) == 0) {
434
+ try log.curestatus(.{ ident, before, .Message });
446
435
  stored.status = 0; // clears EXT if present
447
436
  resetCant(volatiles);
448
437
  volatiles.Nightmare = false;
449
438
  } else {
450
- try log.cant(ident, .Sleep);
439
+ try log.cant(.{ ident, .Sleep });
451
440
  if (move == .Snore or move == .SleepTalk) break :slp;
452
441
  resetCant(volatiles);
453
442
  return false;
@@ -456,7 +445,7 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
456
445
 
457
446
  if (Status.is(stored.status, .FRZ) and !(move == .FlameWheel or move == .SacredFire)) {
458
447
  resetCant(volatiles);
459
- try log.cant(ident, .Freeze);
448
+ try log.cant(.{ ident, .Freeze });
460
449
  return false;
461
450
  }
462
451
 
@@ -466,39 +455,28 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
466
455
  // if (!showdown) volatiles.Flinch = false;
467
456
  volatiles.Flinch = false;
468
457
  resetCant(volatiles);
469
- try log.cant(ident, .Flinch);
458
+ try log.cant(.{ ident, .Flinch });
470
459
  return false;
471
460
  }
472
461
 
473
462
  if (volatiles.disable.duration > 0) {
474
- // if (options.calc.modify(player, .disable)) |extend| {
475
- // if (!extend) volatiles.disable.duration = 0;
476
- // } else {
477
463
  volatiles.disable.duration -= 1;
478
- // }
479
- try options.chance.disable(player, volatiles.disable.duration);
480
464
 
481
465
  if (volatiles.disable.duration == 0) {
482
466
  volatiles.disable.move = 0;
483
- try log.end(ident, .Disable);
467
+ try log.end(.{ ident, .Disable });
484
468
  }
485
469
  }
486
470
 
487
471
  if (volatiles.Confusion) {
488
472
  assert(volatiles.confusion > 0);
489
-
490
- // if (options.calc.modify(player, .confusion)) |extend| {
491
- // if (!extend) volatiles.confusion = 0;
492
- // } else {
493
473
  volatiles.confusion -= 1;
494
- // }
495
- try options.chance.confusion(player, volatiles.confusion);
496
474
 
497
475
  if (volatiles.confusion == 0) {
498
476
  volatiles.Confusion = false;
499
- try log.end(ident, .Confusion);
477
+ try log.end(.{ ident, .Confusion });
500
478
  } else {
501
- try log.activate(ident, .Confusion);
479
+ try log.activate(.{ ident, .Confusion });
502
480
 
503
481
  if (try Rolls.confused(battle, player, options)) {
504
482
  volatiles.BeatUp = false;
@@ -513,11 +491,11 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
513
491
  }
514
492
 
515
493
  if (volatiles.Attract) {
516
- try log.activate(ident, .Attract);
494
+ try log.activate(.{ ident, .Attract });
517
495
 
518
496
  if (try Rolls.attract(battle, player, options)) {
519
497
  resetCant(volatiles);
520
- try log.cant(ident, .Attract);
498
+ try log.cant(.{ ident, .Attract });
521
499
  return false;
522
500
  }
523
501
  }
@@ -528,14 +506,14 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
528
506
  if (m != .None and m == move) {
529
507
  side.active.volatiles.Charging = false;
530
508
  resetCant(volatiles);
531
- try options.log.disabled(ident, move);
509
+ try options.log.cant(.{ ident, .Disable, move });
532
510
  return false;
533
511
  }
534
512
  }
535
513
 
536
514
  if (Status.is(stored.status, .PAR) and try Rolls.paralyzed(battle, player, options)) {
537
515
  resetCant(volatiles);
538
- try log.cant(ident, .Paralysis);
516
+ try log.cant(.{ ident, .Paralysis });
539
517
  return false;
540
518
  }
541
519
 
@@ -555,11 +533,11 @@ fn resetCant(volatiles: *align(8) Volatiles) void {
555
533
 
556
534
  pub fn canMove(battle: anytype, player: Player, state: *State, options: anytype) !bool {
557
535
  var side = battle.side(player);
558
- var volatiles = &side.active.volatiles;
536
+ const volatiles = &side.active.volatiles;
559
537
 
560
538
  // FIXME: pass in target to avoid lookup?
561
539
  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);
540
+ try options.log.move(.{ battle.active(player), state.move, battle.active(target), state.from });
563
541
 
564
542
  const charging = false; // TODO charging uses doUse
565
543
  const skip_pp = charging or state.move == .Struggle or
@@ -638,7 +616,7 @@ pub fn afterMove(
638
616
 
639
617
  pub fn checkCriticalHit(battle: anytype, player: Player, state: *State, options: anytype) !void {
640
618
  const move = Move.get(state.move);
641
- if (move.bp == 0) return;
619
+ if (state.bp == 0) return;
642
620
 
643
621
  const side = battle.side(player);
644
622
  const pokemon = side.stored();
@@ -653,7 +631,7 @@ pub fn checkCriticalHit(battle: anytype, player: Player, state: *State, options:
653
631
  }
654
632
  assert(stage <= 4);
655
633
 
656
- state.crit = try Rolls.criticalHit(battle, player, CRITICAL_HITS[stage], options);
634
+ state.crit = try Rolls.criticalHit(battle, player, state.hit, CRITICAL_HITS[stage], options);
657
635
  }
658
636
 
659
637
  pub fn calcDamage(
@@ -676,35 +654,34 @@ pub fn adjustDamage(battle: anytype, player: Player, state: *State, _: anytype)
676
654
  const side = battle.side(player);
677
655
  const foe = battle.foe(player);
678
656
  const types = foe.active.types;
679
- const move = Move.get(state.move);
680
657
 
681
658
  var d = state.damage;
682
- if ((move.type == .Water and battle.field.weather == .Rain) or
683
- (move.type == .Fire and battle.field.weather == .Sun))
659
+ if ((state.type == .Water and battle.field.weather == .Rain) or
660
+ (state.type == .Fire and battle.field.weather == .Sun))
684
661
  {
685
662
  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))
663
+ } else if ((state.type == .Water and battle.field.weather == .Sun) or
664
+ ((state.type == .Fire or state.move == .SolarBeam) and battle.field.weather == .Rain))
688
665
  {
689
666
  d = d *% @intFromEnum(Effectiveness.Resisted) / 10;
690
667
  }
691
668
 
692
- if (side.active.types.includes(move.type)) d +%= d / 2;
669
+ if (side.active.types.includes(state.type)) d +%= d / 2;
693
670
 
694
671
  const neutral = @intFromEnum(Effectiveness.Neutral);
695
- const foresight = foe.active.volatiles.Foresight and FORESIGHT.includes(move.type);
672
+ const foresight = foe.active.volatiles.Foresight and FORESIGHT.includes(state.type);
696
673
  const eff1: u16 = if (foresight and types.type1 == .Ghost)
697
674
  neutral
698
675
  else
699
- @intFromEnum(move.type.effectiveness(types.type1));
676
+ @intFromEnum(state.type.effectiveness(types.type1));
700
677
  const eff2: u16 = if (foresight and types.type2 == .Ghost)
701
678
  neutral
702
679
  else
703
- @intFromEnum(move.type.effectiveness(types.type2));
680
+ @intFromEnum(state.type.effectiveness(types.type2));
704
681
 
705
682
  // Type effectiveness matchup precedence only matters with (NVE, SE)
706
683
  if (!showdown and (eff1 + eff2) == Effectiveness.mismatch and
707
- types.type1.precedence() > types.type2.precedence())
684
+ Type.precedence(state.type, types.type1) > Type.precedence(state.type, types.type2))
708
685
  {
709
686
  assert(eff2 != neutral);
710
687
  d = d *% eff2 / 10;
@@ -722,7 +699,7 @@ pub fn adjustDamage(battle: anytype, player: Player, state: *State, _: anytype)
722
699
 
723
700
  pub fn randomizeDamage(battle: anytype, player: Player, state: *State, options: anytype) !void {
724
701
  if (state.damage <= 1) return;
725
- const random = try Rolls.damage(battle, player, options);
702
+ const random = try Rolls.damage(battle, player, state.hit, options);
726
703
  options.calc.base(player.foe(), state.damage);
727
704
  state.damage = @intCast(@as(u32, state.damage) *% random / 255);
728
705
  options.calc.final(player.foe(), state.damage);
@@ -840,14 +817,14 @@ fn checkFaint(
840
817
  if (foe_fainted) try faint(battle, player.foe(), !more, options);
841
818
 
842
819
  if (player_out and foe_out) {
843
- try options.log.tie();
844
- return Result.Tie;
820
+ try options.log.tie(.{});
821
+ return .Tie;
845
822
  } else if (player_out) {
846
- try options.log.win(player.foe());
847
- return if (player == .P1) Result.Lose else Result.Win;
823
+ try options.log.win(.{player.foe()});
824
+ return if (player == .P1) .Lose else .Win;
848
825
  } else if (foe_out) {
849
- try options.log.win(player);
850
- return if (player == .P1) Result.Win else Result.Lose;
826
+ try options.log.win(.{player});
827
+ return if (player == .P1) .Win else .Lose;
851
828
  }
852
829
 
853
830
  const foe_choice: Choice.Type = if (foe_fainted) .Switch else .Pass;
@@ -876,19 +853,19 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !void {
876
853
  side.stored().stats.spe;
877
854
  }
878
855
 
879
- try options.log.faint(battle.active(player), done);
856
+ try options.log.faint(.{ battle.active(player), done });
880
857
  options.calc.capped(player);
881
858
  }
882
859
 
883
860
  // TODO: see if we can move *before* applyDamage and include failuretext logic
884
861
  pub fn reportOutcome(battle: anytype, player: Player, state: *State, options: anytype) !void {
885
862
  const foe_ident = battle.active(player.foe());
886
- if (state.crit) try options.log.crit(foe_ident);
863
+ if (state.crit) try options.log.crit(.{foe_ident});
887
864
  if (!state.immune()) {
888
865
  if (state.effectiveness > Effectiveness.neutral) {
889
- try options.log.supereffective(foe_ident);
866
+ try options.log.supereffective(.{foe_ident});
890
867
  } else if (state.effectiveness < Effectiveness.neutral) {
891
- try options.log.resisted(foe_ident);
868
+ try options.log.resisted(.{foe_ident});
892
869
  }
893
870
  }
894
871
  }
@@ -921,8 +898,8 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !bool {
921
898
  }
922
899
 
923
900
  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);
901
+ // Pokémon Showdown uses damage [of] here but its not relevant in Generation II
902
+ try options.log.damage(.{ ident, stored, if (brn) Damage.Burn else Damage.Poison });
926
903
  if (stored.hp == 0) return true;
927
904
  }
928
905
 
@@ -938,27 +915,27 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !bool {
938
915
 
939
916
  const damage = @max(stored.stats.hp / 8, 1);
940
917
  stored.hp -|= damage;
941
- // As above, Pokémon Showdown uses damageOf but its not relevant
942
- try options.log.damage(ident, stored, .LeechSeed);
918
+ // As above, Pokémon Showdown uses damage [of] but its not relevant
919
+ try options.log.damage(.{ ident, stored, .LeechSeed });
943
920
 
944
921
  const before = foe_stored.hp;
945
922
  foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
946
923
  // 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);
924
+ if (foe_stored.hp > before) try options.log.heal(.{ foe_ident, foe_stored, .Silent });
948
925
  if (stored.hp == 0) return true;
949
926
  }
950
927
 
951
928
  if (volatiles.Nightmare) {
952
929
  stored.hp -= @max(stored.stats.hp / 4, 1);
953
930
  // ibid
954
- try options.log.damage(ident, stored, .Nightmare);
931
+ try options.log.damage(.{ ident, stored, .Nightmare });
955
932
  if (stored.hp == 0) return true;
956
933
  }
957
934
 
958
935
  if (volatiles.Curse) {
959
936
  stored.hp -= @max(stored.stats.hp / 4, 1);
960
937
  // ibid
961
- try options.log.damage(ident, stored, .Curse);
938
+ try options.log.damage(.{ ident, stored, .Curse });
962
939
  if (stored.hp == 0) return true;
963
940
  }
964
941
 
@@ -979,7 +956,7 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
979
956
  future_sight.count -= 1;
980
957
  assert(future_sight.count != 0);
981
958
  if (future_sight.count == 1) {
982
- try options.log.end(battle.active(player), .FutureSight);
959
+ try options.log.end(.{ battle.active(player), .FutureSight });
983
960
  // TODO doMove
984
961
  }
985
962
  }
@@ -988,21 +965,24 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
988
965
 
989
966
  // Weather
990
967
  if (battle.field.weather != .None) {
991
- assert(battle.field.weather_duration > 0);
992
968
  battle.field.weather_duration -= 1;
993
969
 
994
970
  if (battle.field.weather_duration == 0) {
995
971
  battle.field.weather = .None;
996
- try options.log.weather(battle.field.weather, .None);
972
+ try options.log.weather(.{ battle.field.weather, protocol.Weather.None });
997
973
  } else {
998
- try options.log.weather(battle.field.weather, .Upkeep);
974
+ try options.log.weather(.{ battle.field.weather, protocol.Weather.Upkeep });
999
975
  if (battle.field.weather == .Sandstorm) {
1000
976
  inline for (players) |player| {
1001
977
  var side = battle.side(player);
1002
978
  const active = side.active;
1003
979
  if (!(active.volatiles.Underground or active.types.sandstormImmune())) {
1004
980
  side.stored().hp -|= @max(side.stored().stats.hp / 8, 1);
1005
- try options.log.damage(battle.active(player), side.stored(), .Sandstorm);
981
+ try options.log.damage(.{
982
+ battle.active(player),
983
+ side.stored(),
984
+ .Sandstorm,
985
+ });
1006
986
  }
1007
987
  }
1008
988
  if (try checkFaint(battle, .P1, options)) |r| return r;
@@ -1014,19 +994,24 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1014
994
  inline for (players) |player| {
1015
995
  var side = battle.side(player);
1016
996
  var volatiles = &side.active.volatiles;
997
+ const ident = battle.active(player);
1017
998
 
1018
999
  if (volatiles.bind.duration > 0 and !volatiles.Substitute) {
1019
1000
  volatiles.bind.duration -= 1;
1020
1001
  if (volatiles.bind.duration == 0) {
1021
1002
  const reason = @intFromEnum(End.Bind) + volatiles.bind.reason - 1;
1022
- try options.log.end(battle.active(player), @enumFromInt(reason));
1003
+ try options.log.end(.{ ident, @as(End, @enumFromInt(reason)) });
1023
1004
  } else {
1024
1005
  var reason = @intFromEnum(Activate.Bind) + volatiles.bind.reason - 1;
1025
- try options.log.activate(battle.active(player), @enumFromInt(reason));
1006
+ try options.log.activate(.{ ident, @as(Activate, @enumFromInt(reason)) });
1026
1007
 
1027
1008
  reason = @intFromEnum(Damage.Bind) + volatiles.bind.reason - 1;
1028
1009
  side.stored().hp -|= @max(side.stored().stats.hp / 16, 1);
1029
- try options.log.damage(battle.active(player), side.stored(), @enumFromInt(reason));
1010
+ try options.log.damage(.{
1011
+ ident,
1012
+ side.stored(),
1013
+ @as(Damage, @enumFromInt(reason)),
1014
+ });
1030
1015
  }
1031
1016
  }
1032
1017
  }
@@ -1038,12 +1023,11 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1038
1023
  var volatiles = &side.active.volatiles;
1039
1024
 
1040
1025
  if (volatiles.PerishSong) {
1041
- assert(volatiles.perish_song > 0);
1042
1026
  volatiles.perish_song -= 1;
1043
1027
 
1044
1028
  assert(volatiles.perish_song < 3);
1045
1029
  const reason = @intFromEnum(Start.PerishSong0) + volatiles.perish_song;
1046
- try options.log.start(battle.active(player), @enumFromInt(reason));
1030
+ try options.log.start(.{ battle.active(player), @as(Start, @enumFromInt(reason)) });
1047
1031
 
1048
1032
  if (volatiles.perish_song == 0) {
1049
1033
  volatiles.PerishSong = false;
@@ -1060,13 +1044,15 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1060
1044
  if (stored.item == .Leftovers) {
1061
1045
  const before = stored.hp;
1062
1046
  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);
1047
+ if (stored.hp > before) {
1048
+ try options.log.heal(.{ battle.active(player), stored, .Leftovers });
1049
+ }
1064
1050
  }
1065
1051
  }
1066
1052
 
1067
1053
  // Mystery Berry
1068
1054
  inline for (players) |player| {
1069
- var stored = battle.side(player).stored();
1055
+ const stored = battle.side(player).stored();
1070
1056
 
1071
1057
  if (stored.item == .MysteryBerry) {
1072
1058
  // TODO stored and active moves, transform, etc
@@ -1081,7 +1067,7 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1081
1067
  !side.active.volatiles.frozen and
1082
1068
  try Rolls.defrost(battle, player, options))
1083
1069
  {
1084
- try options.log.curestatus(battle.active(player), side.stored().status, .Message);
1070
+ try options.log.curestatus(.{ battle.active(player), side.stored().status, .Message });
1085
1071
  side.stored().status = 0;
1086
1072
  }
1087
1073
  }
@@ -1122,14 +1108,14 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1122
1108
  stored.hp < stored.stats.hp;
1123
1109
  if (proc) {
1124
1110
  assert(stored.hp != 0);
1125
- try options.log.enditem(ident, stored.item, .Eat);
1111
+ try options.log.enditem(.{ ident, stored.item, .Eat });
1126
1112
  stored.item = .None;
1127
1113
 
1128
1114
  const offset = num - @intFromEnum(Item.Berry);
1129
1115
  assert(offset < 3);
1130
1116
  stored.hp = @min(stored.hp + ((offset + 1) * 10), stored.stats.hp);
1131
1117
  const reason: Heal = @enumFromInt(@intFromEnum(Heal.Berry) + offset);
1132
- try options.log.heal(ident, stored, reason);
1118
+ try options.log.heal(.{ ident, stored, reason });
1133
1119
  }
1134
1120
  } else if (num < @intFromEnum(Item.BitterBerry)) {
1135
1121
  const proc = if (stored.item == .MiracleBerry)
@@ -1139,7 +1125,7 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1139
1125
 
1140
1126
  if (proc) {
1141
1127
  assert(Status.any(stored.status));
1142
- try options.log.enditem(ident, stored.item, .Eat);
1128
+ try options.log.enditem(.{ ident, stored.item, .Eat });
1143
1129
  stored.item = .None;
1144
1130
 
1145
1131
  // FIXME wtf?
@@ -1147,21 +1133,21 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1147
1133
  // side.active.volatiles.Nightmare = false;
1148
1134
  if (stored.item == .MiracleBerry) {
1149
1135
  side.active.volatiles.Confusion = false;
1150
- try options.log.end(ident, .Confusion);
1136
+ try options.log.end(.{ ident, .Confusion });
1151
1137
  }
1152
1138
  stored.status = 0;
1153
1139
 
1154
1140
  // FIXME recalc stats
1155
1141
 
1156
- try options.log.curestatus(ident, stored.status, .Message);
1142
+ try options.log.curestatus(.{ ident, stored.status, .Message });
1157
1143
  }
1158
1144
  } else if (num == @intFromEnum(Item.BitterBerry)) {
1159
1145
  if (side.active.volatiles.Confusion) {
1160
- try options.log.enditem(ident, stored.item, .Eat);
1146
+ try options.log.enditem(.{ ident, stored.item, .Eat });
1161
1147
  stored.item = .None;
1162
1148
 
1163
1149
  side.active.volatiles.Confusion = false;
1164
- try options.log.end(ident, .Confusion);
1150
+ try options.log.end(.{ ident, .Confusion });
1165
1151
  }
1166
1152
  }
1167
1153
  }
@@ -1173,12 +1159,11 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
1173
1159
  var volatiles = &side.active.volatiles;
1174
1160
 
1175
1161
  if (volatiles.Encore) {
1176
- assert(volatiles.encore > 0);
1177
1162
  volatiles.encore -= 1;
1178
1163
 
1179
1164
  if (volatiles.encore == 0 or side.active.move(mslot).pp == 0) {
1180
1165
  volatiles.Encore = false;
1181
- try options.log.end(battle.active(player), .Encore);
1166
+ try options.log.end(.{ battle.active(player), .Encore });
1182
1167
  }
1183
1168
  }
1184
1169
  }
@@ -1195,12 +1180,11 @@ fn updateSideCondition(
1195
1180
  ) !void {
1196
1181
  var side = battle.side(player);
1197
1182
  if (@field(side.conditions, key)) {
1198
- assert(@field(side.conditions, value) > 0);
1199
1183
  @field(side.conditions, value) -= 1;
1200
1184
 
1201
1185
  if (@field(side.conditions, value) == 0) {
1202
1186
  @field(side.conditions, key) = false;
1203
- try options.log.sideend(player, @field(protocol.Side, key));
1187
+ try options.log.sideend(.{ player, @field(protocol.Side, key) });
1204
1188
  }
1205
1189
  }
1206
1190
  }
@@ -1208,16 +1192,16 @@ fn updateSideCondition(
1208
1192
  fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result {
1209
1193
  battle.turn += 1;
1210
1194
 
1211
- if (showdown and battle.turn >= 1000) {
1212
- try options.log.tie();
1213
- return Result.Tie;
1195
+ if (pkmn.options.mod and battle.turn >= 1000) {
1196
+ try options.log.tie(.{});
1197
+ return .Tie;
1214
1198
  } else if (battle.turn >= 65535) {
1215
- return Result.Error;
1199
+ return .Error;
1216
1200
  }
1217
1201
 
1218
- try options.log.turn(battle.turn);
1202
+ try options.log.turn(.{battle.turn});
1219
1203
 
1220
- return Result.Default;
1204
+ return .Default;
1221
1205
  }
1222
1206
 
1223
1207
  pub fn buildRage(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1231,7 +1215,7 @@ pub fn buildRage(battle: anytype, player: Player, state: *State, options: anytyp
1231
1215
  }
1232
1216
  } else {
1233
1217
  foe.active.volatiles.rage +|= 1;
1234
- try options.log.activate(battle.active(player), .Rage);
1218
+ try options.log.activate(.{ battle.active(player), .Rage });
1235
1219
  }
1236
1220
  }
1237
1221
  }
@@ -1254,16 +1238,12 @@ pub fn destinyBond(battle: anytype, player: Player, _: *State, options: anytype)
1254
1238
 
1255
1239
  if (foe.active.volatiles.DestinyBond) {
1256
1240
  battle.side(player).stored().hp = 0;
1257
- try options.log.activate(battle.active(player.foe()), .DestinyBond);
1241
+ try options.log.activate(.{ battle.active(player.foe()), .DestinyBond });
1258
1242
  }
1259
1243
  return true;
1260
1244
  }
1261
1245
 
1262
1246
  pub const Effects = struct {
1263
- pub fn alwaysHit(battle: anytype, player: Player, state: *State, options: anytype) !void {
1264
- _ = .{ battle, player, state, options }; // TODO
1265
- }
1266
-
1267
1247
  pub fn attract(battle: anytype, player: Player, _: *State, options: anytype) !void {
1268
1248
  const side = battle.side(player);
1269
1249
  var foe = battle.foe(player);
@@ -1271,16 +1251,14 @@ pub const Effects = struct {
1271
1251
  const gender: u8 = @intFromEnum(side.stored().dvs.gender);
1272
1252
  const foe_gender: u8 = @intFromEnum(foe.stored().dvs.gender);
1273
1253
  if (gender + foe_gender != 1) {
1274
- try options.log.immune(battle.active(player.foe()), .None);
1275
- return;
1254
+ return options.log.immune(.{ battle.active(player.foe()), .None });
1276
1255
  } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
1277
- try options.log.lastmiss();
1278
- try options.log.miss(battle.active(player));
1279
- return;
1256
+ try options.log.lastmiss(.{});
1257
+ return options.log.miss(.{battle.active(player)});
1280
1258
  }
1281
1259
 
1282
1260
  foe.active.volatiles.Attract = true;
1283
- try options.log.start(battle.active(player.foe()), .Attract);
1261
+ try options.log.start(.{ battle.active(player.foe()), .Attract });
1284
1262
  }
1285
1263
 
1286
1264
  pub fn batonPass(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1292,7 +1270,32 @@ pub const Effects = struct {
1292
1270
  }
1293
1271
 
1294
1272
  pub fn bellyDrum(battle: anytype, player: Player, state: *State, options: anytype) !void {
1295
- _ = .{ battle, player, state, options }; // TODO
1273
+ assert(!state.miss);
1274
+ var side = battle.side(player);
1275
+
1276
+ const ident = battle.active(player);
1277
+
1278
+ assert(side.active.boosts.atk >= -6 and side.active.boosts.atk <= 6);
1279
+ if (side.active.boosts.atk == 6 or side.active.boosts.atk == MAX_STAT_VALUE) {
1280
+ return options.log.fail(.{ ident, .None });
1281
+ }
1282
+
1283
+ const hp = @max(side.stored().stats.hp / 2, 1);
1284
+ if (side.stored().hp <= hp) {
1285
+ try boostStat(battle, side, &side.active.stats, &side.active.boosts, "atk", 2);
1286
+ try options.log.boost(.{ ident, .Attack, @as(i8, 2), .Silent });
1287
+ return options.log.fail(.{ ident, .None });
1288
+ } else {
1289
+ side.stored().hp -= hp;
1290
+ try options.log.damage(.{ ident, side.stored(), .None });
1291
+
1292
+ const before = side.active.boosts.atk;
1293
+ var i: usize = 0;
1294
+ while (i < 6 and side.active.stats.atk < MAX_STAT_VALUE) : (i += 1) {
1295
+ try boostStat(battle, side, &side.active.stats, &side.active.boosts, "atk", 2);
1296
+ }
1297
+ try options.log.boost(.{ ident, .Attack, @as(i8, side.active.boosts.atk - before) });
1298
+ }
1296
1299
  }
1297
1300
 
1298
1301
  pub const bide = struct {
@@ -1304,18 +1307,11 @@ pub const Effects = struct {
1304
1307
 
1305
1308
  if (!volatiles.Bide) return;
1306
1309
 
1307
- assert(volatiles.attacks > 0);
1308
- // if (options.calc.modify(player, .attacking)) |extend| {
1309
- // if (!extend) volatiles.attacks = 0;
1310
- // } else {
1311
1310
  volatiles.attacks -= 1;
1312
- // }
1313
- try options.chance.attacking(player, volatiles.attacks);
1314
-
1315
- if (volatiles.attacks != 0) return options.log.activate(ident, .Bide);
1311
+ if (volatiles.attacks != 0) return options.log.activate(.{ ident, .Bide });
1316
1312
 
1317
1313
  volatiles.Bide = false;
1318
- try options.log.end(ident, .Bide);
1314
+ try options.log.end(.{ ident, .Bide });
1319
1315
 
1320
1316
  state.damage = volatiles.bide *% 2;
1321
1317
  volatiles.bide = 0;
@@ -1329,7 +1325,16 @@ pub const Effects = struct {
1329
1325
  };
1330
1326
 
1331
1327
  pub fn binding(battle: anytype, player: Player, state: *State, options: anytype) !void {
1332
- _ = .{ battle, player, state, options }; // TODO
1328
+ var foe = battle.foe(player);
1329
+
1330
+ if (state.miss) return;
1331
+ if (foe.active.volatiles.bind.duration > 0) return;
1332
+ if (foe.active.volatiles.Substitute) return;
1333
+
1334
+ foe.active.volatiles.bind = .{
1335
+ .duration = Rolls.bindingDuration(battle, player, options),
1336
+ .reason = Move.get(state.move).extra.protocol,
1337
+ };
1333
1338
  }
1334
1339
 
1335
1340
  pub fn burnChance(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1339,12 +1344,12 @@ pub const Effects = struct {
1339
1344
  if (foe.active.volatiles.Substitute) return;
1340
1345
  if (Status.any(foe_stored.status)) {
1341
1346
  if (Status.is(foe_stored.status, .FRZ)) {
1342
- assert(Move.get(state.move).type == .Fire);
1343
- try options.log.curestatus(
1347
+ assert(Move.get(state.move).type == .Fire or state.move == .TriAttack);
1348
+ try options.log.curestatus(.{
1344
1349
  battle.active(player.foe()),
1345
1350
  foe_stored.status,
1346
1351
  .Message,
1347
- );
1352
+ });
1348
1353
  foe_stored.status = 0;
1349
1354
  }
1350
1355
  return;
@@ -1353,11 +1358,14 @@ pub const Effects = struct {
1353
1358
  if (foe.conditions.Safeguard) return;
1354
1359
  // Nothing is immune to Fire-type, but Ghosts are immune to burnChance called by Tri Attack
1355
1360
  if (state.immune()) return;
1356
- if (foe.active.types.includes(Move.get(state.move).type)) return;
1361
+ if (foe.active.types.includes(Move.get(state.move).type) and state.move != .TriAttack) {
1362
+ return;
1363
+ }
1357
1364
 
1358
1365
  foe_stored.status = Status.init(.BRN);
1359
1366
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
1360
- try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
1367
+
1368
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
1361
1369
 
1362
1370
  // TODO: check for status berry
1363
1371
  }
@@ -1377,10 +1385,7 @@ pub const Effects = struct {
1377
1385
  for (side.active.moves) |m| {
1378
1386
  if (convertible(side.active, m.id)) n += 1;
1379
1387
  }
1380
- if (n == 0) {
1381
- try options.log.fail(battle.active(player), .None);
1382
- return;
1383
- }
1388
+ if (n == 0) return options.log.fail(.{ battle.active(player), .None });
1384
1389
 
1385
1390
  side.active.types.type1 = try Rolls.conversion(battle, player, n, options);
1386
1391
  side.active.types.type2 = side.active.types.type1;
@@ -1390,26 +1395,87 @@ pub const Effects = struct {
1390
1395
  var side = battle.side(player);
1391
1396
 
1392
1397
  const last = battle.foe(player).lastMove(false);
1393
- if (last == .None) {
1394
- try options.log.fail(battle.active(player), .None);
1395
- return;
1396
- }
1398
+ if (last == .None) return options.log.fail(.{ battle.active(player), .None });
1397
1399
  const move = Move.get(last);
1398
- if (move.type == .@"???") {
1399
- try options.log.fail(battle.active(player), .None);
1400
- return;
1401
- }
1400
+ if (move.type == .@"???") return options.log.fail(.{ battle.active(player), .None });
1402
1401
 
1403
1402
  side.active.types.type1 = try Rolls.conversion2(battle, player, move.type, options);
1404
1403
  side.active.types.type2 = side.active.types.type1;
1405
1404
  }
1406
1405
 
1407
- pub fn counter(battle: anytype, player: Player, state: *State, options: anytype) !void {
1408
- _ = .{ battle, player, state, options }; // TODO
1406
+ pub fn counter(battle: anytype, player: Player, state: *State, _: anytype) !void {
1407
+ assert(state.move == .Counter or state.move == .MirrorCoat);
1408
+
1409
+ const foe = battle.foe(player);
1410
+
1411
+ state.miss = true;
1412
+ const last = foe.lastMove(false);
1413
+
1414
+ if (last == .None) return;
1415
+ if (last == state.move) return;
1416
+
1417
+ const immune = state.immune();
1418
+ state.effectiveness = Effectiveness.neutral;
1419
+ if (immune) {
1420
+ state.damage = 0;
1421
+ return;
1422
+ }
1423
+
1424
+ const move = Move.get(last);
1425
+ if (state.bp == 0) return;
1426
+ if (move.type.special() != (last == .MirrorCoat)) return;
1427
+ if (state.damage == 0) return;
1428
+
1429
+ state.damage *|= 2;
1430
+ state.miss = false;
1409
1431
  }
1410
1432
 
1411
1433
  pub fn curse(battle: anytype, player: Player, state: *State, options: anytype) !void {
1412
- _ = .{ battle, player, state, options }; // TODO
1434
+ var side = battle.side(player);
1435
+ var foe = battle.foe(player);
1436
+
1437
+ const ident = battle.active(player);
1438
+ const foe_ident = battle.active(player.foe());
1439
+
1440
+ if (side.active.types.includes(.Ghost)) {
1441
+ if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
1442
+ try options.log.lastmiss(.{});
1443
+ return options.log.miss(.{battle.active(player)});
1444
+ } else if (foe.active.volatiles.Substitute) {
1445
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
1446
+ } else if (foe.active.volatiles.Curse) {
1447
+ return options.log.fail(.{ foe_ident, .None });
1448
+ }
1449
+
1450
+ foe.active.volatiles.Curse = true;
1451
+ try options.log.start(.{ foe_ident, .Curse });
1452
+
1453
+ side.stored().hp -= @max(side.stored().stats.hp / 2, 1);
1454
+ try options.log.damage(.{ ident, side.stored(), .Curse });
1455
+ } else {
1456
+ const stats = &side.active.stats;
1457
+ const boosts = &side.active.boosts;
1458
+
1459
+ const max_atk = boosts.atk == 6 or stats.atk == MAX_STAT_VALUE;
1460
+ const max_def = boosts.def == 6 or stats.def == MAX_STAT_VALUE;
1461
+ if (max_atk and max_def) return options.log.fail(.{ ident, .None });
1462
+
1463
+ if (!(boosts.spe == -6 or stats.spe == 1)) {
1464
+ assert(boosts.spe >= -6 and boosts.spe <= 6);
1465
+ try unboostStat(battle, side, stats, boosts, "spe", 1);
1466
+ try options.log.boost(.{ ident, .Speed, @as(i8, -1) });
1467
+ }
1468
+ if (!max_atk) {
1469
+ assert(boosts.atk >= -6 and boosts.atk <= 6);
1470
+ try boostStat(battle, side, stats, boosts, "atk", 1);
1471
+ try options.log.boost(.{ ident, .Attack, @as(i8, 1) });
1472
+ }
1473
+ if (!max_def) {
1474
+ assert(boosts.def >= -6 and boosts.def <= 6);
1475
+ try boostStat(battle, side, stats, boosts, "def", 1);
1476
+ try options.log.boost(.{ ident, .Defense, @as(i8, 1) });
1477
+ }
1478
+ }
1413
1479
  }
1414
1480
 
1415
1481
  pub fn defenseCurl(battle: anytype, player: Player, _: *State, _: anytype) !void {
@@ -1419,14 +1485,13 @@ pub const Effects = struct {
1419
1485
  pub fn defrost(battle: anytype, player: Player, _: *State, options: anytype) !void {
1420
1486
  var side = battle.side(player);
1421
1487
  assert(Status.is(side.stored().status, .FRZ));
1422
-
1423
- try options.log.curestatus(battle.active(player), side.stored().status, .Message);
1488
+ try options.log.curestatus(.{ battle.active(player), side.stored().status, .Message });
1424
1489
  side.stored().status = 0;
1425
1490
  }
1426
1491
 
1427
1492
  pub fn destinyBond(battle: anytype, player: Player, _: *State, options: anytype) !void {
1428
1493
  battle.side(player).active.volatiles.DestinyBond = true;
1429
- try options.log.singlemove(battle.active(player), Move.DestinyBond);
1494
+ try options.log.singlemove(.{ battle.active(player), Move.DestinyBond });
1430
1495
  }
1431
1496
 
1432
1497
  pub fn disable(battle: anytype, player: Player, _: *State, options: anytype) !void {
@@ -1434,16 +1499,10 @@ pub const Effects = struct {
1434
1499
  var volatiles = &foe.active.volatiles;
1435
1500
  const foe_ident = battle.active(player.foe());
1436
1501
 
1437
- if (volatiles.disable.move != 0) {
1438
- try options.log.fail(foe_ident, .None);
1439
- return;
1440
- }
1502
+ if (volatiles.disable.move != 0) return options.log.fail(.{ foe_ident, .None });
1441
1503
 
1442
1504
  const last = battle.foe(player).lastMove(false);
1443
- if (last == .None or last == .Struggle) {
1444
- try options.log.fail(foe_ident, .None);
1445
- return;
1446
- }
1505
+ if (last == .None or last == .Struggle) return options.log.fail(.{ foe_ident, .None });
1447
1506
 
1448
1507
  var slot: u3 = 0;
1449
1508
  for (foe.active.moves) |m| {
@@ -1452,15 +1511,12 @@ pub const Effects = struct {
1452
1511
  if (m.id == last) break;
1453
1512
  }
1454
1513
  const move = foe.active.move(slot);
1455
- if (move.pp == 0) {
1456
- try options.log.fail(foe_ident, .None);
1457
- return;
1458
- }
1514
+ if (move.pp == 0) return options.log.fail(.{ foe_ident, .None });
1459
1515
 
1460
1516
  volatiles.disable.move = slot;
1461
1517
  volatiles.disable.duration = Rolls.disableDuration(battle, player, options);
1462
1518
 
1463
- try options.log.startEffect(foe_ident, .Disable, move.id);
1519
+ try options.log.start(.{ foe_ident, .Disable, move.id });
1464
1520
  }
1465
1521
 
1466
1522
  pub const double = struct {
@@ -1479,13 +1535,14 @@ pub const Effects = struct {
1479
1535
 
1480
1536
  pub fn drainHP(battle: anytype, player: Player, state: *State, options: anytype) !void {
1481
1537
  var stored = battle.side(player).stored();
1538
+ const ident = battle.active(player);
1482
1539
 
1483
1540
  assert(state.damage != 0);
1484
1541
 
1485
1542
  if (stored.hp == stored.stats.hp) return;
1486
1543
  stored.hp = @min(stored.stats.hp, stored.hp + @max(state.damage / 2, 1));
1487
1544
 
1488
- try options.log.drain(battle.active(player), stored, battle.active(player.foe()));
1545
+ try options.log.heal(.{ ident, stored, .Drain, battle.active(player.foe()) });
1489
1546
  }
1490
1547
 
1491
1548
  pub fn encore(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1496,11 +1553,8 @@ pub const Effects = struct {
1496
1553
  const last = battle.foe(player).lastMove(true);
1497
1554
  const failed = volatiles.Encore or
1498
1555
  (last == .None or last == .Struggle or
1499
- last == .Encore or .last == .MirrorMove);
1500
- if (failed) {
1501
- try options.log.fail(foe_ident, .None);
1502
- return;
1503
- }
1556
+ last == .Encore or .last == .MirrorMove);
1557
+ if (failed) return options.log.fail(.{ foe_ident, .None });
1504
1558
 
1505
1559
  var slot: u3 = 0;
1506
1560
  for (foe.active.moves) |m| {
@@ -1509,10 +1563,7 @@ pub const Effects = struct {
1509
1563
  if (m.id == last) break;
1510
1564
  }
1511
1565
  const move = foe.active.move(slot);
1512
- if (move.pp == 0) {
1513
- try options.log.fail(foe_ident, .None);
1514
- return;
1515
- }
1566
+ if (move.pp == 0) return options.log.fail(.{ foe_ident, .None });
1516
1567
 
1517
1568
  volatiles.Encore = true;
1518
1569
  volatiles.encore = Rolls.encoreDuration(battle, player, options);
@@ -1520,11 +1571,19 @@ pub const Effects = struct {
1520
1571
  // TODO ???
1521
1572
  _ = state;
1522
1573
 
1523
- try options.log.startEffect(foe_ident, .Encore, move.id);
1574
+ try options.log.start(.{ foe_ident, .Encore, move.id });
1524
1575
  }
1525
1576
 
1526
- pub fn explode(battle: anytype, player: Player, state: *State, options: anytype) !void {
1527
- _ = .{ battle, player, state, options }; // TODO
1577
+ pub fn explode(battle: anytype, player: Player, _: *State, _: anytype) !void {
1578
+ var side = battle.side(player);
1579
+ var stored = side.stored();
1580
+
1581
+ stored.hp = 0;
1582
+ // Pokémon Showdown sets the status to 0 on faint(), and we need to do the same to be able
1583
+ // to correctly implement Pokémon Showdown's dumb speed-based switch mechanics
1584
+ if (!showdown) stored.status = 0;
1585
+ side.active.volatiles.LeechSeed = false;
1586
+ side.active.volatiles.DestinyBond = false;
1528
1587
  }
1529
1588
 
1530
1589
  pub fn falseSwipe(battle: anytype, player: Player, state: *State, _: anytype) !void {
@@ -1552,10 +1611,6 @@ pub const Effects = struct {
1552
1611
  };
1553
1612
  }
1554
1613
 
1555
- pub fn flameWheel(battle: anytype, player: Player, state: *State, options: anytype) !void {
1556
- _ = .{ battle, player, state, options }; // TODO
1557
- }
1558
-
1559
1614
  pub fn flinchChance(battle: anytype, player: Player, state: *State, _: anytype) !void {
1560
1615
  const foe = battle.foe(player);
1561
1616
  const status = foe.stored().status;
@@ -1578,12 +1633,11 @@ pub const Effects = struct {
1578
1633
  var side = battle.side(player);
1579
1634
 
1580
1635
  if (side.active.volatiles.FocusEnergy) {
1581
- if (!showdown) try options.log.fail(battle.active(player), .None);
1582
- return;
1636
+ return if (!showdown) try options.log.fail(.{ battle.active(player), .None });
1583
1637
  }
1584
1638
 
1585
1639
  side.active.volatiles.FocusEnergy = true;
1586
- try options.log.start(battle.active(player), .FocusEnergy);
1640
+ try options.log.start(.{ battle.active(player), .FocusEnergy });
1587
1641
  }
1588
1642
 
1589
1643
  var SLOTS: [5]u4 = .{ 0, 0, 0, 0, 0 };
@@ -1593,7 +1647,7 @@ pub const Effects = struct {
1593
1647
 
1594
1648
  const foe = battle.foe(player);
1595
1649
 
1596
- if (state.first) return options.log.fail(battle.active(player), .None);
1650
+ if (state.first) return options.log.fail(.{ battle.active(player), .None });
1597
1651
 
1598
1652
  var n: u4 = 0;
1599
1653
  var i: u4 = 0;
@@ -1607,10 +1661,7 @@ pub const Effects = struct {
1607
1661
  }
1608
1662
  }
1609
1663
 
1610
- if (n == 0) {
1611
- try options.log.fail(battle.active(player), .None);
1612
- return;
1613
- }
1664
+ if (n == 0) return options.log.fail(.{ battle.active(player), .None });
1614
1665
 
1615
1666
  const slot = try Rolls.forceSwitch(battle, player, SLOTS[0..i], n, options);
1616
1667
  try switchIn(battle, player.foe(), slot, .Drag, options);
@@ -1620,16 +1671,14 @@ pub const Effects = struct {
1620
1671
  var foe = battle.foe(player);
1621
1672
 
1622
1673
  if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
1623
- try options.log.lastmiss();
1624
- try options.log.miss(battle.active(player));
1625
- return;
1674
+ try options.log.lastmiss(.{});
1675
+ return options.log.miss(.{battle.active(player)});
1626
1676
  } else if (foe.active.volatiles.Foresight) {
1627
- try options.log.fail(battle.active(player), .None);
1628
- return;
1677
+ return options.log.fail(.{ battle.active(player), .None });
1629
1678
  }
1630
1679
 
1631
1680
  foe.active.volatiles.Foresight = true;
1632
- try options.log.start(battle.active(player.foe()), .FocusEnergy);
1681
+ try options.log.start(.{ battle.active(player.foe()), .FocusEnergy });
1633
1682
  }
1634
1683
 
1635
1684
  pub fn freezeChance(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1641,15 +1690,18 @@ pub const Effects = struct {
1641
1690
  if (Status.any(foe_stored.status)) return;
1642
1691
 
1643
1692
  // Nothing is immune to Ice-type, but Ghosts are immune to freezeChance called by Tri Attack
1693
+ if (state.immune()) return;
1644
1694
  if (battle.field.weather == .Sun) return;
1645
- if (foe.active.types.includes(Move.get(state.move).type)) return;
1695
+ if (foe.active.types.includes(Move.get(state.move).type) and state.move != .TriAttack) {
1696
+ return;
1697
+ }
1646
1698
  if (!state.proc) return;
1647
1699
  if (foe.conditions.Safeguard) return;
1648
1700
  // Freeze Clause Mod
1649
- if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1701
+ if (pkmn.options.mod) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1650
1702
 
1651
1703
  foe_stored.status = Status.init(.FRZ);
1652
- try options.log.status(foe_ident, foe_stored.status, .None);
1704
+ try options.log.status(.{ foe_ident, foe_stored.status, .None });
1653
1705
 
1654
1706
  // TODO: check for status berry
1655
1707
 
@@ -1657,8 +1709,26 @@ pub const Effects = struct {
1657
1709
  foe.active.volatiles.frozen = true;
1658
1710
  }
1659
1711
 
1660
- pub fn furyCutter(battle: anytype, player: Player, state: *State, options: anytype) !void {
1661
- _ = .{ battle, player, state, options }; // TODO
1712
+ // TODO FIXME showdown calls this before calcDamage (after checkHit) and modifies state.bp!
1713
+ pub fn furyCutter(battle: anytype, player: Player, state: *State, _: anytype) !void {
1714
+ var side = battle.side(player);
1715
+ var volatiles = &side.active.volatiles;
1716
+
1717
+ if (state.miss) {
1718
+ volatiles.fury_cutter = 0;
1719
+ return;
1720
+ }
1721
+
1722
+ assert(volatiles.fury_cutter >= 0 and volatiles.fury_cutter <= 5);
1723
+ if (volatiles.fury_cutter == 5) return;
1724
+ volatiles.fury_cutter += 1;
1725
+
1726
+ if (showdown) {
1727
+ state.bp *= 2;
1728
+ assert(state.bp <= 160);
1729
+ } else {
1730
+ state.damage *|= 2;
1731
+ }
1662
1732
  }
1663
1733
 
1664
1734
  pub fn futureSight(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1671,46 +1741,131 @@ pub const Effects = struct {
1671
1741
  state.bp = ((if (frustration) 255 - val else val) * 10) / 25;
1672
1742
  }
1673
1743
 
1674
- pub fn haze(battle: anytype, player: Player, state: *State, options: anytype) !void {
1675
- _ = .{ battle, player, state, options }; // TODO
1744
+ pub fn haze(battle: anytype, player: Player, _: *State, options: anytype) !void {
1745
+ var side = battle.side(player);
1746
+ var stats = unmodifiedStats(battle, side);
1747
+ side.active.boosts = .{};
1748
+ inline for (STATS) |s| @field(side.active.stats, s) = @field(stats, s);
1749
+ statusModify(side.stored().status, &side.active.stats);
1750
+
1751
+ var foe = battle.foe(player);
1752
+ stats = unmodifiedStats(battle, foe);
1753
+ foe.active.boosts = .{};
1754
+ inline for (STATS) |s| @field(foe.active.stats, s) = @field(stats, s);
1755
+ statusModify(foe.stored().status, &foe.active.stats);
1756
+
1757
+ try options.log.clearallboost(.{});
1676
1758
  }
1677
1759
 
1678
1760
  pub fn heal(battle: anytype, player: Player, state: *State, options: anytype) !void {
1679
- _ = .{ battle, player, state, options }; // TODO
1680
- }
1761
+ var side = battle.side(player);
1762
+ var stored = side.stored();
1763
+ const ident = battle.active(player);
1681
1764
 
1682
- pub fn healBell(battle: anytype, player: Player, state: *State, options: anytype) !void {
1683
- _ = .{ battle, player, state, options }; // TODO
1765
+ if (stored.stats.hp == stored.hp) return options.log.fail(.{ ident, .None });
1766
+
1767
+ const rest = state.move == .Rest;
1768
+ if (rest) {
1769
+ // Adding the sleep status runs the sleep condition handler to roll duration
1770
+ if (showdown) battle.rng.advance(1);
1771
+ side.active.volatiles.Toxic = false;
1772
+ stored.status = Status.slf(2);
1773
+ recomputeStats(battle, side);
1774
+ try options.log.status(.{ ident, stored.status, .From, Move.Rest });
1775
+ stored.hp = stored.stats.hp;
1776
+ } else {
1777
+ stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
1778
+ }
1779
+ try options.log.heal(.{ ident, stored, if (rest) Heal.Silent else Heal.None });
1684
1780
  }
1685
1781
 
1686
- pub fn hiddenPower(battle: anytype, player: Player, state: *State, options: anytype) !void {
1687
- _ = .{ battle, player, state, options }; // TODO
1782
+ pub fn healBell(battle: anytype, player: Player, _: *State, options: anytype) !void {
1783
+ var side = battle.side(player);
1784
+ side.active.volatiles.Nightmare = false;
1785
+
1786
+ try options.log.cureteam(.{battle.active(player)});
1787
+ for (&side.pokemon) |*p| p.status = 0;
1788
+
1789
+ // We technically only need to recompute stats if the active Pokémon was burned/paralyzed,
1790
+ // but adding a branch to check for that is probably more expensive than just recomputing
1791
+ recomputeStats(battle, side);
1688
1792
  }
1689
1793
 
1690
- pub fn hyperBeam(battle: anytype, player: Player, state: *State, options: anytype) !void {
1691
- _ = .{ battle, player, state, options }; // TODO
1794
+ pub fn hiddenPower(battle: anytype, player: Player, state: *State, _: anytype) !void {
1795
+ const side = battle.side(player);
1796
+ state.bp = side.stored().dvs.power();
1797
+ state.type = side.stored().dvs.type;
1692
1798
  }
1693
1799
 
1694
- pub fn jumpKick(battle: anytype, player: Player, state: *State, options: anytype) !void {
1695
- _ = .{ battle, player, state, options }; // TODO
1800
+ pub fn hyperBeam(battle: anytype, player: Player, _: *State, options: anytype) !void {
1801
+ battle.side(player).active.volatiles.Recharging = true;
1802
+ try options.log.mustrecharge(.{battle.active(player)});
1696
1803
  }
1697
1804
 
1698
1805
  pub fn leechSeed(battle: anytype, player: Player, state: *State, options: anytype) !void {
1699
- _ = .{ battle, player, state, options }; // TODO
1806
+ var foe = battle.foe(player);
1807
+
1808
+ if (showdown) {
1809
+ // Invulnerability trumps type immunity on Pokémon Showdown
1810
+ const invulnerable = foe.active.volatiles.Underground or foe.active.volatiles.Flying;
1811
+ if (!invulnerable and foe.active.types.includes(.Grass)) {
1812
+ return options.log.immune(.{ battle.active(player.foe()), .None });
1813
+ }
1814
+ try checkHit(battle, player, state, options);
1815
+ if (state.miss) {
1816
+ try options.log.lastmiss(.{});
1817
+ return options.log.miss(.{battle.active(player)});
1818
+ }
1819
+ if (foe.active.volatiles.LeechSeed) return;
1820
+ } else {
1821
+ try checkHit(battle, player, state, options);
1822
+ const miss =
1823
+ state.miss or foe.active.types.includes(.Grass) or foe.active.volatiles.LeechSeed;
1824
+ if (miss) {
1825
+ try options.log.lastmiss(.{});
1826
+ return options.log.miss(.{battle.active(player)});
1827
+ }
1828
+ }
1829
+
1830
+ foe.active.volatiles.LeechSeed = true;
1831
+
1832
+ try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
1700
1833
  }
1701
1834
 
1702
1835
  pub fn lockOn(battle: anytype, player: Player, state: *State, options: anytype) !void {
1703
- _ = .{ battle, player, state, options }; // TODO
1836
+ var foe = battle.foe(player);
1837
+ const foe_ident = battle.active(player.foe());
1838
+
1839
+ if (foe.active.volatiles.Substitute) {
1840
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
1841
+ } else if (state.miss) {
1842
+ return options.log.fail(.{ foe_ident, .None });
1843
+ }
1844
+
1845
+ foe.active.volatiles.LockOn = true;
1846
+ try options.log.activate(.{ battle.active(player), .LockOn });
1704
1847
  }
1705
1848
 
1706
1849
  const MAGNITUDE_POWER = [_]u8{ 10, 30, 50, 70, 90, 110, 150 };
1707
1850
 
1708
1851
  pub fn magnitude(battle: anytype, player: Player, state: *State, options: anytype) !void {
1709
- _ = .{ battle, player, state, options }; // TODO
1852
+ const num = try Rolls.magnitude(battle, player, options);
1853
+ try options.log.activate(.{ battle.active(player), .Magnitude, num });
1854
+ state.bp = MAGNITUDE_POWER[num - 4];
1710
1855
  }
1711
1856
 
1712
- pub fn meanLook(battle: anytype, player: Player, state: *State, options: anytype) !void {
1713
- _ = .{ battle, player, state, options }; // TODO
1857
+ pub fn meanLook(battle: anytype, player: Player, _: *State, options: anytype) !void {
1858
+ var foe = battle.foe(player);
1859
+
1860
+ if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
1861
+ if (!showdown) return options.log.fail(.{ battle.active(player.foe()), .None });
1862
+ try options.log.lastmiss(.{});
1863
+ return options.log.miss(.{battle.active(player)});
1864
+ } else if (foe.active.volatiles.Trapped) {
1865
+ return options.log.fail(.{ battle.active(player.foe()), .None });
1866
+ }
1867
+
1868
+ foe.active.volatiles.Trapped = true;
1714
1869
  }
1715
1870
 
1716
1871
  pub fn metronome(
@@ -1730,15 +1885,33 @@ pub const Effects = struct {
1730
1885
  // FIXME
1731
1886
  state.move = try Rolls.metronome(battle, player, options);
1732
1887
  state.from = .Metronome;
1733
- return try generated.runMove(battle, player, state, options);
1888
+ return generated.runMove(battle, player, state, options);
1734
1889
  }
1735
1890
 
1736
1891
  pub fn mimic(battle: anytype, player: Player, state: *State, options: anytype) !void {
1737
- _ = .{ battle, player, state, options }; // TODO
1738
- }
1892
+ var side = battle.side(player);
1739
1893
 
1740
- pub fn mirrorCoat(battle: anytype, player: Player, state: *State, options: anytype) !void {
1741
- _ = .{ battle, player, state, options }; // TODO
1894
+ if (state.miss) {
1895
+ if (!showdown) return options.log.fail(.{ battle.active(player.foe()), .None });
1896
+ try options.log.lastmiss(.{});
1897
+ return options.log.miss(.{battle.active(player)});
1898
+ }
1899
+
1900
+ const move = battle.foe(player).lastMove(false);
1901
+ if (move == .None or move == .Struggle) {
1902
+ return options.log.fail(.{ battle.active(player.foe()), .None });
1903
+ }
1904
+
1905
+ var slot: u4 = 0;
1906
+ for (side.active.moves, 0..) |m, i| {
1907
+ if (m.id == move) return options.log.fail(.{ battle.active(player.foe()), .None });
1908
+ if (m.id == .Mimic) slot = @intCast(i + 1);
1909
+ }
1910
+ assert(slot != 0 and slot == state.mslot);
1911
+
1912
+ side.active.move(slot).id = move;
1913
+ side.active.move(slot).pp = 5;
1914
+ try options.log.start(.{ battle.active(player), .Mimic, move });
1742
1915
  }
1743
1916
 
1744
1917
  pub fn mirrorMove(
@@ -1755,11 +1928,11 @@ pub const Effects = struct {
1755
1928
 
1756
1929
  const last = foe.lastMove(false);
1757
1930
 
1758
- if (last == .None) return options.log.fail(battle.active(player), .None);
1931
+ if (last == .None) return options.log.fail(.{ battle.active(player), .None });
1759
1932
  const knows = knows: for (side.active.moves) |m| {
1760
1933
  if (last == m.id) break :knows true;
1761
1934
  } else false;
1762
- if (knows) return options.log.fail(battle.active(player), .None);
1935
+ if (knows) return options.log.fail(.{ battle.active(player), .None });
1763
1936
 
1764
1937
  // if (wPlayerCharging) ???
1765
1938
  // wPlayerCharging = 1
@@ -1767,23 +1940,23 @@ pub const Effects = struct {
1767
1940
  // FIXME
1768
1941
  state.move = last;
1769
1942
  state.from = .MirrorMove;
1770
- return try generated.runMove(battle, player, state, options);
1943
+ return generated.runMove(battle, player, state, options);
1771
1944
  }
1772
1945
 
1773
1946
  pub fn mist(battle: anytype, player: Player, _: *State, options: anytype) !void {
1774
1947
  var side = battle.side(player);
1775
1948
 
1776
- if (side.active.volatiles.Mist) return options.log.fail(battle.active(player), .None);
1949
+ if (side.active.volatiles.Mist) return options.log.fail(.{ battle.active(player), .None });
1777
1950
  side.active.volatiles.Mist = true;
1778
1951
 
1779
- try options.log.start(battle.active(player), .Mist);
1952
+ try options.log.start(.{ battle.active(player), .Mist });
1780
1953
  }
1781
1954
 
1782
1955
  pub fn weatherHeal(battle: anytype, player: Player, _: *State, options: anytype) !void {
1783
1956
  var stored = battle.side(player).stored();
1784
1957
  const ident = battle.active(player);
1785
1958
 
1786
- if (stored.hp == stored.stats.hp) return options.log.fail(ident, .None);
1959
+ if (stored.hp == stored.stats.hp) return options.log.fail(.{ ident, .None });
1787
1960
 
1788
1961
  stored.hp = switch (battle.field.weather) {
1789
1962
  .Sun => stored.stats.hp,
@@ -1791,7 +1964,7 @@ pub const Effects = struct {
1791
1964
  else => @min(stored.stats.hp, stored.hp + (stored.stats.hp / 4)),
1792
1965
  };
1793
1966
 
1794
- try options.log.heal(ident, stored, Heal.None);
1967
+ try options.log.heal(.{ ident, stored, .None });
1795
1968
  }
1796
1969
 
1797
1970
  pub fn multiHit(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1804,17 +1977,17 @@ pub const Effects = struct {
1804
1977
  const foe_ident = battle.active(player.foe());
1805
1978
 
1806
1979
  if (foe_volatiles.Flying or foe_volatiles.Underground) {
1807
- if (!showdown) return options.log.fail(battle.active(player.foe()), .None);
1808
- try options.log.lastmiss();
1809
- return options.log.miss(battle.active(player));
1980
+ if (!showdown) return options.log.fail(.{ battle.active(player.foe()), .None });
1981
+ try options.log.lastmiss(.{});
1982
+ return options.log.miss(.{battle.active(player)});
1810
1983
  } else if (foe_volatiles.Substitute) {
1811
- return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
1984
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
1812
1985
  } else if (!Status.is(foe.stored().status, .SLP) or foe_volatiles.Nightmare) {
1813
- return options.log.fail(foe_ident, .None);
1986
+ return options.log.fail(.{ foe_ident, .None });
1814
1987
  }
1815
1988
 
1816
1989
  foe_volatiles.Nightmare = true;
1817
- try options.log.start(battle.active(player.foe()), .Nightmare);
1990
+ try options.log.start(.{ battle.active(player.foe()), .Nightmare });
1818
1991
  }
1819
1992
 
1820
1993
  pub const ohko = struct {
@@ -1838,9 +2011,9 @@ pub const Effects = struct {
1838
2011
  const foe_ident = battle.active(player.foe());
1839
2012
 
1840
2013
  state.damage = 0;
1841
- if (state.immune()) return options.log.immune(foe_ident, .None);
2014
+ if (state.immune()) return options.log.immune(.{ foe_ident, .None });
1842
2015
  const delta = side.stored().level -| foe.stored().level;
1843
- if (delta == 0) return options.log.immune(foe_ident, .OHKO);
2016
+ if (delta == 0) return options.log.immune(.{ foe_ident, .OHKO });
1844
2017
 
1845
2018
  state.accuracy = 2 * delta +| Gen12.percent(30);
1846
2019
  try checkHit(battle, player, state, options);
@@ -1856,21 +2029,39 @@ pub const Effects = struct {
1856
2029
  const ident = battle.active(player);
1857
2030
  const foe_ident = battle.active(player.foe());
1858
2031
 
1859
- if (state.miss) return options.log.fail(foe_ident, .None);
2032
+ if (state.miss) return options.log.fail(.{ foe_ident, .None });
1860
2033
  if (foe.active.volatiles.Substitute) {
1861
- return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
2034
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
1862
2035
  }
1863
2036
 
1864
2037
  const avg = @max((foe.stored().hp + side.stored().hp) / 2, 1);
1865
2038
  foe.stored().hp -= avg;
1866
2039
  side.stored().hp = avg;
1867
2040
 
1868
- try options.log.sethp(foe_ident, foe.stored(), .Silent);
1869
- try options.log.sethp(ident, side.stored(), .None);
2041
+ try options.log.sethp(.{ foe_ident, foe.stored(), .Silent });
2042
+ try options.log.sethp(.{ ident, side.stored(), .None });
1870
2043
  }
1871
2044
 
1872
2045
  pub fn paralyze(battle: anytype, player: Player, state: *State, options: anytype) !void {
1873
- _ = .{ battle, player, state, options }; // TODO
2046
+ var foe = battle.foe(player);
2047
+ const foe_ident = battle.active(player.foe());
2048
+
2049
+ if (Status.is(foe.stored().status, .PAR)) {
2050
+ return options.log.fail(.{ foe_ident, .Paralysis });
2051
+ }
2052
+ if (state.immune()) return options.log.immune(.{ foe_ident, .None });
2053
+ if (Status.any(foe.stored().status)) return options.log.fail(.{ foe_ident, .None });
2054
+ if (state.miss) {
2055
+ try options.log.lastmiss(.{});
2056
+ return options.log.miss(.{battle.active(player)});
2057
+ } else if (foe.active.volatiles.Substitute) {
2058
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
2059
+ }
2060
+
2061
+ foe.stored().status = Status.init(.PAR);
2062
+ foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2063
+
2064
+ try options.log.status(.{ foe_ident, foe.stored().status, .None });
1874
2065
  }
1875
2066
 
1876
2067
  pub fn paralyzeChance(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1885,14 +2076,15 @@ pub const Effects = struct {
1885
2076
 
1886
2077
  foe_stored.status = Status.init(.PAR);
1887
2078
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
1888
- try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
2079
+
2080
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
1889
2081
 
1890
2082
  // TODO: check for status berry
1891
2083
  }
1892
2084
 
1893
2085
  pub fn payDay(battle: anytype, player: Player, _: *State, options: anytype) !void {
1894
2086
  if (!showdown or !battle.foe(player).active.volatiles.Substitute) {
1895
- try options.log.fieldactivate(); // FIXME .PayDay
2087
+ try options.log.fieldactivate(.{}); // FIXME .PayDay
1896
2088
  }
1897
2089
  }
1898
2090
 
@@ -1901,17 +2093,17 @@ pub const Effects = struct {
1901
2093
  var foe = battle.foe(player);
1902
2094
 
1903
2095
  if (foe.active.volatiles.PerishSong) {
1904
- return options.log.fail(battle.active(player.foe()), .None);
2096
+ return options.log.fail(.{ battle.active(player.foe()), .None });
1905
2097
  } else if (!side.active.volatiles.PerishSong) {
1906
2098
  side.active.volatiles.PerishSong = true;
1907
2099
  side.active.volatiles.perish_song = 4;
1908
- try options.log.start(battle.active(player), .PerishSong3Silent);
2100
+ try options.log.start(.{ battle.active(player), .PerishSong3Silent });
1909
2101
  }
1910
2102
  foe.active.volatiles.PerishSong = true;
1911
2103
  foe.active.volatiles.perish_song = 4;
1912
- try options.log.start(battle.active(player.foe()), .PerishSong3Silent);
2104
+ try options.log.start(.{ battle.active(player.foe()), .PerishSong3Silent });
1913
2105
 
1914
- try options.log.fieldactivate(); // FIXME .PerishSong
2106
+ try options.log.fieldactivate(.{}); // FIXME .PerishSong
1915
2107
  }
1916
2108
 
1917
2109
  pub fn poison(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -1921,24 +2113,24 @@ pub const Effects = struct {
1921
2113
 
1922
2114
  const toxic = state.move == .Toxic;
1923
2115
  if (state.immune() or foe.active.types.includes(.Poison)) {
1924
- return options.log.immune(foe_ident, .None);
2116
+ return options.log.immune(.{ foe_ident, .None });
1925
2117
  } else if (Status.any(foe_stored.status)) {
1926
2118
  // Pokémon Showdown considers Toxic to be a status even in Generation II and so
1927
2119
  // will not include a fail reason for Toxic vs. Poison or vice-versa...
1928
- return options.log.fail(foe_ident, if (Status.is(foe_stored.status, .PSN))
2120
+ return options.log.fail(.{ foe_ident, if (Status.is(foe_stored.status, .PSN))
1929
2121
  if (!showdown)
1930
- .Poison
2122
+ Fail.Poison
1931
2123
  else if (toxic == (foe_stored.status == Status.TOX))
1932
- if (toxic) .Toxic else .Poison
2124
+ if (toxic) Fail.Toxic else Fail.Poison
1933
2125
  else
1934
- .None
2126
+ Fail.None
1935
2127
  else
1936
- .None);
2128
+ Fail.None });
1937
2129
  } else if (foe.active.volatiles.Substitute) {
1938
- return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
2130
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
1939
2131
  } else if (state.miss) {
1940
- try options.log.lastmiss();
1941
- try options.log.miss(battle.active(player));
2132
+ try options.log.lastmiss(.{});
2133
+ return options.log.miss(.{battle.active(player)});
1942
2134
  }
1943
2135
 
1944
2136
  foe_stored.status = Status.init(.PSN);
@@ -1948,7 +2140,7 @@ pub const Effects = struct {
1948
2140
  foe.active.volatiles.toxic = 0;
1949
2141
  }
1950
2142
 
1951
- try options.log.status(foe_ident, foe_stored.status, .None);
2143
+ try options.log.status(.{ foe_ident, foe_stored.status, .None });
1952
2144
 
1953
2145
  // TODO: check for status berry
1954
2146
  }
@@ -1964,7 +2156,8 @@ pub const Effects = struct {
1964
2156
  if (foe.conditions.Safeguard) return;
1965
2157
 
1966
2158
  foe_stored.status = Status.init(.PSN);
1967
- try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
2159
+
2160
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
1968
2161
 
1969
2162
  // TODO: check for status berry
1970
2163
  }
@@ -1979,10 +2172,7 @@ pub const Effects = struct {
1979
2172
 
1980
2173
  const failed = !state.first or volatiles.Substitute or
1981
2174
  !try Rolls.protect(battle, player, volatiles.protect, options);
1982
- if (failed) {
1983
- try options.log.fail(battle.active(player), .None);
1984
- return;
1985
- }
2175
+ if (failed) return options.log.fail(.{ battle.active(player), .None });
1986
2176
 
1987
2177
  volatiles.protect = @max(8, volatiles.protect + 1);
1988
2178
  if (state.move == .Endure) {
@@ -1990,21 +2180,20 @@ pub const Effects = struct {
1990
2180
  } else {
1991
2181
  volatiles.Protect = true;
1992
2182
  }
1993
- try options.log.singleturn(battle.active(player), state.move);
2183
+ try options.log.singleturn(.{ battle.active(player), state.move });
1994
2184
  }
1995
2185
 
1996
2186
  pub fn psychUp(battle: anytype, player: Player, _: *State, options: anytype) !void {
1997
- _ = .{ battle, player, options }; // TODO
1998
- // var side = battle.side(player);
1999
- // const foe = battle.foe(player);
2187
+ var side = battle.side(player);
2188
+ const foe = battle.foe(player);
2000
2189
 
2001
- // // Pokémon Showdown should fail when the enemy has no boosts/unboosts but doesn't
2002
- // if (!showdown and @as(u32, @bitCast(foe.active.boosts)) == 0) {
2003
- // return options.log.fail(battle.active(player), .None);
2004
- // }
2190
+ // Pokémon Showdown should fail when the enemy has no boosts/unboosts but doesn't
2191
+ if (!showdown and @as(u32, @bitCast(foe.active.boosts)) == 0) {
2192
+ return options.log.fail(.{ battle.active(player), .None });
2193
+ }
2005
2194
 
2006
- // side.active.boosts = foe.active.boosts;
2007
- // try options.log.copyboost(battle.active(player), battle.active(player.foe()));
2195
+ side.active.boosts = foe.active.boosts;
2196
+ try options.log.copyboost(.{ battle.active(player), battle.active(player.foe()) });
2008
2197
  }
2009
2198
 
2010
2199
  pub fn pursuit(battle: anytype, player: Player, state: *State, _: anytype) !void {
@@ -2015,15 +2204,14 @@ pub const Effects = struct {
2015
2204
  pub const rage = struct {
2016
2205
  pub fn start(battle: anytype, player: Player, _: *State, options: anytype) !void {
2017
2206
  battle.side(player).active.volatiles.Rage = true;
2018
- try options.log.singlemove(battle.active(player), Move.Rage);
2207
+ try options.log.singlemove(.{ battle.active(player), Move.Rage });
2019
2208
  }
2020
2209
 
2021
2210
  pub fn damage(battle: anytype, player: Player, state: *State, _: anytype) !void {
2022
- var side = battle.side(player);
2023
- var volatiles = &side.active.volatiles;
2211
+ const side = battle.side(player);
2024
2212
 
2025
- assert(volatiles.Rage);
2026
- state.damage *|= (volatiles.rage +| 1);
2213
+ assert(side.active.volatiles.Rage);
2214
+ state.damage *|= (side.active.volatiles.rage +| 1);
2027
2215
  }
2028
2216
  };
2029
2217
 
@@ -2031,16 +2219,37 @@ pub const Effects = struct {
2031
2219
  return weather(battle, .Rain, options);
2032
2220
  }
2033
2221
 
2034
- pub fn rapidSpin(battle: anytype, player: Player, state: *State, options: anytype) !void {
2035
- _ = .{ battle, player, state, options }; // TODO
2036
- }
2222
+ pub fn rapidSpin(battle: anytype, player: Player, _: *State, options: anytype) !void {
2223
+ var side = battle.side(player);
2037
2224
 
2038
- pub fn razorWind(battle: anytype, player: Player, state: *State, options: anytype) !void {
2039
- _ = .{ battle, player, state, options }; // TODO
2225
+ if (side.active.volatiles.LeechSeed) {
2226
+ try options.log.end(.{ battle.active(player), .LeechSeedFrom });
2227
+ side.active.volatiles.LeechSeed = false;
2228
+ }
2229
+
2230
+ if (side.conditions.Spikes) {
2231
+ try options.log.sideend(.{ player, protocol.Side.Spikes, battle.active(player) });
2232
+ side.conditions.Spikes = false;
2233
+ }
2234
+
2235
+ if (side.active.volatiles.bind.duration > 0) {
2236
+ const reason = @intFromEnum(End.Bind) + side.active.volatiles.bind.reason - 1;
2237
+ try options.log.end(.{ battle.active(player), @as(End, @enumFromInt(reason)) });
2238
+ side.active.volatiles.bind = .{};
2239
+ }
2040
2240
  }
2041
2241
 
2042
2242
  pub fn recoil(battle: anytype, player: Player, state: *State, options: anytype) !void {
2043
- _ = .{ battle, player, state, options }; // TODO
2243
+ var side = battle.side(player);
2244
+ var stored = side.stored();
2245
+ const ident = battle.active(player);
2246
+
2247
+ assert(state.damage > 0);
2248
+
2249
+ const damage: i16 = @intCast(@max(state.damage / 4, 1));
2250
+ stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
2251
+
2252
+ try options.log.damage(.{ ident, stored, .RecoilOf, battle.active(player.foe()) });
2044
2253
  }
2045
2254
 
2046
2255
  pub fn reversal(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -2068,25 +2277,72 @@ pub const Effects = struct {
2068
2277
  try calcDamage(battle, player, state, options);
2069
2278
  }
2070
2279
 
2071
- pub fn rollout(battle: anytype, player: Player, state: *State, options: anytype) !void {
2072
- _ = .{ battle, player, state, options }; // TODO
2280
+ pub const rollout = struct {
2281
+ pub fn check(battle: anytype, player: Player, _: *State, _: anytype) !void {
2282
+ var side = battle.side(player);
2283
+
2284
+ if (!side.active.volatiles.Rollout) {
2285
+ side.active.volatiles.attacks = 0;
2286
+ } else {
2287
+ // TODO SkipToBattleCommand(doturn_command)
2288
+ }
2289
+ }
2290
+
2291
+ pub fn power(battle: anytype, player: Player, state: *State, _: anytype) !void {
2292
+ var side = battle.side(player);
2293
+
2294
+ if (Status.is(side.stored().status, .SLP)) return;
2295
+
2296
+ if (side.active.volatiles.attacks > 0) {
2297
+ // TODO set wSomeoneIsRampaging
2298
+ }
2299
+
2300
+ if (state.miss) {
2301
+ side.active.volatiles.Rollout = false;
2302
+ return;
2303
+ }
2304
+
2305
+ assert(side.active.volatiles.attacks < 5);
2306
+ side.active.volatiles.attacks += 1;
2307
+ side.active.volatiles.Rollout = side.active.volatiles.attacks != 5;
2308
+
2309
+ if (side.active.volatiles.DefenseCurl) state.damage *|= 2;
2310
+ }
2311
+ };
2312
+
2313
+ pub fn sandstorm(battle: anytype, player: Player, _: *State, options: anytype) !void {
2314
+ if (battle.field.weather != .Sandstorm) return weather(battle, .Sandstorm, options);
2315
+ return options.log.fail(.{ battle.active(player), .None });
2073
2316
  }
2074
2317
 
2075
- pub fn sacredFire(battle: anytype, player: Player, state: *State, options: anytype) !void {
2076
- _ = .{ battle, player, state, options }; // TODO
2318
+ pub fn safeguard(battle: anytype, player: Player, _: *State, options: anytype) !void {
2319
+ return screens(battle, player, "Safeguard", "safeguard", options);
2077
2320
  }
2078
2321
 
2079
- pub fn safeguard(battle: anytype, player: Player, state: *State, options: anytype) !void {
2080
- _ = .{ battle, player, state, options }; // TODO
2322
+ pub fn reflect(battle: anytype, player: Player, _: *State, options: anytype) !void {
2323
+ return screens(battle, player, "Reflect", "reflect", options);
2081
2324
  }
2082
2325
 
2083
- pub fn sandstorm(battle: anytype, player: Player, _: *State, options: anytype) !void {
2084
- if (battle.field.weather != .Sandstorm) return weather(battle, .Sandstorm, options);
2085
- return options.log.fail(battle.active(player), .None);
2326
+ pub fn lightScreen(battle: anytype, player: Player, _: *State, options: anytype) !void {
2327
+ return screens(battle, player, "LightScreen", "light_screen", options);
2086
2328
  }
2087
2329
 
2088
- pub fn screens(battle: anytype, player: Player, state: *State, options: anytype) !void {
2089
- _ = .{ battle, player, state, options }; // TODO
2330
+ fn screens(
2331
+ battle: anytype,
2332
+ player: Player,
2333
+ comptime key: []const u8,
2334
+ comptime value: []const u8,
2335
+ options: anytype,
2336
+ ) !void {
2337
+ var side = battle.side(player);
2338
+
2339
+ if (@field(side.conditions, key)) {
2340
+ return options.log.fail(.{ battle.active(player), .None });
2341
+ }
2342
+
2343
+ @field(side.conditions, key) = true;
2344
+ @field(side.conditions, value) = 5;
2345
+ try options.log.sidestart(.{ player, @field(protocol.Side, key) });
2090
2346
  }
2091
2347
 
2092
2348
  pub fn sketch(battle: anytype, player: Player, _: *State, options: anytype) !void {
@@ -2097,63 +2353,51 @@ pub const Effects = struct {
2097
2353
 
2098
2354
  if (showdown) {
2099
2355
  // yes, actually "Splash". '|nothing' gets blindly mapped to '|-activate||move:Splash'
2100
- try options.log.activate(battle.active(player), .Splash);
2356
+ try options.log.activate(.{ battle.active(player), .Splash });
2101
2357
  } else {
2102
- try options.log.fail(battle.active(player), .None);
2358
+ try options.log.fail(.{ battle.active(player), .None });
2103
2359
  }
2104
2360
  }
2105
2361
 
2106
- pub fn skullBash(battle: anytype, player: Player, state: *State, options: anytype) !void {
2107
- _ = .{ battle, player, state, options }; // TODO
2108
- }
2109
-
2110
- pub fn skyAttack(battle: anytype, player: Player, state: *State, options: anytype) !void {
2111
- _ = .{ battle, player, state, options }; // TODO
2112
- }
2113
-
2114
2362
  pub fn sleep(battle: anytype, player: Player, state: *State, options: anytype) !void {
2115
2363
  var foe = battle.foe(player);
2116
2364
  var foe_stored = foe.stored();
2117
2365
  const foe_ident = battle.active(player.foe());
2118
2366
 
2119
2367
  if (Status.any(foe_stored.status)) {
2120
- return options.log.fail(
2368
+ return options.log.fail(.{
2121
2369
  foe_ident,
2122
- if (Status.is(foe_stored.status, .SLP)) .Sleep else .None,
2123
- );
2370
+ if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
2371
+ });
2124
2372
  }
2125
2373
 
2126
2374
  if (foe.active.volatiles.Substitute) {
2127
- return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
2375
+ return options.log.activate(.{ foe_ident, .SubstituteBlock, state.move });
2128
2376
  }
2129
2377
 
2130
2378
  // Sleep Clause Mod
2131
- if (showdown) {
2379
+ if (pkmn.options.mod) {
2132
2380
  for (foe.pokemon) |p| {
2133
2381
  if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
2134
2382
  }
2135
2383
  }
2136
2384
 
2137
2385
  foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2138
- try options.log.statusFrom(foe_ident, foe_stored.status, state.move);
2386
+ try options.log.status(.{ foe_ident, foe_stored.status, .From, state.move });
2139
2387
  }
2140
2388
 
2141
2389
  pub fn sleepTalk(battle: anytype, player: Player, state: *State, options: anytype) !void {
2142
2390
  const side = battle.side(player);
2143
2391
 
2144
2392
  if (!Status.is(side.stored().status, .SLP)) {
2145
- if (!showdown) try options.log.fail(battle.active(player), .None);
2146
- return;
2393
+ return if (!showdown) try options.log.fail(.{ battle.active(player), .None });
2147
2394
  }
2148
2395
 
2149
2396
  var n: u3 = 0;
2150
2397
  for (side.active.moves, 0..) |m, i| {
2151
2398
  if (state.mslot - 1 != i and Move.get(m.id).extra.sleep_talk) n += 1;
2152
2399
  }
2153
- if (n == 0) {
2154
- try options.log.fail(battle.active(player), .None);
2155
- return;
2156
- }
2400
+ if (n == 0) return options.log.fail(.{ battle.active(player), .None });
2157
2401
 
2158
2402
  _ = try Rolls.sleepTalk(battle, player, n, state.mslot, options); // TODO
2159
2403
  }
@@ -2162,28 +2406,75 @@ pub const Effects = struct {
2162
2406
  if (Status.is(battle.side(player).stored().status, .SLP)) return true;
2163
2407
  state.damage = 0;
2164
2408
  state.miss = true; // TODO is this necessary?
2165
- if (!showdown) try options.log.fail(battle.active(player), .None);
2409
+ if (!showdown) try options.log.fail(.{ battle.active(player), .None });
2166
2410
  return false;
2167
2411
  }
2168
2412
 
2169
- pub fn spikes(battle: anytype, player: Player, state: *State, options: anytype) !void {
2170
- _ = .{ battle, player, state, options }; // TODO
2413
+ pub fn spikes(battle: anytype, player: Player, _: *State, options: anytype) !void {
2414
+ var foe = battle.foe(player);
2415
+ if (foe.conditions.Spikes) return options.log.fail(.{ battle.active(player), .None });
2416
+
2417
+ foe.conditions.Spikes = true;
2418
+ try options.log.sidestart(.{ player.foe(), protocol.Side.Spikes });
2171
2419
  }
2172
2420
 
2173
2421
  pub fn spite(battle: anytype, player: Player, state: *State, options: anytype) !void {
2174
- _ = .{ battle, player, state, options }; // TODO
2422
+ var foe = battle.foe(player);
2423
+
2424
+ const last = foe.lastMove(false);
2425
+ if (state.miss or last == .None or last == .Struggle) {
2426
+ return options.log.fail(.{ battle.active(player.foe()), .None });
2427
+ }
2428
+
2429
+ var slot: u4 = 0;
2430
+ for (foe.active.moves, 0..) |m, i| {
2431
+ if (m.id == last) slot = @intCast(i + 1);
2432
+ }
2433
+ // If the move was called via Metronome it might not exist
2434
+ if (slot == 0 or foe.active.move(slot).pp == 0) {
2435
+ return options.log.fail(.{ battle.active(player.foe()), .None });
2436
+ }
2437
+
2438
+ // Pokémon Showdown always deducts 4 instead of randomizing
2439
+ const pp = @min(
2440
+ foe.active.move(slot).pp,
2441
+ if (showdown) 4 else try Rolls.spite(battle, player, options),
2442
+ );
2443
+ foe.active.move(slot).pp -= pp;
2444
+ if (!foe.active.volatiles.Transform) {
2445
+ var move_slot = foe.stored().move(slot);
2446
+ assert(move_slot.pp > 0);
2447
+
2448
+ move_slot.pp = @as(u6, @intCast(move_slot.pp)) -% 1;
2449
+ assert(foe.active.move(slot).pp == foe.stored().move(slot).pp);
2450
+ }
2451
+
2452
+ try options.log.activate(.{ battle.active(player.foe()), .Spite, last, pp });
2175
2453
  }
2176
2454
 
2177
2455
  pub fn splash(battle: anytype, player: Player, _: *State, options: anytype) !void {
2178
- try options.log.activate(battle.active(player), .Splash);
2456
+ try options.log.activate(.{ battle.active(player), .Splash });
2179
2457
  }
2180
2458
 
2181
- pub fn stomp(battle: anytype, player: Player, state: *State, options: anytype) !void {
2182
- _ = .{ battle, player, state, options }; // TODO
2183
- }
2459
+ pub fn substitute(battle: anytype, player: Player, _: *State, options: anytype) !void {
2460
+ var side = battle.side(player);
2461
+ if (side.active.volatiles.Substitute) {
2462
+ return options.log.fail(.{ battle.active(player), .Substitute });
2463
+ }
2184
2464
 
2185
- pub fn substitute(battle: anytype, player: Player, state: *State, options: anytype) !void {
2186
- _ = .{ battle, player, state, options }; // TODO
2465
+ assert(side.stored().stats.hp <= 1023);
2466
+ const hp: u8 = @intCast(side.stored().stats.hp / 4);
2467
+
2468
+ if (hp == 0 or side.stored().hp <= hp) {
2469
+ return options.log.fail(.{ battle.active(player), .Weak });
2470
+ }
2471
+
2472
+ side.stored().hp -= hp;
2473
+ side.active.volatiles.substitute = hp;
2474
+ side.active.volatiles.Substitute = true;
2475
+ side.active.volatiles.bind = .{};
2476
+ try options.log.start(.{ battle.active(player), .Substitute });
2477
+ try options.log.damage(.{ battle.active(player), side.stored(), .None });
2187
2478
  }
2188
2479
 
2189
2480
  pub fn sunnyDay(battle: anytype, _: Player, _: *State, options: anytype) !void {
@@ -2193,18 +2484,14 @@ pub const Effects = struct {
2193
2484
  fn weather(battle: anytype, w: Weather, options: anytype) !void {
2194
2485
  battle.field.weather = w;
2195
2486
  battle.field.weather_duration = 5;
2196
- try options.log.weather(w, .None);
2197
- }
2198
-
2199
- pub fn swagger(battle: anytype, player: Player, state: *State, options: anytype) !void {
2200
- _ = .{ battle, player, state, options }; // TODO
2487
+ try options.log.weather(.{ w, protocol.Weather.None });
2201
2488
  }
2202
2489
 
2203
2490
  pub fn teleport(battle: anytype, player: Player, _: *State, options: anytype) !void {
2204
2491
  if (showdown) return;
2205
2492
 
2206
- try options.log.fail(battle.active(player), .None);
2207
- try options.log.laststill();
2493
+ try options.log.fail(.{ battle.active(player), .None });
2494
+ try options.log.laststill(.{});
2208
2495
  }
2209
2496
 
2210
2497
  pub fn thief(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -2219,15 +2506,21 @@ pub const Effects = struct {
2219
2506
  side.stored().item = foe.stored().item;
2220
2507
  foe.stored().item = .None;
2221
2508
 
2222
- try options.log.item(
2509
+ try options.log.item(.{
2223
2510
  battle.active(player),
2224
2511
  side.stored().item,
2225
2512
  battle.active(player.foe()),
2226
- );
2513
+ });
2227
2514
  }
2228
2515
 
2229
- pub fn thrashing(battle: anytype, player: Player, state: *State, options: anytype) !void {
2230
- _ = .{ battle, player, state, options }; // TODO
2516
+ pub fn thrashing(battle: anytype, player: Player, _: *State, options: anytype) !void {
2517
+ var side = battle.side(player);
2518
+ if (Status.is(side.stored().status, .SLP)) return;
2519
+
2520
+ assert(!side.active.volatiles.Thrashing);
2521
+ side.active.volatiles.Thrashing = true;
2522
+ side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
2523
+ // TODO wSomeoneIsRampaging = true;
2231
2524
  }
2232
2525
 
2233
2526
  pub fn transform(battle: anytype, player: Player, _: *State, options: anytype) !void {
@@ -2239,11 +2532,11 @@ pub const Effects = struct {
2239
2532
  side.active.volatiles.dirty = false;
2240
2533
 
2241
2534
  if (foe.active.volatiles.Transform) {
2242
- return options.log.fail(battle.active(player), .None);
2535
+ return options.log.fail(.{ battle.active(player), .None });
2243
2536
  } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2244
- if (!showdown) return options.log.fail(battle.active(player), .None);
2245
- try options.log.lastmiss();
2246
- return options.log.miss(battle.active(player));
2537
+ if (!showdown) return options.log.fail(.{ battle.active(player), .None });
2538
+ try options.log.lastmiss(.{});
2539
+ return options.log.miss(.{battle.active(player)});
2247
2540
  }
2248
2541
 
2249
2542
  side.active.volatiles.Transform = true;
@@ -2265,7 +2558,7 @@ pub const Effects = struct {
2265
2558
  side.active.moves[i].pp = if (m.id != .None) if (m.id == .Sketch) 1 else 5 else 0;
2266
2559
  }
2267
2560
 
2268
- try options.log.transform(battle.active(player), foe_ident);
2561
+ try options.log.transform(.{ battle.active(player), foe_ident });
2269
2562
  }
2270
2563
 
2271
2564
  pub fn triAttack(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -2284,31 +2577,25 @@ pub const Effects = struct {
2284
2577
  _ = .{ battle, player, state, options }; // TODO
2285
2578
  }
2286
2579
 
2287
- const ALL_STAT_UP_STATS = [_][]const u8{ "atk", "def", "spe", "spa", "spd" };
2288
- const ALL_STAT_UP_REASONS =
2580
+ const STAT_UP_REASONS =
2289
2581
  [_]Boost{ .Attack, .Defense, .Speed, .SpecialAttack, .SpecialDefense };
2290
2582
 
2291
2583
  pub fn allStatUpChance(battle: anytype, player: Player, state: *State, options: anytype) !void {
2292
2584
  var side = battle.side(player);
2293
2585
 
2294
- var stats = &side.active.stats;
2295
- var boosts = &side.active.boosts;
2586
+ const stats = &side.active.stats;
2587
+ const boosts = &side.active.boosts;
2296
2588
 
2297
2589
  if (!state.proc) return;
2298
- inline for (ALL_STAT_UP_STATS, ALL_STAT_UP_REASONS) |s, r| {
2590
+ inline for (STATS, STAT_UP_REASONS) |s, r| {
2299
2591
  assert(@field(boosts, s) >= -6 and @field(boosts, s) <= 6);
2300
2592
  if (@field(boosts, s) == 6 or @field(stats, s) == MAX_STAT_VALUE) {
2301
- try options.log.boost(battle.active(player), r, 0);
2593
+ try options.log.boost(.{ battle.active(player), r, 0 });
2302
2594
  } else {
2303
- @field(boosts, s) = @intCast(@min(6, @as(i8, @field(boosts, s)) + 1));
2304
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, @field(boosts, s)) + 6))];
2305
- const stat = @field(unmodifiedStats(battle, side), s);
2306
- @field(stats, s) = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2307
- try options.log.boost(battle.active(player), r, 1);
2595
+ try boostStat(battle, side, stats, boosts, s, 1);
2596
+ try options.log.boost(.{ battle.active(player), r, 1 });
2308
2597
  }
2309
2598
  }
2310
-
2311
- statusModify(side.stored().status, stats);
2312
2599
  }
2313
2600
 
2314
2601
  pub fn boost(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -2316,72 +2603,65 @@ pub const Effects = struct {
2316
2603
  var side = battle.side(player);
2317
2604
  const ident = battle.active(player);
2318
2605
 
2319
- if (state.miss) return log.fail(ident, .None);
2606
+ if (state.miss) return log.fail(.{ ident, .None });
2320
2607
 
2321
- var stats = &side.active.stats;
2608
+ const stats = &side.active.stats;
2322
2609
  var boosts = &side.active.boosts;
2323
2610
 
2324
2611
  const move = Move.get(state.move);
2325
2612
  switch (move.effect) {
2326
- .AttackUp1, .AttackUp2, .Rage => {
2613
+ .AttackUp1, .AttackUp2, .Rage, .Swagger => {
2327
2614
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2328
- if (boosts.atk == 6 or stats.atk == MAX_STAT_VALUE) return log.fail(ident, .None);
2329
- const n: u2 = if (move.effect == .AttackUp2) 2 else 1;
2330
- boosts.atk = @as(i4, @intCast(@min(6, @as(i8, boosts.atk) + n)));
2615
+ if (boosts.atk == 6 or stats.atk == MAX_STAT_VALUE) {
2616
+ return log.fail(.{ ident, .None });
2617
+ }
2618
+ const n: u2 = if (move.effect == .AttackUp2 or move.effect == .Swagger) 2 else 1;
2619
+ try boostStat(battle, side, stats, boosts, "atk", n);
2331
2620
  const reason = if (showdown and move.effect == .Rage) Boost.Rage else Boost.Attack;
2332
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2333
- const stat = unmodifiedStats(battle, side).atk;
2334
- stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2335
- try log.boost(ident, reason, n);
2621
+ try log.boost(.{ ident, reason, n });
2336
2622
  },
2337
- .DefenseUp1, .DefenseUp2 => {
2623
+ .DefenseUp1, .DefenseUp2, .SkullBash => {
2338
2624
  assert(boosts.def >= -6 and boosts.def <= 6);
2339
- if (boosts.def == 6 or stats.def == MAX_STAT_VALUE) return log.fail(ident, .None);
2625
+ if (boosts.def == 6 or stats.def == MAX_STAT_VALUE) {
2626
+ return log.fail(.{ ident, .None });
2627
+ }
2340
2628
  const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
2341
- boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
2342
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2343
- const stat = unmodifiedStats(battle, side).def;
2344
- stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2345
- try log.boost(ident, .Defense, n);
2629
+ try boostStat(battle, side, stats, boosts, "def", n);
2630
+ try log.boost(.{ ident, .Defense, n });
2346
2631
  },
2347
2632
  .SpeedUp2 => {
2348
2633
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2349
- if (boosts.spe == 6 or stats.spe == MAX_STAT_VALUE) return log.fail(ident, .None);
2350
- boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
2351
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2352
- const stat = unmodifiedStats(battle, side).spe;
2353
- stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2354
- try log.boost(ident, .Speed, 2);
2634
+ if (boosts.spe == 6 or stats.spe == MAX_STAT_VALUE) {
2635
+ return log.fail(.{ ident, .None });
2636
+ }
2637
+ try boostStat(battle, side, stats, boosts, "spe", 2);
2638
+ try log.boost(.{ ident, .Speed, 2 });
2355
2639
  },
2356
2640
  .SpAtkUp1 => {
2357
2641
  assert(boosts.spa >= -6 and boosts.spa <= 6);
2358
- if (boosts.spa == 6 or stats.spa == MAX_STAT_VALUE) return log.fail(ident, .None);
2359
- boosts.spa = @intCast(@min(6, @as(i8, boosts.spa) + 1));
2360
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.spa) + 6))];
2361
- const stat = unmodifiedStats(battle, side).spa;
2362
- stats.spa = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2363
- try log.boost(ident, .SpecialAttack, 1);
2642
+ if (boosts.spa == 6 or stats.spa == MAX_STAT_VALUE) {
2643
+ return log.fail(.{ ident, .None });
2644
+ }
2645
+ try boostStat(battle, side, stats, boosts, "spa", 1);
2646
+ try log.boost(.{ ident, .SpecialAttack, 1 });
2364
2647
  },
2365
2648
  .SpDefUp2 => {
2366
2649
  assert(boosts.spd >= -6 and boosts.spd <= 6);
2367
- if (boosts.spd == 6 or stats.spd == MAX_STAT_VALUE) return log.fail(ident, .None);
2368
- boosts.spd = @intCast(@min(6, @as(i8, boosts.spd) + 2));
2369
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.spd) + 6))];
2370
- const stat = unmodifiedStats(battle, side).spd;
2371
- stats.spd = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2372
- try log.boost(ident, .SpecialDefense, 2);
2650
+ if (boosts.spd == 6 or stats.spd == MAX_STAT_VALUE) {
2651
+ return log.fail(.{ ident, .None });
2652
+ }
2653
+ try boostStat(battle, side, stats, boosts, "spd", 2);
2654
+ try log.boost(.{ ident, .SpecialDefense, 2 });
2373
2655
  },
2374
2656
  .EvasionUp1 => {
2375
2657
  assert(boosts.evasion >= -6 and boosts.evasion <= 6);
2376
- if (boosts.evasion == 6) return log.fail(ident, .None);
2658
+ if (boosts.evasion == 6) return log.fail(.{ ident, .None });
2377
2659
  boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
2378
- try log.boost(ident, .Evasion, 1);
2660
+ try log.boost(.{ ident, .Evasion, 1 });
2379
2661
  if (state.move == .Minimize) side.active.volatiles.minimized = true;
2380
2662
  },
2381
2663
  else => unreachable,
2382
2664
  }
2383
-
2384
- statusModify(side.stored().status, stats);
2385
2665
  }
2386
2666
 
2387
2667
  pub fn unboost(battle: anytype, player: Player, state: *State, options: anytype) !void {
@@ -2390,102 +2670,86 @@ pub const Effects = struct {
2390
2670
  const foe_ident = battle.active(player.foe());
2391
2671
 
2392
2672
  if (foe.active.volatiles.Mist) {
2393
- try log.activate(foe_ident, .Mist);
2394
- return log.fail(foe_ident, .None);
2673
+ try log.activate(.{ foe_ident, .Mist });
2674
+ return log.fail(.{ foe_ident, .None });
2395
2675
  }
2396
2676
 
2397
2677
  const fail = foe.active.volatiles.Substitute or state.miss or !state.proc;
2398
2678
 
2399
- var stats = &foe.active.stats;
2679
+ const stats = &foe.active.stats;
2400
2680
  var boosts = &foe.active.boosts;
2401
2681
 
2402
2682
  const move = Move.get(state.move);
2683
+ const none: Fail = .None;
2403
2684
  switch (move.effect) {
2404
2685
  .AttackDown1, .AttackDown2, .AttackDownChance => {
2405
2686
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2406
2687
  if (fail or boosts.atk == -6) {
2407
- return if (move.effect != .AttackDownChance) try log.fail(foe_ident, .None);
2688
+ return if (move.effect != .AttackDownChance) try log.fail(.{ foe_ident, none });
2408
2689
  } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2409
- try options.log.lastmiss();
2410
- try options.log.miss(battle.active(player));
2411
- return;
2690
+ try options.log.lastmiss(.{});
2691
+ return options.log.miss(.{battle.active(player)});
2412
2692
  } else if (stats.atk == 1) {
2413
- return if (move.effect != .AttackDownChance) try log.fail(foe_ident, .None);
2693
+ return if (move.effect != .AttackDownChance) try log.fail(.{ foe_ident, none });
2414
2694
  }
2415
2695
  const n: u2 = if (move.effect == .AttackDown2) 2 else 1;
2416
- boosts.atk = @intCast(@max(-6, @as(i8, boosts.atk) - n));
2417
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2418
- const stat = unmodifiedStats(battle, foe).atk;
2419
- stats.atk = @max(1, stat * mod[0] / mod[1]);
2420
- try log.boost(foe_ident, .Attack, -@as(i8, n));
2696
+ try unboostStat(battle, foe, stats, boosts, "atk", n);
2697
+ try log.boost(.{ foe_ident, .Attack, -@as(i8, n) });
2421
2698
  },
2422
2699
  .DefenseDown1, .DefenseDown2, .DefenseDownChance => {
2423
2700
  assert(boosts.def >= -6 and boosts.def <= 6);
2424
2701
  if (fail or boosts.def == -6) {
2425
- return if (move.effect != .DefenseDownChance) try log.fail(foe_ident, .None);
2426
- } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2427
- try options.log.lastmiss();
2428
- try options.log.miss(battle.active(player));
2702
+ if (move.effect != .DefenseDownChance) try log.fail(.{ foe_ident, none });
2429
2703
  return;
2704
+ } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2705
+ try options.log.lastmiss(.{});
2706
+ return options.log.miss(.{battle.active(player)});
2430
2707
  } else if (stats.def == 1) {
2431
- return if (move.effect != .DefenseDownChance) try log.fail(foe_ident, .None);
2708
+ if (move.effect != .DefenseDownChance) try log.fail(.{ foe_ident, none });
2709
+ return;
2432
2710
  }
2433
2711
  const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
2434
- boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
2435
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2436
- const stat = unmodifiedStats(battle, foe).def;
2437
- stats.def = @max(1, stat * mod[0] / mod[1]);
2438
- try log.boost(foe_ident, .Defense, -@as(i8, n));
2712
+ try unboostStat(battle, foe, stats, boosts, "def", n);
2713
+ try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
2439
2714
  },
2440
2715
  .SpeedDown1, .SpeedDown2, .SpeedDownChance => {
2441
2716
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2442
2717
  if (fail or boosts.spe == -6) {
2443
- return if (move.effect != .SpeedDownChance) try log.fail(foe_ident, .None);
2718
+ return if (move.effect != .SpeedDownChance) try log.fail(.{ foe_ident, none });
2444
2719
  } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2445
- try options.log.lastmiss();
2446
- try options.log.miss(battle.active(player));
2447
- return;
2720
+ try options.log.lastmiss(.{});
2721
+ return options.log.miss(.{battle.active(player)});
2448
2722
  } else if (stats.spe == 1) {
2449
- return if (move.effect != .SpeedDownChance) try log.fail(foe_ident, .None);
2723
+ return if (move.effect != .SpeedDownChance) try log.fail(.{ foe_ident, none });
2450
2724
  }
2451
2725
  const n: u2 = if (move.effect == .SpeedDown2) 2 else 1;
2452
- boosts.spe = @intCast(@max(-6, @as(i8, boosts.spe) - 1));
2453
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2454
- const stat = unmodifiedStats(battle, foe).spe;
2455
- stats.spe = @max(1, stat * mod[0] / mod[1]);
2456
- try log.boost(foe_ident, .Speed, -@as(i8, n));
2457
- assert(boosts.spe >= -6);
2726
+ try unboostStat(battle, foe, stats, boosts, "spe", n);
2727
+ try log.boost(.{ foe_ident, .Speed, -@as(i8, n) });
2458
2728
  },
2459
2729
  .SpDefDownChance => {
2460
2730
  assert(boosts.spd >= -6 and boosts.spd <= 6);
2461
2731
  if (fail or boosts.spd == -6) return;
2462
2732
  if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2463
- try options.log.lastmiss();
2464
- try options.log.miss(battle.active(player));
2465
- return;
2733
+ try options.log.lastmiss(.{});
2734
+ return options.log.miss(.{battle.active(player)});
2466
2735
  } else if (stats.spd == 1) return;
2467
- boosts.spd = @intCast(@max(-6, @as(i8, boosts.spd) - 1));
2468
- var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.spd) + 6))];
2469
- const stat = unmodifiedStats(battle, foe).spd;
2470
- stats.spd = @max(1, stat * mod[0] / mod[1]);
2471
- try log.boost(foe_ident, .SpecialDefense, -1);
2736
+ try unboostStat(battle, foe, stats, boosts, "spd", 1);
2737
+ try log.boost(.{ foe_ident, .SpecialDefense, -1 });
2472
2738
  },
2473
2739
  .AccuracyDown1, .AccuracyDownChance => {
2474
2740
  assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
2475
2741
  if (fail or boosts.accuracy == -6) {
2476
- return if (move.effect != .AccuracyDownChance) try log.fail(foe_ident, .None);
2477
- } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2478
- try options.log.lastmiss();
2479
- try options.log.miss(battle.active(player));
2742
+ if (move.effect != .AccuracyDownChance) try log.fail(.{ foe_ident, none });
2480
2743
  return;
2744
+ } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2745
+ try options.log.lastmiss(.{});
2746
+ return options.log.miss(.{battle.active(player)});
2481
2747
  }
2482
2748
  boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
2483
- try log.boost(foe_ident, .Accuracy, -1);
2749
+ try log.boost(.{ foe_ident, .Accuracy, -1 });
2484
2750
  },
2485
2751
  else => unreachable,
2486
2752
  }
2487
-
2488
- statusModify(foe.stored().status, stats);
2489
2753
  }
2490
2754
  };
2491
2755
 
@@ -2495,6 +2759,15 @@ fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
2495
2759
  return &battle.side(id.player).pokemon[id.id - 1].stats;
2496
2760
  }
2497
2761
 
2762
+ fn recomputeStats(battle: anytype, side: *Side) void {
2763
+ const stats = unmodifiedStats(battle, side);
2764
+ inline for (STATS) |s| {
2765
+ const mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, @field(side.active.boosts, s)) + 6))];
2766
+ const val = @field(stats, s) * mod[0] / mod[1];
2767
+ @field(side.active.stats, s) = @max(1, @min(MAX_STAT_VALUE, val));
2768
+ }
2769
+ }
2770
+
2498
2771
  fn statusModify(status: u8, stats: *Stats(u16)) void {
2499
2772
  if (Status.is(status, .PAR)) {
2500
2773
  stats.spe = @max(stats.spe / 4, 1);
@@ -2503,6 +2776,52 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
2503
2776
  }
2504
2777
  }
2505
2778
 
2779
+ fn boostStat(
2780
+ battle: anytype,
2781
+ side: *Side,
2782
+ stats: *Stats(u16),
2783
+ boosts: *Boosts,
2784
+ comptime s: []const u8,
2785
+ n: u2,
2786
+ ) !void {
2787
+ @field(boosts, s) = @intCast(@min(6, @as(i8, @field(boosts, s)) + n));
2788
+ const mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, @field(boosts, s)) + 6))];
2789
+ const stat = @field(unmodifiedStats(battle, side), s);
2790
+ @field(stats, s) = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2791
+ if (comptime std.mem.eql(u8, s, "atk")) {
2792
+ if (Status.is(side.stored().status, .BRN)) {
2793
+ @field(stats, s) = @max(@field(stats, s) / 2, 1);
2794
+ }
2795
+ } else if (comptime std.mem.eql(u8, s, "spe")) {
2796
+ if (Status.is(side.stored().status, .PAR)) {
2797
+ @field(stats, s) = @max(@field(stats, s) / 4, 1);
2798
+ }
2799
+ }
2800
+ }
2801
+
2802
+ fn unboostStat(
2803
+ battle: anytype,
2804
+ side: *Side,
2805
+ stats: *Stats(u16),
2806
+ boosts: *Boosts,
2807
+ comptime s: []const u8,
2808
+ n: u2,
2809
+ ) !void {
2810
+ @field(boosts, s) = @intCast(@max(-6, @as(i8, @field(boosts, s)) - n));
2811
+ const mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, @field(boosts, s)) + 6))];
2812
+ const stat = @field(unmodifiedStats(battle, side), s);
2813
+ @field(stats, s) = @max(1, stat * mod[0] / mod[1]);
2814
+ if (comptime std.mem.eql(u8, s, "atk")) {
2815
+ if (Status.is(side.stored().status, .BRN)) {
2816
+ @field(stats, s) = @max(@field(stats, s) / 2, 1);
2817
+ }
2818
+ } else if (comptime std.mem.eql(u8, s, "spe")) {
2819
+ if (Status.is(side.stored().status, .PAR)) {
2820
+ @field(stats, s) = @max(@field(stats, s) / 4, 1);
2821
+ }
2822
+ }
2823
+ }
2824
+
2506
2825
  fn isForced(active: anytype) bool {
2507
2826
  return active.volatiles.Recharging or active.volatiles.Rage or
2508
2827
  active.volatiles.Thrashing or active.volatiles.Charging or
@@ -2517,7 +2836,7 @@ fn convertible(active: ActivePokemon, m: Move) bool {
2517
2836
 
2518
2837
  pub const Rolls = struct {
2519
2838
  fn speedTie(battle: anytype, options: anytype) !bool {
2520
- const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
2839
+ const p1 = if (options.calc.overridden(.P1, 0, .speed_tie)) |player|
2521
2840
  player == .P1
2522
2841
  else if (showdown)
2523
2842
  battle.rng.range(u8, 0, 2) == 0
@@ -2529,7 +2848,7 @@ pub const Rolls = struct {
2529
2848
  }
2530
2849
 
2531
2850
  fn quickClaw(battle: anytype, player: Player, options: anytype) !bool {
2532
- const qkc = if (options.calc.overridden(player, .quick_claw)) |val|
2851
+ const qkc = if (options.calc.overridden(player, 0, .quick_claw)) |val|
2533
2852
  val == .true
2534
2853
  else if (showdown)
2535
2854
  battle.rng.chance(u8, 60, 256)
@@ -2541,7 +2860,7 @@ pub const Rolls = struct {
2541
2860
  }
2542
2861
 
2543
2862
  fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) !bool {
2544
- const ok = if (options.calc.overridden(player, .hit)) |val|
2863
+ const ok = if (options.calc.overridden(player, 0, .hit)) |val|
2545
2864
  val == .true
2546
2865
  else if (showdown)
2547
2866
  battle.rng.chance(u8, accuracy, 256)
@@ -2552,20 +2871,22 @@ pub const Rolls = struct {
2552
2871
  return ok;
2553
2872
  }
2554
2873
 
2555
- fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2556
- const crit = if (options.calc.overridden(player, .critical_hit)) |val|
2874
+ fn criticalHit(battle: anytype, player: Player, i: u3, rate: u8, options: anytype) !bool {
2875
+ _ = .{ player, options };
2876
+ const crit = if (options.calc.overridden(player, i, .critical_hits)) |val|
2557
2877
  val == .true
2558
2878
  else if (showdown)
2559
2879
  battle.rng.chance(u8, rate, 256)
2560
2880
  else
2561
2881
  battle.rng.next() < rate;
2562
2882
 
2563
- try options.chance.criticalHit(player, crit, rate);
2883
+ try options.chance.criticalHit(player, i, crit, rate);
2564
2884
  return crit;
2565
2885
  }
2566
2886
 
2567
- fn damage(battle: anytype, player: Player, options: anytype) !u8 {
2568
- const roll = if (options.calc.overridden(player, .damage)) |val|
2887
+ fn damage(battle: anytype, player: Player, i: u3, options: anytype) !u8 {
2888
+ _ = .{ player, options };
2889
+ const roll = if (options.calc.overridden(player, i, .damages)) |val|
2569
2890
  val
2570
2891
  else roll: {
2571
2892
  if (showdown) break :roll battle.rng.range(u8, 217, 256);
@@ -2576,12 +2897,12 @@ pub const Rolls = struct {
2576
2897
  };
2577
2898
 
2578
2899
  assert(roll >= 217 and roll <= 255);
2579
- try options.chance.damage(player, roll);
2900
+ try options.chance.damage(player, i, roll);
2580
2901
  return roll;
2581
2902
  }
2582
2903
 
2583
2904
  fn confused(battle: anytype, player: Player, options: anytype) !bool {
2584
- const cfz = if (options.calc.overridden(player, .confused)) |val|
2905
+ const cfz = if (options.calc.overridden(player, 0, .confused)) |val|
2585
2906
  val == .true
2586
2907
  else if (showdown)
2587
2908
  !battle.rng.chance(u8, 128, 256)
@@ -2593,7 +2914,7 @@ pub const Rolls = struct {
2593
2914
  }
2594
2915
 
2595
2916
  fn attract(battle: anytype, player: Player, options: anytype) !bool {
2596
- const cant = if (options.calc.overridden(player, .attract)) |val|
2917
+ const cant = if (options.calc.overridden(player, 0, .attract)) |val|
2597
2918
  val == .true
2598
2919
  else if (showdown)
2599
2920
  battle.rng.chance(u8, 1, 2)
@@ -2605,7 +2926,7 @@ pub const Rolls = struct {
2605
2926
  }
2606
2927
 
2607
2928
  fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2608
- const par = if (options.calc.overridden(player, .paralyzed)) |val|
2929
+ const par = if (options.calc.overridden(player, 0, .paralyzed)) |val|
2609
2930
  val == .true
2610
2931
  else if (showdown)
2611
2932
  battle.rng.chance(u8, 63, 256)
@@ -2617,7 +2938,7 @@ pub const Rolls = struct {
2617
2938
  }
2618
2939
 
2619
2940
  fn defrost(battle: anytype, player: Player, options: anytype) !bool {
2620
- const thaw = if (options.calc.overridden(player, .defrost)) |val|
2941
+ const thaw = if (options.calc.overridden(player, 0, .defrost)) |val|
2621
2942
  val == .true
2622
2943
  else if (showdown)
2623
2944
  battle.rng.chance(u8, 25, 256)
@@ -2630,7 +2951,7 @@ pub const Rolls = struct {
2630
2951
 
2631
2952
  fn secondaryChance(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2632
2953
  // GLITCH: 100% secondary chance will still fail 1/256 of the time
2633
- const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2954
+ const proc = if (options.calc.overridden(player, 0, .secondary_chance)) |val|
2634
2955
  val == .true
2635
2956
  else if (showdown)
2636
2957
  battle.rng.chance(u8, rate, 256)
@@ -2642,7 +2963,7 @@ pub const Rolls = struct {
2642
2963
  }
2643
2964
 
2644
2965
  fn item(battle: anytype, player: Player, options: anytype) !bool {
2645
- const proc = if (options.calc.overridden(player, .item)) |val|
2966
+ const proc = if (options.calc.overridden(player, 0, .item)) |val|
2646
2967
  val == .true
2647
2968
  else if (showdown)
2648
2969
  battle.rng.chance(u8, 30, 256)
@@ -2654,7 +2975,7 @@ pub const Rolls = struct {
2654
2975
  }
2655
2976
 
2656
2977
  fn triAttack(battle: anytype, player: Player, options: anytype) !TriAttack {
2657
- const status: TriAttack = if (options.calc.overridden(player, .tri_attack)) |val|
2978
+ const status: TriAttack = if (options.calc.overridden(player, 0, .tri_attack)) |val|
2658
2979
  @enumFromInt(@intFromEnum(val) - 1)
2659
2980
  else if (showdown)
2660
2981
  @enumFromInt(battle.rng.range(u2, 0, 2))
@@ -2670,7 +2991,7 @@ pub const Rolls = struct {
2670
2991
  }
2671
2992
 
2672
2993
  fn present(battle: anytype, player: Player, options: anytype) !u8 {
2673
- const power = if (options.calc.overridden(player, .present)) |val|
2994
+ const power = if (options.calc.overridden(player, 0, .present)) |val|
2674
2995
  (val - 1) * 40
2675
2996
  else if (showdown) power: {
2676
2997
  const r = battle.rng.range(u8, 0, 10);
@@ -2692,7 +3013,7 @@ pub const Rolls = struct {
2692
3013
  }
2693
3014
 
2694
3015
  fn magnitude(battle: anytype, player: Player, options: anytype) !u8 {
2695
- const num = if (options.calc.overridden(player, .magnitude)) |val|
3016
+ const num: u8 = if (options.calc.overridden(player, 0, .magnitude)) |val|
2696
3017
  val + 3
2697
3018
  else if (showdown) num: {
2698
3019
  const r = battle.rng.range(u8, 0, 100);
@@ -2720,7 +3041,7 @@ pub const Rolls = struct {
2720
3041
  }
2721
3042
 
2722
3043
  fn tripleKick(battle: anytype, player: Player, options: anytype) !u2 {
2723
- const hits = if (options.calc.overridden(player, .triple_kick)) |val|
3044
+ const hits = if (options.calc.overridden(player, 0, .triple_kick)) |val|
2724
3045
  val
2725
3046
  else if (showdown)
2726
3047
  battle.rng.range(u2, 1, 4)
@@ -2736,13 +3057,12 @@ pub const Rolls = struct {
2736
3057
  return hits;
2737
3058
  }
2738
3059
 
2739
- fn spite(battle: anytype, player: Player, options: anytype) !u8 {
2740
- const pp = if (options.calc.overridden(player, .spite)) |val|
3060
+ fn spite(battle: anytype, player: Player, options: anytype) !u3 {
3061
+ assert(!showdown);
3062
+ const pp: u3 = if (options.calc.overridden(player, 0, .spite)) |val|
2741
3063
  val + 1
2742
- else if (showdown)
2743
- battle.rng.range(u8, 2, 6)
2744
3064
  else
2745
- (battle.rng.next() & 3) + 2;
3065
+ @intCast((battle.rng.next() & 3) + 2);
2746
3066
 
2747
3067
  assert(pp >= 2 and pp <= 5);
2748
3068
  try options.chance.spite(player, pp);
@@ -2750,13 +3070,13 @@ pub const Rolls = struct {
2750
3070
  }
2751
3071
 
2752
3072
  // Conversion 2 can at most choose between 7 types (vs. a Grass-type attack)
2753
- var CONVERSION_2: [7]Type = [_]Type{.@"???"} ** 7;
3073
+ var CONVERSION_2: [7]Type = @splat(.@"???");
2754
3074
 
2755
3075
  fn conversion2(battle: anytype, player: Player, mtype: Type, options: anytype) !Type {
2756
3076
  var i: u8 = 0;
2757
3077
  const neutral = @intFromEnum(Effectiveness.Neutral);
2758
3078
  assert(mtype != .@"???");
2759
- const ty: Type = if (options.calc.overridden(player, .conversion_2)) |val|
3079
+ const ty: Type = if (options.calc.overridden(player, 0, .conversion_2)) |val|
2760
3080
  @enumFromInt(@intFromEnum(val) - 1)
2761
3081
  else if (showdown) ty: {
2762
3082
  for (Type.SHOWDOWN) |t| {
@@ -2790,7 +3110,7 @@ pub const Rolls = struct {
2790
3110
  assert(count <= 8);
2791
3111
  const num = if (count == 8) 0 else @as(u8, 255) >> @intCast(count);
2792
3112
 
2793
- const ok = if (options.calc.overridden(player, .protect)) |val|
3113
+ const ok = if (options.calc.overridden(player, 0, .protect)) |val|
2794
3114
  val == .true
2795
3115
  else if (showdown)
2796
3116
  battle.rng.chance(u8, num, 255)
@@ -2812,7 +3132,7 @@ pub const Rolls = struct {
2812
3132
  const moves = active.moves;
2813
3133
 
2814
3134
  // TODO: consider throwing error instead of rerolling?
2815
- const overridden = if (options.calc.overridden(player, .move_slot)) |val|
3135
+ const overridden = if (options.calc.overridden(player, 0, .move_slot)) |val|
2816
3136
  if (convertible(active, moves[val - 1].id)) val else null
2817
3137
  else
2818
3138
  null;
@@ -2849,7 +3169,7 @@ pub const Rolls = struct {
2849
3169
  const moves = active.moves;
2850
3170
 
2851
3171
  // TODO: consider throwing error instead of rerolling?
2852
- const overridden = if (options.calc.overridden(player, .move_slot)) |val|
3172
+ const overridden = if (options.calc.overridden(player, 0, .move_slot)) |val|
2853
3173
  if (val != mslot and Move.get(moves[val - 1].id).extra.sleep_talk) val else null
2854
3174
  else
2855
3175
  null;
@@ -2893,7 +3213,7 @@ pub const Rolls = struct {
2893
3213
  assert(n > 0 and n <= 6);
2894
3214
 
2895
3215
  // TODO: consider throwing error instead of rerolling?
2896
- const overridden = if (options.calc.overridden(player, .force_switch)) |val| val: {
3216
+ const overridden = if (options.calc.overridden(player, 0, .force_switch)) |val| val: {
2897
3217
  for (slots) |slot| if (slot == val) break :val if (foe.get(slot).hp > 0) val else null;
2898
3218
  break :val null;
2899
3219
  } else null;
@@ -2919,7 +3239,7 @@ pub const Rolls = struct {
2919
3239
  const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
2920
3240
 
2921
3241
  fn multiHit(battle: anytype, player: Player, options: anytype) !u3 {
2922
- const n: u3 = if (options.calc.overridden(player, .multi_hit)) |val|
3242
+ const n: u3 = if (options.calc.overridden(player, 0, .multi_hit)) |val|
2923
3243
  @intCast(val)
2924
3244
  else if (showdown)
2925
3245
  DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
@@ -2934,7 +3254,7 @@ pub const Rolls = struct {
2934
3254
  }
2935
3255
 
2936
3256
  fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2937
- const power = if (options.calc.overridden(player, .psywave)) |val|
3257
+ const power = if (options.calc.overridden(player, 0, .psywave)) |val|
2938
3258
  val
2939
3259
  else if (showdown)
2940
3260
  battle.rng.range(u8, 1, max)
@@ -2960,14 +3280,14 @@ pub const Rolls = struct {
2960
3280
  }
2961
3281
 
2962
3282
  // TODO: consider throwing error instead of rerolling?
2963
- const overridden = if (options.calc.overridden(player, .metronome)) |val| val: {
3283
+ const overridden = if (options.calc.overridden(player, 0, .metronome)) |val| val: {
2964
3284
  for (moves) |m| if (m.id == val) break :val null;
2965
3285
  break :val val;
2966
3286
  } else null;
2967
3287
 
2968
3288
  const move: Move = overridden orelse move: {
2969
3289
  if (showdown) {
2970
- var r = battle.rng.range(u8, 0, @intCast(Move.METRONOME.len - n));
3290
+ var r = battle.rng.range(u8, 0, @as(u8, @intCast(Move.METRONOME.len - n)));
2971
3291
  for (moves) |m| {
2972
3292
  if (m.id == .None) break;
2973
3293
  if (@intFromEnum(m.id) > @intFromEnum(Move.METRONOME[r])) break;
@@ -2992,7 +3312,7 @@ pub const Rolls = struct {
2992
3312
  }
2993
3313
 
2994
3314
  fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2995
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
3315
+ const duration: u3 = if (options.calc.overridden(player, 0, .duration)) |val|
2996
3316
  @intCast(val)
2997
3317
  else if (showdown)
2998
3318
  battle.rng.range(u3, 1, 7)
@@ -3004,12 +3324,12 @@ pub const Rolls = struct {
3004
3324
  };
3005
3325
 
3006
3326
  assert(duration >= 1 and duration <= 6);
3007
- options.chance.duration(.sleep, player, player.foe(), duration);
3327
+ options.chance.duration(player, duration);
3008
3328
  return duration + 1;
3009
3329
  }
3010
3330
 
3011
3331
  fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
3012
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
3332
+ const duration: u3 = if (options.calc.overridden(player, 0, .duration)) |val|
3013
3333
  @intCast(val)
3014
3334
  else if (showdown)
3015
3335
  // Pokémon Showdown incorrectly uses the same duration for self-confusion
@@ -3018,16 +3338,16 @@ pub const Rolls = struct {
3018
3338
  @intCast((battle.rng.next() & if (self) 1 else 3) + 2);
3019
3339
 
3020
3340
  assert(duration >= 2 and duration <= if (!showdown and self) 3 else 5);
3021
- options.chance.duration(.confusion, player, if (self) player else player.foe(), duration);
3341
+ options.chance.duration(player, duration);
3022
3342
  return duration;
3023
3343
  }
3024
3344
 
3025
3345
  fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
3026
- const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
3346
+ const duration: u3 = if (options.calc.overridden(player, 0, .duration)) |val|
3027
3347
  @intCast(val)
3028
3348
  else if (showdown)
3029
3349
  // Pokémon Showdown incorrectly inherits Generation III's Disable duration
3030
- battle.rng.range(u4, 2, 6)
3350
+ battle.rng.range(u3, 2, 6)
3031
3351
  else duration: {
3032
3352
  while (true) {
3033
3353
  const r = battle.rng.next() & 7;
@@ -3036,12 +3356,12 @@ pub const Rolls = struct {
3036
3356
  };
3037
3357
 
3038
3358
  assert(duration >= 2 and duration <= (if (showdown) 5 else 7));
3039
- options.chance.duration(.disable, player, player.foe(), duration);
3359
+ options.chance.duration(player, duration);
3040
3360
  return duration;
3041
3361
  }
3042
3362
 
3043
3363
  fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
3044
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
3364
+ const duration: u3 = if (options.calc.overridden(player, 0, .duration)) |val|
3045
3365
  @intCast(val)
3046
3366
  else if (showdown)
3047
3367
  battle.rng.range(u3, 2, 4)
@@ -3049,12 +3369,12 @@ pub const Rolls = struct {
3049
3369
  @intCast((battle.rng.next() & 1) + 2); // BUG: thrash is + 1 not +2
3050
3370
 
3051
3371
  assert(duration >= 2 and duration <= 3);
3052
- options.chance.duration(.attacking, player, player, duration);
3372
+ options.chance.duration(player, duration);
3053
3373
  return duration;
3054
3374
  }
3055
3375
 
3056
3376
  fn bindingDuration(battle: anytype, player: Player, options: anytype) u3 {
3057
- const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
3377
+ const duration: u3 = (if (options.calc.overridden(player, 0, .duration)) |val|
3058
3378
  @intCast(val)
3059
3379
  else if (showdown)
3060
3380
  // TODO: PS does range(3, 6) here which after subtracting 1 is actually 2-4 not 2-5!
@@ -3063,13 +3383,13 @@ pub const Rolls = struct {
3063
3383
  @intCast(battle.rng.next() & 3 + 2));
3064
3384
 
3065
3385
  assert(duration >= 2 and duration <= 5);
3066
- options.chance.duration(.binding, player, player.foe(), duration);
3386
+ options.chance.duration(player, duration);
3067
3387
  return duration + 1;
3068
3388
  }
3069
3389
 
3070
3390
  // TODO: consider sharing implementation with bindingDuration
3071
3391
  fn encoreDuration(battle: anytype, player: Player, options: anytype) u3 {
3072
- const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
3392
+ const duration: u3 = (if (options.calc.overridden(player, 0, .duration)) |val|
3073
3393
  @intCast(val)
3074
3394
  else if (showdown)
3075
3395
  battle.rng.range(u3, 2, 6)
@@ -3077,7 +3397,7 @@ pub const Rolls = struct {
3077
3397
  @intCast(battle.rng.next() & 3 + 2));
3078
3398
 
3079
3399
  assert(duration >= 2 and duration <= 5);
3080
- options.chance.duration(.encore, player, player.foe(), duration);
3400
+ options.chance.duration(player, duration);
3081
3401
  return duration + 1;
3082
3402
  }
3083
3403
  };
@@ -3175,11 +3495,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3175
3495
  },
3176
3496
  .Switch => {
3177
3497
  const side = battle.side(player);
3178
- var slot: u4 = 2;
3498
+ var slot: usize = 2;
3179
3499
  while (slot <= 6) : (slot += 1) {
3180
3500
  const id = side.order[slot - 1];
3181
3501
  if (id == 0 or side.pokemon[id - 1].hp == 0) continue;
3182
- out[n] = .{ .type = .Switch, .data = slot };
3502
+ out[n] = .{ .type = .Switch, .data = @intCast(slot) };
3183
3503
  n += 1;
3184
3504
  }
3185
3505
  if (n == 0) {
@@ -3190,7 +3510,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3190
3510
  .Move => {
3191
3511
  const side = battle.side(player);
3192
3512
 
3193
- var active = &side.active;
3513
+ const active = &side.active;
3194
3514
  const stored = side.stored();
3195
3515
 
3196
3516
  // While players are not given any input options on the cartridge in these cases,
@@ -3209,11 +3529,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3209
3529
  return n;
3210
3530
  }
3211
3531
 
3212
- var slot: u4 = 2;
3213
- while (!active.volatiles.trapped and slot <= 6) : (slot += 1) {
3532
+ var slot: usize = 2;
3533
+ while (!active.volatiles.Trapped and slot <= 6) : (slot += 1) {
3214
3534
  const id = side.order[slot - 1];
3215
3535
  if (id == 0 or side.pokemon[id - 1].hp == 0) continue;
3216
- out[n] = .{ .type = .Switch, .data = slot };
3536
+ out[n] = .{ .type = .Switch, .data = @intCast(slot) };
3217
3537
  n += 1;
3218
3538
  }
3219
3539
 
@@ -3246,7 +3566,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3246
3566
  const struggle =
3247
3567
  m.id == .Bide and (m.pp == 0 or active.volatiles.disable.move == slot);
3248
3568
  const s = if (struggle) 0 else slot;
3249
- out[n] = .{ .type = .Move, .data = s };
3569
+ out[n] = .{ .type = .Move, .data = @intCast(s) };
3250
3570
  n += 1;
3251
3571
  return n;
3252
3572
  }
@@ -3260,7 +3580,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3260
3580
  if (m.id == .None) break;
3261
3581
  if (m.pp == 0) continue;
3262
3582
  if (active.volatiles.disable.move == slot) continue;
3263
- out[n] = .{ .type = .Move, .data = slot };
3583
+ out[n] = .{ .type = .Move, .data = @intCast(slot) };
3264
3584
  n += 1;
3265
3585
  }
3266
3586
  // Struggle (Pokémon Showdown would use 'move 1' here)