@pkmn/engine 0.1.0-dev.a93847b2 → 0.1.0-dev.ac652f4e

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