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

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.
@@ -9,6 +9,7 @@ const rng = @import("../common/rng.zig");
9
9
  const util = @import("../common/util.zig");
10
10
 
11
11
  const data = @import("data.zig");
12
+ const generated = @import("generated.zig");
12
13
 
13
14
  const assert = std.debug.assert;
14
15
 
@@ -24,6 +25,7 @@ const Player = common.Player;
24
25
  const Result = common.Result;
25
26
 
26
27
  const Activate = protocol.Activate;
28
+ const Boost = protocol.Boost;
27
29
  const Cant = protocol.Cant;
28
30
  const Damage = protocol.Damage;
29
31
  const End = protocol.End;
@@ -48,6 +50,7 @@ const TriAttack = data.TriAttack;
48
50
  const Type = data.Type;
49
51
  const Types = data.Types;
50
52
  const Volatiles = data.Volatiles;
53
+ const Weather = data.Weather;
51
54
 
52
55
  // zig fmt: off
53
56
  const STAT_BOOSTS = &[_][2]u8{
@@ -93,21 +96,25 @@ const CRITICAL_HITS = &[_]u8{
93
96
 
94
97
  const MAX_STAT_VALUE = 999;
95
98
 
96
- const State = struct {
97
- damage: u16 = 0,
98
- effectiveness: u16 = Effectiveness.neutral,
99
- bp: u8 = 0,
100
- move: Move = .None,
101
- mslot: u4 = 0,
102
- first: bool = false,
103
- miss: bool = false,
104
- crit: bool = false,
99
+ pub const State = struct {
100
+ damage: u16 = 0, // wCurDamage
101
+ effectiveness: u16 = Effectiveness.neutral, // wTypeModifier
102
+ bp: u8, // wPlayerMoveStructPower
103
+ accuracy: u8, // wEnemyMoveStruct + MOVE_ACC
104
+ move: Move, // wCurPlayerMove
105
+ from: Move = .None,
106
+ mslot: u4, // wCurMoveNum
107
+ first: bool, // wEnemyGoesFirst
108
+ miss: bool = false, // wAttackMissed
109
+ crit: bool = false, // wCriticalHit = 1
110
+ ohko: bool = false, // wCriticalHit = 2
111
+ proc: bool = true, // wEffectFailed
105
112
 
106
113
  comptime {
107
- assert(@sizeOf(State) == 10);
114
+ assert(@sizeOf(State) == 14);
108
115
  }
109
116
 
110
- inline fn immune(self: *State) bool {
117
+ pub fn immune(self: *State) bool {
111
118
  return self.effectiveness == 0;
112
119
  }
113
120
  };
@@ -137,8 +144,8 @@ fn start(battle: anytype, options: anytype) !Result {
137
144
  assert(!showdown or p2_slot == 1);
138
145
  if (p2_slot == 0) return Result.Win;
139
146
 
140
- try switchIn(battle, .P1, p1_slot, true, options);
141
- try switchIn(battle, .P2, p2_slot, true, options);
147
+ try switchIn(battle, .P1, p1_slot, .Initial, options);
148
+ try switchIn(battle, .P2, p2_slot, .Initial, options);
142
149
 
143
150
  return endTurn(battle, options);
144
151
  }
@@ -212,7 +219,9 @@ fn selectMove(battle: anytype, player: Player, choice: Choice) void {
212
219
  volatiles.protect = 0;
213
220
  }
214
221
 
215
- fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: anytype) !void {
222
+ const SwitchIn = enum { Initial, Switch, Drag };
223
+
224
+ fn switchIn(battle: anytype, player: Player, slot: u8, kind: SwitchIn, options: anytype) !void {
216
225
  var side = battle.side(player);
217
226
  var foe = battle.foe(player);
218
227
 
@@ -220,7 +229,7 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: a
220
229
  const incoming = side.get(slot);
221
230
 
222
231
  assert(incoming.hp != 0);
223
- assert(slot != 1 or initial);
232
+ assert(slot != 1 or kind == .Initial);
224
233
 
225
234
  const out = side.pokemon[0].position;
226
235
  side.pokemon[0].position = side.pokemon[slot - 1].position;
@@ -238,7 +247,11 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: a
238
247
 
239
248
  statusModify(incoming.status, &active.stats);
240
249
 
241
- try options.log.switched(battle.active(player), incoming);
250
+ if (kind == .Drag) {
251
+ try options.log.drag(battle.active(player), incoming);
252
+ } else {
253
+ try options.log.switched(battle.active(player), incoming);
254
+ }
242
255
  options.chance.switched(player, side.pokemon[0].position, out);
243
256
 
244
257
  if (side.conditions.Spikes and !active.types.includes(.Flying)) {
@@ -257,7 +270,13 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice, options: anytype) !Player
257
270
  // TODO: add to pending and only commit roll on Pokémon Showdown if roll is used
258
271
  const qkc = showdown and try Rolls.quickClaw(battle, .P1, options);
259
272
 
260
- if ((c1.type == .Switch) != (c2.type == .Switch)) return if (c1.type == .Switch) .P1 else .P2;
273
+ if ((c1.type == .Switch) != (c2.type == .Switch)) {
274
+ const m1 = .Pound; // TODO battle.side(.P1).last_selected_move;
275
+ const m2 = .Pound; // TODO battle.side(.P2).last_selected_move;
276
+ return if (c1.type == .Switch)
277
+ if (m1 == .Pursuit) .P2 else .P1
278
+ else if (m2 == .Pursuit) .P1 else .P2;
279
+ }
261
280
 
262
281
  // https://www.smogon.com/forums/threads/adv-switch-priority.3622189/
263
282
  // > In Gen 1 it's irrelevant [which player switches first] because switches happen instantly on
@@ -337,25 +356,33 @@ fn doTurn(
337
356
  const volatiles = &battle.side(player).active.volatiles;
338
357
  const foe_volatiles = &battle.foe(player).active.volatiles;
339
358
 
359
+ // Technically this happens at the very beginning of update but its unused until now
360
+ // TODO: move into state and pass into betweenTurns
361
+ volatiles.frozen = false;
362
+ foe_volatiles.frozen = false;
363
+
340
364
  volatiles.DestinyBond = false;
341
365
  if (try executeMove(battle, player, player_choice, true, options)) |r| return r;
342
366
  foe_volatiles.Protect = false;
343
367
  foe_volatiles.Endure = false;
344
368
  foe_volatiles.DestinyBond = false;
369
+ if (try checkFaint(battle, foe_player, options)) |r| return r;
370
+ if (try handleResidual(battle, player, options)) {
371
+ if (try checkFaint(battle, player, options)) |r| return r;
372
+ }
345
373
 
346
374
  // foe_volatiles.DestinyBond = false;
347
375
  if (try executeMove(battle, foe_player, foe_choice, false, options)) |r| return r;
348
376
  volatiles.Protect = false;
349
377
  volatiles.Endure = false;
350
378
  volatiles.DestinyBond = false;
379
+ if (try checkFaint(battle, player, options)) |r| return r;
380
+ if (try handleResidual(battle, foe_player, options)) {
381
+ if (try checkFaint(battle, foe_player, options)) |r| return r;
382
+ }
351
383
 
352
384
  // FIXME
353
- try handleResidual(battle, player, options);
354
-
355
- // FIXME
356
- try betweenTurns(battle, 0, options);
357
-
358
- return null;
385
+ return betweenTurns(battle, 0, options);
359
386
  }
360
387
 
361
388
  fn executeMove(
@@ -366,7 +393,7 @@ fn executeMove(
366
393
  options: anytype,
367
394
  ) !?Result {
368
395
  if (choice.type == .Switch) {
369
- try switchIn(battle, player, choice.data, false, options);
396
+ try switchIn(battle, player, choice.data, .Switch, options);
370
397
  return null;
371
398
  }
372
399
 
@@ -376,8 +403,12 @@ fn executeMove(
376
403
 
377
404
  if (!try beforeMove(battle, player, move, options)) return null;
378
405
 
379
- var state: State = .{ .move = move, .mslot = mslot, .first = first };
380
- return doMove(battle, player, &state, options);
406
+ // FIXME: need damage from doTurn
407
+ const m = Move.get(move);
408
+ var state: State =
409
+ .{ .move = move, .mslot = mslot, .first = first, .accuracy = m.accuracy, .bp = m.bp };
410
+ try generated.runMove(battle, player, &state, options);
411
+ return null;
381
412
  }
382
413
 
383
414
  fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bool {
@@ -391,7 +422,7 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
391
422
 
392
423
  if (volatiles.Recharging) {
393
424
  volatiles.Recharging = false;
394
- cantMove(volatiles);
425
+ resetCant(volatiles);
395
426
  try log.cant(ident, .Recharge);
396
427
  return false;
397
428
  }
@@ -413,18 +444,18 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
413
444
  if (duration == 0) {
414
445
  try log.curestatus(ident, before, .Message);
415
446
  stored.status = 0; // clears EXT if present
416
- cantMove(volatiles);
447
+ resetCant(volatiles);
417
448
  volatiles.Nightmare = false;
418
449
  } else {
419
450
  try log.cant(ident, .Sleep);
420
451
  if (move == .Snore or move == .SleepTalk) break :slp;
421
- cantMove(volatiles);
452
+ resetCant(volatiles);
422
453
  return false;
423
454
  }
424
455
  }
425
456
 
426
457
  if (Status.is(stored.status, .FRZ) and !(move == .FlameWheel or move == .SacredFire)) {
427
- cantMove(volatiles);
458
+ resetCant(volatiles);
428
459
  try log.cant(ident, .Freeze);
429
460
  return false;
430
461
  }
@@ -434,7 +465,7 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
434
465
  // Pokémon can sometimes flinch multiple times from the same original hit
435
466
  // if (!showdown) volatiles.Flinch = false;
436
467
  volatiles.Flinch = false;
437
- cantMove(volatiles);
468
+ resetCant(volatiles);
438
469
  try log.cant(ident, .Flinch);
439
470
  return false;
440
471
  }
@@ -472,7 +503,7 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
472
503
  if (try Rolls.confused(battle, player, options)) {
473
504
  volatiles.BeatUp = false;
474
505
  volatiles.Flinch = false;
475
- cantMove(volatiles);
506
+ resetCant(volatiles);
476
507
 
477
508
  // TODO HitConfusion
478
509
 
@@ -485,7 +516,7 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
485
516
  try log.activate(ident, .Attract);
486
517
 
487
518
  if (try Rolls.attract(battle, player, options)) {
488
- cantMove(volatiles);
519
+ resetCant(volatiles);
489
520
  try log.cant(ident, .Attract);
490
521
  return false;
491
522
  }
@@ -496,14 +527,14 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
496
527
  const m = side.active.moves[side.active.volatiles.disable.move - 1].id;
497
528
  if (m != .None and m == move) {
498
529
  side.active.volatiles.Charging = false;
499
- cantMove(volatiles);
530
+ resetCant(volatiles);
500
531
  try options.log.disabled(ident, move);
501
532
  return false;
502
533
  }
503
534
  }
504
535
 
505
536
  if (Status.is(stored.status, .PAR) and try Rolls.paralyzed(battle, player, options)) {
506
- cantMove(volatiles);
537
+ resetCant(volatiles);
507
538
  try log.cant(ident, .Paralysis);
508
539
  return false;
509
540
  }
@@ -511,7 +542,7 @@ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bo
511
542
  return true;
512
543
  }
513
544
 
514
- fn cantMove(volatiles: *align(8) Volatiles) void {
545
+ fn resetCant(volatiles: *align(8) Volatiles) void {
515
546
  volatiles.Bide = false;
516
547
  volatiles.Thrashing = false;
517
548
  volatiles.Charging = false;
@@ -522,14 +553,32 @@ fn cantMove(volatiles: *align(8) Volatiles) void {
522
553
  volatiles.fury_cutter = 0;
523
554
  }
524
555
 
525
- fn canMove(battle: anytype, player: Player, options: anytype) !void {
556
+ pub fn canMove(battle: anytype, player: Player, state: *State, options: anytype) !bool {
557
+ var side = battle.side(player);
558
+ var volatiles = &side.active.volatiles;
559
+
560
+ // FIXME: pass in target to avoid lookup?
561
+ const target = if (Move.get(state.move).target == .Self) player else player.foe();
562
+ try options.log.moveFrom(battle.active(player), state.move, battle.active(target), state.from);
563
+
564
+ const charging = false; // TODO charging uses doUse
565
+ const skip_pp = charging or state.move == .Struggle or
566
+ (volatiles.BeatUp or volatiles.Thrashing or volatiles.Bide);
567
+ if (!skip_pp) _ = decrementPP(side, state.move, state.mslot); // TODO if no pp return
568
+
569
+ return true;
570
+ }
571
+
572
+ pub fn canCharge(battle: anytype, player: Player, state: *State, options: anytype) !bool {
526
573
  _ = battle;
527
574
  _ = player;
575
+ _ = state;
528
576
  _ = options;
529
- return false;
577
+ // TODO skipsuncharge account for SolarBeam
578
+ return true;
530
579
  }
531
580
 
532
- fn decrementPP(side: *Side, move: Move, mslot: u4) bool {
581
+ pub fn decrementPP(side: *Side, move: Move, mslot: u4) bool {
533
582
  var active = &side.active;
534
583
  const volatiles = &active.volatiles;
535
584
 
@@ -553,32 +602,41 @@ fn decrementPP(side: *Side, move: Move, mslot: u4) bool {
553
602
  return true;
554
603
  }
555
604
 
556
- fn doMove(battle: anytype, player: Player, state: *State, options: anytype) !?Result {
605
+ pub fn doMove(
606
+ comptime secondary: bool,
607
+ battle: anytype,
608
+ player: Player,
609
+ state: *State,
610
+ options: anytype,
611
+ ) !bool {
557
612
  try checkCriticalHit(battle, player, state, options);
558
613
  try calcDamage(battle, player, state, options);
559
614
  try adjustDamage(battle, player, state, options);
560
615
  try randomizeDamage(battle, player, state, options);
616
+ try checkHit(battle, player, state, options);
617
+ if (secondary) try effectChance(battle, player, state, options);
561
618
  try applyDamage(battle, player, state, options);
619
+ try reportOutcome(battle, player, state, options);
620
+ if (try destinyBond(battle, player, state, options)) return false;
562
621
  try buildRage(battle, player, state, options);
563
622
  try kingsRock(battle, player, state, options);
623
+ return true;
624
+ }
564
625
 
565
- try Effects.attract(battle, player, state, options);
566
- try Effects.conversion(battle, player, state, options);
567
- try Effects.conversion2(battle, player, state, options);
568
- try Effects.destinyBond(battle, player, state, options);
569
- try Effects.disable(battle, player, state, options);
570
- try Effects.encore(battle, player, state, options);
571
- try Effects.falseSwipe(battle, player, state, options);
572
- try Effects.focusEnergy(battle, player, state, options);
573
- try Effects.forceSwitch(battle, player, state, options);
574
- try Effects.foresight(battle, player, state, options);
575
- try Effects.happiness(battle, player, state, options);
576
- try Effects.protect(battle, player, state, options);
577
-
578
- return null;
626
+ pub fn afterMove(
627
+ comptime kings: bool,
628
+ battle: anytype,
629
+ player: Player,
630
+ state: *State,
631
+ options: anytype,
632
+ ) !bool {
633
+ if (try destinyBond(battle, player, state, options)) return false;
634
+ try buildRage(battle, player, state, options);
635
+ if (kings) try kingsRock(battle, player, state, options);
636
+ return true;
579
637
  }
580
638
 
581
- fn checkCriticalHit(battle: anytype, player: Player, state: *State, options: anytype) !void {
639
+ pub fn checkCriticalHit(battle: anytype, player: Player, state: *State, options: anytype) !void {
582
640
  const move = Move.get(state.move);
583
641
  if (move.bp == 0) return;
584
642
 
@@ -598,7 +656,7 @@ fn checkCriticalHit(battle: anytype, player: Player, state: *State, options: any
598
656
  state.crit = try Rolls.criticalHit(battle, player, CRITICAL_HITS[stage], options);
599
657
  }
600
658
 
601
- fn calcDamage(
659
+ pub fn calcDamage(
602
660
  battle: anytype,
603
661
  player: Player,
604
662
  state: *State,
@@ -612,7 +670,7 @@ fn calcDamage(
612
670
 
613
671
  const FORESIGHT: Types = .{ .type1 = .Normal, .type2 = .Fighting };
614
672
 
615
- fn adjustDamage(battle: anytype, player: Player, state: *State, _: anytype) !void {
673
+ pub fn adjustDamage(battle: anytype, player: Player, state: *State, _: anytype) !void {
616
674
  if (state.move == .Struggle) return;
617
675
 
618
676
  const side = battle.side(player);
@@ -662,7 +720,7 @@ fn adjustDamage(battle: anytype, player: Player, state: *State, _: anytype) !voi
662
720
  state.damage = d;
663
721
  }
664
722
 
665
- fn randomizeDamage(battle: anytype, player: Player, state: *State, options: anytype) !void {
723
+ pub fn randomizeDamage(battle: anytype, player: Player, state: *State, options: anytype) !void {
666
724
  if (state.damage <= 1) return;
667
725
  const random = try Rolls.damage(battle, player, options);
668
726
  options.calc.base(player.foe(), state.damage);
@@ -670,12 +728,13 @@ fn randomizeDamage(battle: anytype, player: Player, state: *State, options: anyt
670
728
  options.calc.final(player.foe(), state.damage);
671
729
  }
672
730
 
673
- fn applyDamage(
731
+ pub fn applyDamage(
674
732
  battle: anytype,
675
733
  player: Player,
676
734
  state: *State,
677
735
  options: anytype,
678
736
  ) !void {
737
+ // TODO failuretext (if not Bide) + applydamage
679
738
  _ = battle;
680
739
  _ = player;
681
740
  _ = state;
@@ -683,7 +742,7 @@ fn applyDamage(
683
742
  }
684
743
 
685
744
  // TODO: set damage = 0 if missed and not (High) Jump Kick
686
- fn checkHit(
745
+ pub fn checkHit(
687
746
  battle: anytype,
688
747
  player: Player,
689
748
  state: *State,
@@ -708,7 +767,7 @@ fn checkHit(
708
767
  return;
709
768
  }
710
769
  if (side.active.volatiles.LockOn) {
711
- state.miss = !(foe.active.volatiles.Flying and move.extra.undeground);
770
+ state.miss = !(foe.active.volatiles.Flying and move.extra.underground);
712
771
  return;
713
772
  }
714
773
  assert(!(foe.active.volatiles.Flying and foe.active.volatiles.Underground));
@@ -723,16 +782,23 @@ fn checkHit(
723
782
  return;
724
783
  }
725
784
  }
726
- if (move.effect == .Thunder and battle.field.weather == .Rain) {
727
- state.miss = false;
728
- return;
785
+ if (move.effect == .Thunder) {
786
+ if (battle.field.weather == .Rain) {
787
+ state.miss = false;
788
+ return;
789
+ } else if (battle.field.weather == .Sun) {
790
+ // This latter part gets applied in a separate Thunder accuracy handler
791
+ // on the cartridge but it makes more sense to just do it here
792
+ state.accuracy = Gen12.percent(50) + 1;
793
+ }
729
794
  }
730
795
  if (move.effect == .AlwaysHit) {
731
796
  state.miss = false;
732
797
  return;
733
798
  }
734
799
 
735
- var accuracy: u16 = move.accuracy;
800
+ var accuracy: u16 = state.accuracy;
801
+ assert(accuracy > 0);
736
802
  if (foe.active.boosts.evasion <= side.active.boosts.accuracy or
737
803
  !foe.active.volatiles.Foresight)
738
804
  {
@@ -757,24 +823,85 @@ fn checkHit(
757
823
  fn checkFaint(
758
824
  battle: anytype,
759
825
  player: Player,
760
- state: *State,
761
826
  options: anytype,
762
827
  ) @TypeOf(options.log).Error!?Result {
763
- _ = battle;
764
- _ = player;
765
- _ = state;
766
- return null;
828
+ const side = battle.side(player);
829
+ const foe = battle.foe(player);
830
+
831
+ const player_fainted = side.stored().hp == 0;
832
+ const foe_fainted = foe.stored().hp == 0;
833
+ if (!(player_fainted or foe_fainted)) return null;
834
+
835
+ const player_out = player_fainted and findFirstAlive(side) == 0;
836
+ const foe_out = foe_fainted and findFirstAlive(foe) == 0;
837
+ const more = player_out or foe_out;
838
+
839
+ if (player_fainted) try faint(battle, player, !(more or foe_fainted), options);
840
+ if (foe_fainted) try faint(battle, player.foe(), !more, options);
841
+
842
+ if (player_out and foe_out) {
843
+ try options.log.tie();
844
+ return Result.Tie;
845
+ } else if (player_out) {
846
+ try options.log.win(player.foe());
847
+ return if (player == .P1) Result.Lose else Result.Win;
848
+ } else if (foe_out) {
849
+ try options.log.win(player);
850
+ return if (player == .P1) Result.Win else Result.Lose;
851
+ }
852
+
853
+ const foe_choice: Choice.Type = if (foe_fainted) .Switch else .Pass;
854
+ if (player == .P1) return .{ .p1 = .Switch, .p2 = foe_choice };
855
+ return .{ .p1 = foe_choice, .p2 = .Switch };
767
856
  }
768
857
 
769
- fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
770
- _ = battle;
771
- _ = player;
772
- _ = done;
773
- _ = options;
774
- return null;
858
+ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !void {
859
+ var side = battle.side(player);
860
+ var foe = battle.foe(player);
861
+ assert(side.stored().hp == 0);
862
+
863
+ var foe_volatiles = &foe.active.volatiles;
864
+ foe_volatiles.BeatUp = false;
865
+
866
+ // We always need to clear status for Pokémon Showdown's Sleep/Freeze Clause Mod
867
+ const status = side.stored().status;
868
+
869
+ side.stored().status = 0;
870
+ // This shouldn't matter, but Pokémon Showdown decides double switching priority based on speed,
871
+ // and resets a Pokémon's stats when it faints... only it still factors in paralysis -_-
872
+ if (showdown) {
873
+ side.active.stats.spe = if (Status.is(status, .PAR))
874
+ @max(side.stored().stats.spe / 4, 1)
875
+ else
876
+ side.stored().stats.spe;
877
+ }
878
+
879
+ try options.log.faint(battle.active(player), done);
880
+ options.calc.capped(player);
881
+ }
882
+
883
+ // TODO: see if we can move *before* applyDamage and include failuretext logic
884
+ pub fn reportOutcome(battle: anytype, player: Player, state: *State, options: anytype) !void {
885
+ const foe_ident = battle.active(player.foe());
886
+ if (state.crit) try options.log.crit(foe_ident);
887
+ if (!state.immune()) {
888
+ if (state.effectiveness > Effectiveness.neutral) {
889
+ try options.log.supereffective(foe_ident);
890
+ } else if (state.effectiveness < Effectiveness.neutral) {
891
+ try options.log.resisted(foe_ident);
892
+ }
893
+ }
894
+ }
895
+
896
+ pub fn effectChance(battle: anytype, player: Player, state: *State, options: anytype) !void {
897
+ const rate = Move.get(state.move).chance;
898
+ assert(rate > 0);
899
+ // FIXME: only commit secondary chance roll if effect actually is successful (doesn't fail)
900
+ state.proc = !battle.foe(player).active.volatiles.Substitute and
901
+ try Rolls.secondaryChance(battle, player, rate, options);
775
902
  }
776
903
 
777
- fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
904
+ fn handleResidual(battle: anytype, player: Player, options: anytype) !bool {
778
905
  var side = battle.side(player);
779
906
  var stored = side.stored();
780
907
  const ident = battle.active(player);
@@ -796,7 +923,7 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
796
923
  stored.hp -|= damage;
797
924
  // Pokémon Showdown uses damageOf here but its not relevant in Generation II
798
925
  try options.log.damage(ident, stored, if (brn) Damage.Burn else Damage.Poison);
799
- if (stored.hp == 0) return;
926
+ if (stored.hp == 0) return true;
800
927
  }
801
928
 
802
929
  if (volatiles.LeechSeed) {
@@ -818,28 +945,30 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
818
945
  foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
819
946
  // Pokémon Showdown uses the less specific heal here instead of drain... because reasons?
820
947
  if (foe_stored.hp > before) try options.log.heal(foe_ident, foe_stored, .Silent);
821
- if (stored.hp == 0) return;
948
+ if (stored.hp == 0) return true;
822
949
  }
823
950
 
824
951
  if (volatiles.Nightmare) {
825
952
  stored.hp -= @max(stored.stats.hp / 4, 1);
826
953
  // ibid
827
954
  try options.log.damage(ident, stored, .Nightmare);
828
- if (stored.hp == 0) return;
955
+ if (stored.hp == 0) return true;
829
956
  }
830
957
 
831
958
  if (volatiles.Curse) {
832
959
  stored.hp -= @max(stored.stats.hp / 4, 1);
833
960
  // ibid
834
961
  try options.log.damage(ident, stored, .Curse);
835
- if (stored.hp == 0) return;
962
+ if (stored.hp == 0) return true;
836
963
  }
964
+
965
+ return false;
837
966
  }
838
967
 
839
- fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !void {
968
+ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
840
969
  const players = comptime std.enums.values(Player);
841
970
 
842
- // TODO checkFaintThen(p1, p2);
971
+ if (try checkFaint(battle, .P1, options)) |r| return r;
843
972
 
844
973
  // Future Sight
845
974
  inline for (players) |player| {
@@ -855,7 +984,7 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !void {
855
984
  }
856
985
  }
857
986
  }
858
- // TODO checkFaintThen(p1, p2);
987
+ if (try checkFaint(battle, .P1, options)) |r| return r;
859
988
 
860
989
  // Weather
861
990
  if (battle.field.weather != .None) {
@@ -876,7 +1005,7 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !void {
876
1005
  try options.log.damage(battle.active(player), side.stored(), .Sandstorm);
877
1006
  }
878
1007
  }
879
- // TODO checkFaintThen(p1, p2);
1008
+ if (try checkFaint(battle, .P1, options)) |r| return r;
880
1009
  }
881
1010
  }
882
1011
  }
@@ -901,7 +1030,7 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !void {
901
1030
  }
902
1031
  }
903
1032
  }
904
- // TODO checkFaintThen(p1, p2);
1033
+ if (try checkFaint(battle, .P1, options)) |r| return r;
905
1034
 
906
1035
  // Perish Song
907
1036
  inline for (players) |player| {
@@ -922,7 +1051,7 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !void {
922
1051
  }
923
1052
  }
924
1053
  }
925
- // TODO checkFaintThen(p1, p2);
1054
+ if (try checkFaint(battle, .P1, options)) |r| return r;
926
1055
 
927
1056
  // Leftovers
928
1057
  inline for (players) |player| {
@@ -1015,7 +1144,7 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !void {
1015
1144
 
1016
1145
  // FIXME wtf?
1017
1146
  // side.active.volatiles.Toxic = false;
1018
- // side.active.volatiles.NightMare = false;
1147
+ // side.active.volatiles.Nightmare = false;
1019
1148
  if (stored.item == .MiracleBerry) {
1020
1149
  side.active.volatiles.Confusion = false;
1021
1150
  try options.log.end(ident, .Confusion);
@@ -1053,9 +1182,11 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !void {
1053
1182
  }
1054
1183
  }
1055
1184
  }
1185
+
1186
+ return null;
1056
1187
  }
1057
1188
 
1058
- inline fn updateSideCondition(
1189
+ fn updateSideCondition(
1059
1190
  battle: anytype,
1060
1191
  player: Player,
1061
1192
  comptime key: []const u8,
@@ -1089,7 +1220,8 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
1089
1220
  return Result.Default;
1090
1221
  }
1091
1222
 
1092
- fn buildRage(battle: anytype, player: Player, state: *State, options: anytype) !void {
1223
+ pub fn buildRage(battle: anytype, player: Player, state: *State, options: anytype) !void {
1224
+ assert(!state.miss); // TODO
1093
1225
  var foe = battle.foe(player);
1094
1226
  if (foe.active.volatiles.Rage) {
1095
1227
  if (showdown) {
@@ -1104,7 +1236,8 @@ fn buildRage(battle: anytype, player: Player, state: *State, options: anytype) !
1104
1236
  }
1105
1237
  }
1106
1238
 
1107
- fn kingsRock(battle: anytype, player: Player, _: *State, options: anytype) !void {
1239
+ pub fn kingsRock(battle: anytype, player: Player, state: *State, options: anytype) !void {
1240
+ assert(!state.miss); // TODO
1108
1241
  var foe = battle.foe(player);
1109
1242
  const flinch = battle.side(player).stored().item == .KingsRock and
1110
1243
  !foe.active.volatiles.Substitute and
@@ -1115,123 +1248,186 @@ fn kingsRock(battle: anytype, player: Player, _: *State, options: anytype) !void
1115
1248
  }
1116
1249
  }
1117
1250
 
1118
- pub const Effects = struct {
1119
- fn alwaysHit(battle: anytype, player: Player, state: *State, options: anytype) !void {
1120
- _ = .{ battle, player, state, options }; // TODO
1251
+ pub fn destinyBond(battle: anytype, player: Player, _: *State, options: anytype) !bool {
1252
+ const foe = battle.foe(player);
1253
+ if (foe.stored().hp > 0) return false;
1254
+
1255
+ if (foe.active.volatiles.DestinyBond) {
1256
+ battle.side(player).stored().hp = 0;
1257
+ try options.log.activate(battle.active(player.foe()), .DestinyBond);
1121
1258
  }
1259
+ return true;
1260
+ }
1122
1261
 
1123
- fn attract(battle: anytype, player: Player, _: *State, options: anytype) !void {
1262
+ pub const Effects = struct {
1263
+ pub fn attract(battle: anytype, player: Player, _: *State, options: anytype) !void {
1124
1264
  const side = battle.side(player);
1125
1265
  var foe = battle.foe(player);
1126
1266
 
1127
1267
  const gender: u8 = @intFromEnum(side.stored().dvs.gender);
1128
1268
  const foe_gender: u8 = @intFromEnum(foe.stored().dvs.gender);
1129
1269
  if (gender + foe_gender != 1) {
1130
- try options.log.immune(battle.active(player.foe()), .None);
1131
- return;
1270
+ return options.log.immune(battle.active(player.foe()), .None);
1132
1271
  } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
1133
1272
  try options.log.lastmiss();
1134
- try options.log.miss(battle.active(player));
1135
- return;
1273
+ return options.log.miss(battle.active(player));
1136
1274
  }
1137
1275
 
1138
1276
  foe.active.volatiles.Attract = true;
1139
1277
  try options.log.start(battle.active(player.foe()), .Attract);
1140
1278
  }
1141
1279
 
1142
- fn batonPass(battle: anytype, player: Player, state: *State, options: anytype) !void {
1280
+ pub fn batonPass(battle: anytype, player: Player, state: *State, options: anytype) !void {
1143
1281
  _ = .{ battle, player, state, options }; // TODO
1144
1282
  }
1145
1283
 
1146
- fn beatUp(battle: anytype, player: Player, state: *State, options: anytype) !void {
1284
+ pub fn beatUp(battle: anytype, player: Player, state: *State, options: anytype) !void {
1147
1285
  _ = .{ battle, player, state, options }; // TODO
1148
1286
  }
1149
1287
 
1150
- fn bellyDrum(battle: anytype, player: Player, state: *State, options: anytype) !void {
1288
+ pub fn bellyDrum(battle: anytype, player: Player, state: *State, options: anytype) !void {
1151
1289
  _ = .{ battle, player, state, options }; // TODO
1152
1290
  }
1153
1291
 
1154
- fn bide(battle: anytype, player: Player, state: *State, options: anytype) !void {
1292
+ pub const bide = struct {
1293
+ pub fn store(battle: anytype, player: Player, state: *State, options: anytype) !void {
1294
+ var side = battle.side(player);
1295
+ var volatiles = &side.active.volatiles;
1296
+
1297
+ const ident = battle.active(player);
1298
+
1299
+ if (!volatiles.Bide) return;
1300
+
1301
+ assert(volatiles.attacks > 0);
1302
+ // if (options.calc.modify(player, .attacking)) |extend| {
1303
+ // if (!extend) volatiles.attacks = 0;
1304
+ // } else {
1305
+ volatiles.attacks -= 1;
1306
+ // }
1307
+ try options.chance.attacking(player, volatiles.attacks);
1308
+
1309
+ if (volatiles.attacks != 0) return options.log.activate(ident, .Bide);
1310
+
1311
+ volatiles.Bide = false;
1312
+ try options.log.end(ident, .Bide);
1313
+
1314
+ state.damage = volatiles.bide *% 2;
1315
+ volatiles.bide = 0;
1316
+
1317
+ if (state.damage == 0) state.miss = true;
1318
+ }
1319
+
1320
+ pub fn unleash(battle: anytype, player: Player, state: *State, options: anytype) !void {
1321
+ _ = .{ battle, player, state, options }; // TODO
1322
+ }
1323
+ };
1324
+
1325
+ pub fn binding(battle: anytype, player: Player, state: *State, options: anytype) !void {
1155
1326
  _ = .{ battle, player, state, options }; // TODO
1156
1327
  }
1157
1328
 
1158
- fn binding(battle: anytype, player: Player, state: *State, options: anytype) !void {
1159
- _ = .{ battle, player, state, options }; // TODO
1329
+ pub fn burnChance(battle: anytype, player: Player, state: *State, options: anytype) !void {
1330
+ var foe = battle.foe(player);
1331
+ var foe_stored = foe.stored();
1332
+
1333
+ if (foe.active.volatiles.Substitute) return;
1334
+ if (Status.any(foe_stored.status)) {
1335
+ if (Status.is(foe_stored.status, .FRZ)) {
1336
+ assert(Move.get(state.move).type == .Fire or state.move == .TriAttack);
1337
+ try options.log.curestatus(
1338
+ battle.active(player.foe()),
1339
+ foe_stored.status,
1340
+ .Message,
1341
+ );
1342
+ foe_stored.status = 0;
1343
+ }
1344
+ return;
1345
+ }
1346
+ if (!state.proc) return;
1347
+ if (foe.conditions.Safeguard) return;
1348
+ // Nothing is immune to Fire-type, but Ghosts are immune to burnChance called by Tri Attack
1349
+ if (state.immune()) return;
1350
+ if (foe.active.types.includes(Move.get(state.move).type) and state.move != .TriAttack) {
1351
+ return;
1352
+ }
1353
+
1354
+ foe_stored.status = Status.init(.BRN);
1355
+ foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
1356
+ try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
1357
+
1358
+ // TODO: check for status berry
1160
1359
  }
1161
1360
 
1162
- fn burnChance(battle: anytype, player: Player, state: *State, options: anytype) !void {
1361
+ pub fn confusion(battle: anytype, player: Player, state: *State, options: anytype) !void {
1163
1362
  _ = .{ battle, player, state, options }; // TODO
1164
1363
  }
1165
1364
 
1166
- fn confusion(battle: anytype, player: Player, state: *State, options: anytype) !void {
1365
+ pub fn confusionChance(battle: anytype, player: Player, state: *State, options: anytype) !void {
1167
1366
  _ = .{ battle, player, state, options }; // TODO
1168
1367
  }
1169
1368
 
1170
- fn conversion(battle: anytype, player: Player, _: *State, options: anytype) !void {
1369
+ pub fn conversion(battle: anytype, player: Player, _: *State, options: anytype) !void {
1171
1370
  var side = battle.side(player);
1172
1371
 
1173
1372
  var n: u3 = 0;
1174
1373
  for (side.active.moves) |m| {
1175
1374
  if (convertible(side.active, m.id)) n += 1;
1176
1375
  }
1177
- if (n == 0) {
1178
- try options.log.fail(battle.active(player), .None);
1179
- return;
1180
- }
1376
+ if (n == 0) return options.log.fail(battle.active(player), .None);
1181
1377
 
1182
1378
  side.active.types.type1 = try Rolls.conversion(battle, player, n, options);
1183
1379
  side.active.types.type2 = side.active.types.type1;
1184
1380
  }
1185
1381
 
1186
- fn conversion2(battle: anytype, player: Player, _: *State, options: anytype) !void {
1382
+ pub fn conversion2(battle: anytype, player: Player, _: *State, options: anytype) !void {
1187
1383
  var side = battle.side(player);
1188
1384
 
1189
1385
  const last = battle.foe(player).lastMove(false);
1190
- if (last == .None) {
1191
- try options.log.fail(battle.active(player), .None);
1192
- return;
1193
- }
1386
+ if (last == .None) return options.log.fail(battle.active(player), .None);
1194
1387
  const move = Move.get(last);
1195
- if (move.type == .@"???") {
1196
- try options.log.fail(battle.active(player), .None);
1197
- return;
1198
- }
1388
+ if (move.type == .@"???") return options.log.fail(battle.active(player), .None);
1199
1389
 
1200
1390
  side.active.types.type1 = try Rolls.conversion2(battle, player, move.type, options);
1201
1391
  side.active.types.type2 = side.active.types.type1;
1202
1392
  }
1203
1393
 
1204
- fn counter(battle: anytype, player: Player, state: *State, options: anytype) !void {
1394
+ pub fn counter(battle: anytype, player: Player, state: *State, options: anytype) !void {
1205
1395
  _ = .{ battle, player, state, options }; // TODO
1206
1396
  }
1207
1397
 
1208
- fn curse(battle: anytype, player: Player, state: *State, options: anytype) !void {
1398
+ pub fn curse(battle: anytype, player: Player, state: *State, options: anytype) !void {
1209
1399
  _ = .{ battle, player, state, options }; // TODO
1210
1400
  }
1211
1401
 
1212
- fn defenseCurl(battle: anytype, player: Player, state: *State, options: anytype) !void {
1213
- _ = .{ battle, player, state, options }; // TODO
1402
+ pub fn defenseCurl(battle: anytype, player: Player, _: *State, _: anytype) !void {
1403
+ battle.side(player).active.volatiles.DefenseCurl = true;
1214
1404
  }
1215
1405
 
1216
- fn destinyBond(battle: anytype, player: Player, _: *State, options: anytype) !void {
1406
+ pub fn defrost(battle: anytype, player: Player, _: *State, options: anytype) !void {
1407
+ var side = battle.side(player);
1408
+ assert(Status.is(side.stored().status, .FRZ));
1409
+
1410
+ try options.log.curestatus(battle.active(player), side.stored().status, .Message);
1411
+ side.stored().status = 0;
1412
+ }
1413
+
1414
+ pub fn destinyBond(battle: anytype, player: Player, _: *State, options: anytype) !void {
1217
1415
  battle.side(player).active.volatiles.DestinyBond = true;
1218
1416
  try options.log.singlemove(battle.active(player), Move.DestinyBond);
1219
1417
  }
1220
1418
 
1221
- fn disable(battle: anytype, player: Player, _: *State, options: anytype) !void {
1419
+ pub fn disable(battle: anytype, player: Player, _: *State, options: anytype) !void {
1222
1420
  var foe = battle.foe(player);
1223
1421
  var volatiles = &foe.active.volatiles;
1224
1422
  const foe_ident = battle.active(player.foe());
1225
1423
 
1226
1424
  if (volatiles.disable.move != 0) {
1227
- try options.log.fail(foe_ident, .None);
1228
- return;
1425
+ return options.log.fail(foe_ident, .None);
1229
1426
  }
1230
1427
 
1231
1428
  const last = battle.foe(player).lastMove(false);
1232
1429
  if (last == .None or last == .Struggle) {
1233
- try options.log.fail(foe_ident, .None);
1234
- return;
1430
+ return options.log.fail(foe_ident, .None);
1235
1431
  }
1236
1432
 
1237
1433
  var slot: u3 = 0;
@@ -1241,10 +1437,7 @@ pub const Effects = struct {
1241
1437
  if (m.id == last) break;
1242
1438
  }
1243
1439
  const move = foe.active.move(slot);
1244
- if (move.pp == 0) {
1245
- try options.log.fail(foe_ident, .None);
1246
- return;
1247
- }
1440
+ if (move.pp == 0) return options.log.fail(foe_ident, .None);
1248
1441
 
1249
1442
  volatiles.disable.move = slot;
1250
1443
  volatiles.disable.duration = Rolls.disableDuration(battle, player, options);
@@ -1252,15 +1445,32 @@ pub const Effects = struct {
1252
1445
  try options.log.startEffect(foe_ident, .Disable, move.id);
1253
1446
  }
1254
1447
 
1255
- fn doubleHit(battle: anytype, player: Player, state: *State, options: anytype) !void {
1256
- _ = .{ battle, player, state, options }; // TODO
1257
- }
1448
+ pub const double = struct {
1449
+ pub fn flying(battle: anytype, player: Player, state: *State, _: anytype) !void {
1450
+ if (battle.foe(player).active.volatiles.Flying) state.damage *|= 2;
1451
+ }
1258
1452
 
1259
- fn drainHP(battle: anytype, player: Player, state: *State, options: anytype) !void {
1260
- _ = .{ battle, player, state, options }; // TODO
1453
+ pub fn underground(battle: anytype, player: Player, state: *State, _: anytype) !void {
1454
+ if (battle.foe(player).active.volatiles.Underground) state.damage *|= 2;
1455
+ }
1456
+
1457
+ pub fn minimize(battle: anytype, player: Player, state: *State, _: anytype) !void {
1458
+ if (battle.foe(player).active.volatiles.minimized) state.damage *|= 2;
1459
+ }
1460
+ };
1461
+
1462
+ pub fn drainHP(battle: anytype, player: Player, state: *State, options: anytype) !void {
1463
+ var stored = battle.side(player).stored();
1464
+
1465
+ assert(state.damage != 0);
1466
+
1467
+ if (stored.hp == stored.stats.hp) return;
1468
+ stored.hp = @min(stored.stats.hp, stored.hp + @max(state.damage / 2, 1));
1469
+
1470
+ try options.log.drain(battle.active(player), stored, battle.active(player.foe()));
1261
1471
  }
1262
1472
 
1263
- fn encore(battle: anytype, player: Player, state: *State, options: anytype) !void {
1473
+ pub fn encore(battle: anytype, player: Player, state: *State, options: anytype) !void {
1264
1474
  var foe = battle.foe(player);
1265
1475
  var volatiles = &foe.active.volatiles;
1266
1476
  const foe_ident = battle.active(player.foe());
@@ -1269,10 +1479,7 @@ pub const Effects = struct {
1269
1479
  const failed = volatiles.Encore or
1270
1480
  (last == .None or last == .Struggle or
1271
1481
  last == .Encore or .last == .MirrorMove);
1272
- if (failed) {
1273
- try options.log.fail(foe_ident, .None);
1274
- return;
1275
- }
1482
+ if (failed) return options.log.fail(foe_ident, .None);
1276
1483
 
1277
1484
  var slot: u3 = 0;
1278
1485
  for (foe.active.moves) |m| {
@@ -1281,10 +1488,7 @@ pub const Effects = struct {
1281
1488
  if (m.id == last) break;
1282
1489
  }
1283
1490
  const move = foe.active.move(slot);
1284
- if (move.pp == 0) {
1285
- try options.log.fail(foe_ident, .None);
1286
- return;
1287
- }
1491
+ if (move.pp == 0) return options.log.fail(foe_ident, .None);
1288
1492
 
1289
1493
  volatiles.Encore = true;
1290
1494
  volatiles.encore = Rolls.encoreDuration(battle, player, options);
@@ -1295,34 +1499,54 @@ pub const Effects = struct {
1295
1499
  try options.log.startEffect(foe_ident, .Encore, move.id);
1296
1500
  }
1297
1501
 
1298
- fn explode(battle: anytype, player: Player, state: *State, options: anytype) !void {
1502
+ pub fn explode(battle: anytype, player: Player, state: *State, options: anytype) !void {
1299
1503
  _ = .{ battle, player, state, options }; // TODO
1300
1504
  }
1301
1505
 
1302
- fn falseSwipe(battle: anytype, player: Player, state: *State, _: anytype) !void {
1506
+ pub fn falseSwipe(battle: anytype, player: Player, state: *State, _: anytype) !void {
1303
1507
  const foe = battle.foe(player);
1304
1508
  assert(foe.stored().hp > 0);
1305
1509
  // FIXME crit?
1306
1510
  if (state.damage > foe.stored().hp) state.damage = foe.stored().hp - 1;
1307
1511
  }
1308
1512
 
1309
- fn fixedDamage(battle: anytype, player: Player, state: *State, options: anytype) !void {
1310
- _ = .{ battle, player, state, options }; // TODO
1311
- }
1513
+ pub fn fixedDamage(battle: anytype, player: Player, state: *State, options: anytype) !void {
1514
+ const side = battle.side(player);
1515
+ const foe = battle.foe(player);
1312
1516
 
1313
- fn flameWheel(battle: anytype, player: Player, state: *State, options: anytype) !void {
1314
- _ = .{ battle, player, state, options }; // TODO
1517
+ state.damage = switch (Move.get(state.move).effect) {
1518
+ .SuperFang => @max(foe.stored().hp / 2, 1),
1519
+ .LevelDamage => side.stored().level,
1520
+ .FixedDamage => if (state.move == .SonicBoom) 20 else 40,
1521
+ .Psywave => try Rolls.psywave(
1522
+ battle,
1523
+ player,
1524
+ @intCast(@as(u16, side.stored().level) * 3 / 2),
1525
+ options,
1526
+ ),
1527
+ else => unreachable,
1528
+ };
1315
1529
  }
1316
1530
 
1317
- fn flinchChance(battle: anytype, player: Player, state: *State, options: anytype) !void {
1318
- _ = .{ battle, player, state, options }; // TODO
1531
+ pub fn flinchChance(battle: anytype, player: Player, state: *State, _: anytype) !void {
1532
+ const foe = battle.foe(player);
1533
+ const status = foe.stored().status;
1534
+ var volatiles = &foe.active.volatiles;
1535
+
1536
+ if (volatiles.Substitute) return;
1537
+ if (Status.is(status, .FRZ) or Status.is(status, .SLP)) return;
1538
+ if (!state.first) return;
1539
+ if (!state.proc) return;
1540
+
1541
+ volatiles.Flinch = true;
1542
+ volatiles.Recharging = false;
1319
1543
  }
1320
1544
 
1321
- fn flyDig(battle: anytype, player: Player, state: *State, options: anytype) !void {
1545
+ pub fn flyDig(battle: anytype, player: Player, state: *State, options: anytype) !void {
1322
1546
  _ = .{ battle, player, state, options }; // TODO
1323
1547
  }
1324
1548
 
1325
- fn focusEnergy(battle: anytype, player: Player, _: *State, options: anytype) !void {
1549
+ pub fn focusEnergy(battle: anytype, player: Player, _: *State, options: anytype) !void {
1326
1550
  var side = battle.side(player);
1327
1551
 
1328
1552
  if (side.active.volatiles.FocusEnergy) {
@@ -1336,9 +1560,13 @@ pub const Effects = struct {
1336
1560
 
1337
1561
  var SLOTS: [5]u4 = .{ 0, 0, 0, 0, 0 };
1338
1562
 
1339
- fn forceSwitch(battle: anytype, player: Player, _: *State, options: anytype) !void {
1563
+ pub fn forceSwitch(battle: anytype, player: Player, state: *State, options: anytype) !void {
1564
+ assert(!state.miss); // FIXME ensure checkHit handles fail message
1565
+
1340
1566
  const foe = battle.foe(player);
1341
1567
 
1568
+ if (state.first) return options.log.fail(battle.active(player), .None);
1569
+
1342
1570
  var n: u4 = 0;
1343
1571
  var i: u4 = 0;
1344
1572
  for (foe.pokemon, 0..) |pokemon, j| {
@@ -1351,168 +1579,390 @@ pub const Effects = struct {
1351
1579
  }
1352
1580
  }
1353
1581
 
1354
- if (n == 0) {
1355
- try options.log.fail(battle.active(player), .None);
1356
- return;
1357
- }
1582
+ if (n == 0) return options.log.fail(battle.active(player), .None);
1358
1583
 
1359
- _ = try Rolls.forceSwitch(battle, player, SLOTS[0..i], n, options); // TODO
1584
+ const slot = try Rolls.forceSwitch(battle, player, SLOTS[0..i], n, options);
1585
+ try switchIn(battle, player.foe(), slot, .Drag, options);
1360
1586
  }
1361
1587
 
1362
- fn foresight(battle: anytype, player: Player, _: *State, options: anytype) !void {
1588
+ pub fn foresight(battle: anytype, player: Player, _: *State, options: anytype) !void {
1363
1589
  var foe = battle.foe(player);
1364
1590
 
1365
1591
  if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
1366
1592
  try options.log.lastmiss();
1367
- try options.log.miss(battle.active(player));
1368
- return;
1593
+ return options.log.miss(battle.active(player));
1369
1594
  } else if (foe.active.volatiles.Foresight) {
1370
- try options.log.fail(battle.active(player), .None);
1371
- return;
1595
+ return options.log.fail(battle.active(player), .None);
1372
1596
  }
1373
1597
 
1374
1598
  foe.active.volatiles.Foresight = true;
1375
1599
  try options.log.start(battle.active(player.foe()), .FocusEnergy);
1376
1600
  }
1377
1601
 
1378
- fn freezeChance(battle: anytype, player: Player, state: *State, options: anytype) !void {
1379
- _ = .{ battle, player, state, options }; // TODO
1602
+ pub fn freezeChance(battle: anytype, player: Player, state: *State, options: anytype) !void {
1603
+ var foe = battle.foe(player);
1604
+ var foe_stored = foe.stored();
1605
+ const foe_ident = battle.active(player.foe());
1606
+
1607
+ if (foe.active.volatiles.Substitute) return;
1608
+ if (Status.any(foe_stored.status)) return;
1609
+
1610
+ // Nothing is immune to Ice-type, but Ghosts are immune to freezeChance called by Tri Attack
1611
+ if (state.immune()) return;
1612
+ if (battle.field.weather == .Sun) return;
1613
+ if (foe.active.types.includes(Move.get(state.move).type) and state.move != .TriAttack) {
1614
+ return;
1615
+ }
1616
+ if (!state.proc) return;
1617
+ if (foe.conditions.Safeguard) return;
1618
+ // Freeze Clause Mod
1619
+ if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1620
+
1621
+ foe_stored.status = Status.init(.FRZ);
1622
+ try options.log.status(foe_ident, foe_stored.status, .None);
1623
+
1624
+ // TODO: check for status berry
1625
+
1626
+ foe.active.volatiles.Recharging = false;
1627
+ foe.active.volatiles.frozen = true;
1380
1628
  }
1381
1629
 
1382
- fn furyCutter(battle: anytype, player: Player, state: *State, options: anytype) !void {
1630
+ pub fn furyCutter(battle: anytype, player: Player, state: *State, options: anytype) !void {
1383
1631
  _ = .{ battle, player, state, options }; // TODO
1384
1632
  }
1385
1633
 
1386
- fn futureSight(battle: anytype, player: Player, state: *State, options: anytype) !void {
1634
+ pub fn futureSight(battle: anytype, player: Player, state: *State, options: anytype) !void {
1387
1635
  _ = .{ battle, player, state, options }; // TODO
1388
1636
  }
1389
1637
 
1390
- fn happiness(battle: anytype, player: Player, state: *State, _: anytype) !void {
1638
+ pub fn happiness(battle: anytype, player: Player, state: *State, _: anytype) !void {
1391
1639
  const val = battle.side(player).stored().happiness;
1392
1640
  const frustration = Move.get(state.move).effect == .Frustration;
1393
1641
  state.bp = ((if (frustration) 255 - val else val) * 10) / 25;
1394
1642
  }
1395
1643
 
1396
- fn haze(battle: anytype, player: Player, state: *State, options: anytype) !void {
1644
+ pub fn haze(battle: anytype, player: Player, state: *State, options: anytype) !void {
1397
1645
  _ = .{ battle, player, state, options }; // TODO
1398
1646
  }
1399
1647
 
1400
- fn heal(battle: anytype, player: Player, state: *State, options: anytype) !void {
1648
+ pub fn heal(battle: anytype, player: Player, state: *State, options: anytype) !void {
1401
1649
  _ = .{ battle, player, state, options }; // TODO
1402
1650
  }
1403
1651
 
1404
- fn healBell(battle: anytype, player: Player, state: *State, options: anytype) !void {
1652
+ pub fn healBell(battle: anytype, player: Player, state: *State, options: anytype) !void {
1405
1653
  _ = .{ battle, player, state, options }; // TODO
1406
1654
  }
1407
1655
 
1408
- fn hiddenPower(battle: anytype, player: Player, state: *State, options: anytype) !void {
1656
+ pub fn hiddenPower(battle: anytype, player: Player, state: *State, options: anytype) !void {
1409
1657
  _ = .{ battle, player, state, options }; // TODO
1410
1658
  }
1411
1659
 
1412
- fn hyperBeam(battle: anytype, player: Player, state: *State, options: anytype) !void {
1660
+ pub fn hyperBeam(battle: anytype, player: Player, state: *State, options: anytype) !void {
1413
1661
  _ = .{ battle, player, state, options }; // TODO
1414
1662
  }
1415
1663
 
1416
- fn jumpKick(battle: anytype, player: Player, state: *State, options: anytype) !void {
1664
+ pub fn leechSeed(battle: anytype, player: Player, state: *State, options: anytype) !void {
1417
1665
  _ = .{ battle, player, state, options }; // TODO
1418
1666
  }
1419
1667
 
1420
- fn leechSeed(battle: anytype, player: Player, state: *State, options: anytype) !void {
1668
+ pub fn lockOn(battle: anytype, player: Player, state: *State, options: anytype) !void {
1421
1669
  _ = .{ battle, player, state, options }; // TODO
1422
1670
  }
1423
1671
 
1424
- fn levelDamage(battle: anytype, player: Player, state: *State, options: anytype) !void {
1672
+ const MAGNITUDE_POWER = [_]u8{ 10, 30, 50, 70, 90, 110, 150 };
1673
+
1674
+ pub fn magnitude(battle: anytype, player: Player, state: *State, options: anytype) !void {
1425
1675
  _ = .{ battle, player, state, options }; // TODO
1426
1676
  }
1427
1677
 
1428
- fn lockOn(battle: anytype, player: Player, state: *State, options: anytype) !void {
1678
+ pub fn meanLook(battle: anytype, player: Player, state: *State, options: anytype) !void {
1429
1679
  _ = .{ battle, player, state, options }; // TODO
1430
1680
  }
1431
1681
 
1432
- const MAGNITUDE_POWER = [_]u8{ 10, 30, 50, 70, 90, 110, 150 };
1682
+ pub fn metronome(
1683
+ battle: anytype,
1684
+ player: Player,
1685
+ state: *State,
1686
+ options: anytype,
1687
+ ) anyerror!void {
1688
+ var side = battle.side(player);
1433
1689
 
1434
- fn magnitude(battle: anytype, player: Player, state: *State, options: anytype) !void {
1435
- _ = .{ battle, player, state, options }; // TODO
1436
- }
1690
+ side.last_used_move = .None;
1691
+ side.active.volatiles.dirty = false;
1437
1692
 
1438
- fn meanLook(battle: anytype, player: Player, state: *State, options: anytype) !void {
1439
- _ = .{ battle, player, state, options }; // TODO
1440
- }
1693
+ // if (wPlayerCharging) ???
1694
+ // wPlayerCharging = 1
1441
1695
 
1442
- fn metronome(battle: anytype, player: Player, state: *State, options: anytype) !void {
1443
- _ = .{ battle, player, state, options }; // TODO
1696
+ // FIXME
1697
+ state.move = try Rolls.metronome(battle, player, options);
1698
+ state.from = .Metronome;
1699
+ return generated.runMove(battle, player, state, options);
1444
1700
  }
1445
1701
 
1446
- fn mimic(battle: anytype, player: Player, state: *State, options: anytype) !void {
1702
+ pub fn mimic(battle: anytype, player: Player, state: *State, options: anytype) !void {
1447
1703
  _ = .{ battle, player, state, options }; // TODO
1448
1704
  }
1449
1705
 
1450
- fn mirrorCoat(battle: anytype, player: Player, state: *State, options: anytype) !void {
1706
+ pub fn mirrorCoat(battle: anytype, player: Player, state: *State, options: anytype) !void {
1451
1707
  _ = .{ battle, player, state, options }; // TODO
1452
1708
  }
1453
1709
 
1454
- fn mirrorMove(battle: anytype, player: Player, state: *State, options: anytype) !void {
1455
- _ = .{ battle, player, state, options }; // TODO
1710
+ pub fn mirrorMove(
1711
+ battle: anytype,
1712
+ player: Player,
1713
+ state: *State,
1714
+ options: anytype,
1715
+ ) anyerror!void {
1716
+ var side = battle.side(player);
1717
+ const foe = battle.foe(player);
1718
+
1719
+ side.last_used_move = .None;
1720
+ side.active.volatiles.dirty = false;
1721
+
1722
+ const last = foe.lastMove(false);
1723
+
1724
+ if (last == .None) return options.log.fail(battle.active(player), .None);
1725
+ const knows = knows: for (side.active.moves) |m| {
1726
+ if (last == m.id) break :knows true;
1727
+ } else false;
1728
+ if (knows) return options.log.fail(battle.active(player), .None);
1729
+
1730
+ // if (wPlayerCharging) ???
1731
+ // wPlayerCharging = 1
1732
+
1733
+ // FIXME
1734
+ state.move = last;
1735
+ state.from = .MirrorMove;
1736
+ return generated.runMove(battle, player, state, options);
1456
1737
  }
1457
1738
 
1458
- fn mist(battle: anytype, player: Player, state: *State, options: anytype) !void {
1459
- _ = .{ battle, player, state, options }; // TODO
1739
+ pub fn mist(battle: anytype, player: Player, _: *State, options: anytype) !void {
1740
+ var side = battle.side(player);
1741
+
1742
+ if (side.active.volatiles.Mist) return options.log.fail(battle.active(player), .None);
1743
+ side.active.volatiles.Mist = true;
1744
+
1745
+ try options.log.start(battle.active(player), .Mist);
1460
1746
  }
1461
1747
 
1462
- fn weatherHeal(battle: anytype, player: Player, state: *State, options: anytype) !void {
1463
- _ = .{ battle, player, state, options }; // TODO
1748
+ pub fn weatherHeal(battle: anytype, player: Player, _: *State, options: anytype) !void {
1749
+ var stored = battle.side(player).stored();
1750
+ const ident = battle.active(player);
1751
+
1752
+ if (stored.hp == stored.stats.hp) return options.log.fail(ident, .None);
1753
+
1754
+ stored.hp = switch (battle.field.weather) {
1755
+ .Sun => stored.stats.hp,
1756
+ .None => @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2)),
1757
+ else => @min(stored.stats.hp, stored.hp + (stored.stats.hp / 4)),
1758
+ };
1759
+
1760
+ try options.log.heal(ident, stored, Heal.None);
1464
1761
  }
1465
1762
 
1466
- fn multiHit(battle: anytype, player: Player, state: *State, options: anytype) !void {
1763
+ pub fn multiHit(battle: anytype, player: Player, state: *State, options: anytype) !void {
1467
1764
  _ = .{ battle, player, state, options }; // TODO
1468
1765
  }
1469
1766
 
1470
- fn nightmare(battle: anytype, player: Player, state: *State, options: anytype) !void {
1471
- _ = .{ battle, player, state, options }; // TODO
1767
+ pub fn nightmare(battle: anytype, player: Player, state: *State, options: anytype) !void {
1768
+ var foe = battle.foe(player);
1769
+ var foe_volatiles = &foe.active.volatiles;
1770
+ const foe_ident = battle.active(player.foe());
1771
+
1772
+ if (foe_volatiles.Flying or foe_volatiles.Underground) {
1773
+ if (!showdown) return options.log.fail(battle.active(player.foe()), .None);
1774
+ try options.log.lastmiss();
1775
+ return options.log.miss(battle.active(player));
1776
+ } else if (foe_volatiles.Substitute) {
1777
+ return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
1778
+ } else if (!Status.is(foe.stored().status, .SLP) or foe_volatiles.Nightmare) {
1779
+ return options.log.fail(foe_ident, .None);
1780
+ }
1781
+
1782
+ foe_volatiles.Nightmare = true;
1783
+ try options.log.start(battle.active(player.foe()), .Nightmare);
1472
1784
  }
1473
1785
 
1474
- fn ohko(battle: anytype, player: Player, state: *State, options: anytype) !void {
1475
- _ = .{ battle, player, state, options }; // TODO
1786
+ pub const ohko = struct {
1787
+ // OHKO calls adjustDamage but only actually cares about immunity
1788
+ pub fn immune(battle: anytype, player: Player, state: *State, _: anytype) !void {
1789
+ const move = Move.get(state.move);
1790
+
1791
+ assert(move.type == .Normal or move.type == .Ground);
1792
+ const foe = battle.foe(player);
1793
+ const zero = if (move.type == .Normal)
1794
+ foe.active.types.includes(.Ghost) and !foe.active.volatiles.Foresight
1795
+ else
1796
+ foe.active.types.includes(.Flying);
1797
+ state.effectiveness = if (zero) 0 else Effectiveness.neutral;
1798
+ }
1799
+
1800
+ pub fn damage(battle: anytype, player: Player, state: *State, options: anytype) !void {
1801
+ const side = battle.side(player);
1802
+ const foe = battle.foe(player);
1803
+
1804
+ const foe_ident = battle.active(player.foe());
1805
+
1806
+ state.damage = 0;
1807
+ if (state.immune()) return options.log.immune(foe_ident, .None);
1808
+ const delta = side.stored().level -| foe.stored().level;
1809
+ if (delta == 0) return options.log.immune(foe_ident, .OHKO);
1810
+
1811
+ state.accuracy = 2 * delta +| Gen12.percent(30);
1812
+ try checkHit(battle, player, state, options);
1813
+ if (!state.miss) state.damage = 65535;
1814
+ state.ohko = true;
1815
+ }
1816
+ };
1817
+
1818
+ pub fn painSplit(battle: anytype, player: Player, state: *State, options: anytype) !void {
1819
+ var side = battle.side(player);
1820
+ var foe = battle.foe(player);
1821
+
1822
+ const ident = battle.active(player);
1823
+ const foe_ident = battle.active(player.foe());
1824
+
1825
+ if (state.miss) return options.log.fail(foe_ident, .None);
1826
+ if (foe.active.volatiles.Substitute) {
1827
+ return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
1828
+ }
1829
+
1830
+ const avg = @max((foe.stored().hp + side.stored().hp) / 2, 1);
1831
+ foe.stored().hp -= avg;
1832
+ side.stored().hp = avg;
1833
+
1834
+ try options.log.sethp(foe_ident, foe.stored(), .Silent);
1835
+ try options.log.sethp(ident, side.stored(), .None);
1476
1836
  }
1477
1837
 
1478
- fn painSplit(battle: anytype, player: Player, state: *State, options: anytype) !void {
1479
- _ = .{ battle, player, state, options }; // TODO
1838
+ pub fn paralyze(battle: anytype, player: Player, state: *State, options: anytype) !void {
1839
+ var foe = battle.foe(player);
1840
+ const foe_ident = battle.active(player.foe());
1841
+
1842
+ if (Status.is(foe.stored().status, .PAR)) return options.log.fail(foe_ident, .Paralysis);
1843
+ if (state.immune()) return options.log.immune(foe_ident, .None);
1844
+ if (Status.any(foe.stored().status)) return options.log.fail(foe_ident, .None);
1845
+ if (state.miss) {
1846
+ try options.log.lastmiss();
1847
+ return options.log.miss(battle.active(player));
1848
+ } else if (foe.active.volatiles.Substitute) {
1849
+ return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
1850
+ }
1851
+
1852
+ foe.stored().status = Status.init(.PAR);
1853
+ foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
1854
+
1855
+ try options.log.status(foe_ident, foe.stored().status, .None);
1480
1856
  }
1481
1857
 
1482
- fn paralyze(battle: anytype, player: Player, state: *State, options: anytype) !void {
1483
- _ = .{ battle, player, state, options }; // TODO
1858
+ pub fn paralyzeChance(battle: anytype, player: Player, state: *State, options: anytype) !void {
1859
+ var foe = battle.foe(player);
1860
+ var foe_stored = foe.stored();
1861
+
1862
+ if (foe.active.volatiles.Substitute) return;
1863
+ if (Status.any(foe_stored.status)) return;
1864
+ if (state.immune()) return;
1865
+ if (!state.proc) return;
1866
+ if (foe.conditions.Safeguard) return;
1867
+
1868
+ foe_stored.status = Status.init(.PAR);
1869
+ foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
1870
+ try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
1871
+
1872
+ // TODO: check for status berry
1484
1873
  }
1485
1874
 
1486
- fn paralyzeChance(battle: anytype, player: Player, state: *State, options: anytype) !void {
1487
- // TODO: Thunder does acc/checkHit/effectChance before stab + damage variation
1488
- _ = .{ battle, player, state, options }; // TODO
1875
+ pub fn payDay(battle: anytype, player: Player, _: *State, options: anytype) !void {
1876
+ if (!showdown or !battle.foe(player).active.volatiles.Substitute) {
1877
+ try options.log.fieldactivate(); // FIXME .PayDay
1878
+ }
1489
1879
  }
1490
1880
 
1491
- fn payDay(battle: anytype, player: Player, state: *State, options: anytype) !void {
1492
- _ = .{ battle, player, state, options }; // TODO
1881
+ pub fn perishSong(battle: anytype, player: Player, _: *State, options: anytype) !void {
1882
+ var side = battle.side(player);
1883
+ var foe = battle.foe(player);
1884
+
1885
+ if (foe.active.volatiles.PerishSong) {
1886
+ return options.log.fail(battle.active(player.foe()), .None);
1887
+ } else if (!side.active.volatiles.PerishSong) {
1888
+ side.active.volatiles.PerishSong = true;
1889
+ side.active.volatiles.perish_song = 4;
1890
+ try options.log.start(battle.active(player), .PerishSong3Silent);
1891
+ }
1892
+ foe.active.volatiles.PerishSong = true;
1893
+ foe.active.volatiles.perish_song = 4;
1894
+ try options.log.start(battle.active(player.foe()), .PerishSong3Silent);
1895
+
1896
+ try options.log.fieldactivate(); // FIXME .PerishSong
1493
1897
  }
1494
1898
 
1495
- fn perishSong(battle: anytype, player: Player, state: *State, options: anytype) !void {
1496
- _ = .{ battle, player, state, options }; // TODO
1899
+ pub fn poison(battle: anytype, player: Player, state: *State, options: anytype) !void {
1900
+ var foe = battle.foe(player);
1901
+ var foe_stored = foe.stored();
1902
+ const foe_ident = battle.active(player.foe());
1903
+
1904
+ const toxic = state.move == .Toxic;
1905
+ if (state.immune() or foe.active.types.includes(.Poison)) {
1906
+ return options.log.immune(foe_ident, .None);
1907
+ } else if (Status.any(foe_stored.status)) {
1908
+ // Pokémon Showdown considers Toxic to be a status even in Generation II and so
1909
+ // will not include a fail reason for Toxic vs. Poison or vice-versa...
1910
+ return options.log.fail(foe_ident, if (Status.is(foe_stored.status, .PSN))
1911
+ if (!showdown)
1912
+ .Poison
1913
+ else if (toxic == (foe_stored.status == Status.TOX))
1914
+ if (toxic) .Toxic else .Poison
1915
+ else
1916
+ .None
1917
+ else
1918
+ .None);
1919
+ } else if (foe.active.volatiles.Substitute) {
1920
+ return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
1921
+ } else if (state.miss) {
1922
+ try options.log.lastmiss();
1923
+ return options.log.miss(battle.active(player));
1924
+ }
1925
+
1926
+ foe_stored.status = Status.init(.PSN);
1927
+ if (toxic) {
1928
+ if (showdown) foe_stored.status = Status.TOX;
1929
+ foe.active.volatiles.Toxic = true;
1930
+ foe.active.volatiles.toxic = 0;
1931
+ }
1932
+
1933
+ try options.log.status(foe_ident, foe_stored.status, .None);
1934
+
1935
+ // TODO: check for status berry
1497
1936
  }
1498
1937
 
1499
- fn poison(battle: anytype, player: Player, state: *State, options: anytype) !void {
1500
- _ = .{ battle, player, state, options }; // TODO
1938
+ pub fn poisonChance(battle: anytype, player: Player, state: *State, options: anytype) !void {
1939
+ var foe = battle.foe(player);
1940
+ var foe_stored = foe.stored();
1941
+
1942
+ if (foe.active.volatiles.Substitute) return;
1943
+ if (Status.any(foe_stored.status)) return if (showdown) battle.rng.advance(1);
1944
+ if (state.immune() or foe.active.types.includes(.Poison)) return;
1945
+ if (!state.proc) return;
1946
+ if (foe.conditions.Safeguard) return;
1947
+
1948
+ foe_stored.status = Status.init(.PSN);
1949
+ try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
1950
+
1951
+ // TODO: check for status berry
1501
1952
  }
1502
1953
 
1503
- fn present(battle: anytype, player: Player, state: *State, options: anytype) !void {
1954
+ pub fn present(battle: anytype, player: Player, state: *State, options: anytype) !void {
1504
1955
  _ = .{ battle, player, state, options }; // TODO
1505
1956
  }
1506
1957
 
1507
- fn protect(battle: anytype, player: Player, state: *State, options: anytype) !void {
1958
+ pub fn protect(battle: anytype, player: Player, state: *State, options: anytype) !void {
1508
1959
  var side = battle.side(player);
1509
1960
  var volatiles = &side.active.volatiles;
1510
1961
 
1511
1962
  const failed = !state.first or volatiles.Substitute or
1512
1963
  !try Rolls.protect(battle, player, volatiles.protect, options);
1513
1964
  if (failed) {
1514
- try options.log.fail(battle.active(player), .None);
1515
- return;
1965
+ return options.log.fail(battle.active(player), .None);
1516
1966
  }
1517
1967
 
1518
1968
  volatiles.protect = @max(8, volatiles.protect + 1);
@@ -1524,75 +1974,213 @@ pub const Effects = struct {
1524
1974
  try options.log.singleturn(battle.active(player), state.move);
1525
1975
  }
1526
1976
 
1527
- fn psychUp(battle: anytype, player: Player, state: *State, options: anytype) !void {
1528
- _ = .{ battle, player, state, options }; // TODO
1529
- }
1977
+ pub fn psychUp(battle: anytype, player: Player, _: *State, options: anytype) !void {
1978
+ var side = battle.side(player);
1979
+ const foe = battle.foe(player);
1530
1980
 
1531
- fn pursuit(battle: anytype, player: Player, state: *State, options: anytype) !void {
1532
- _ = .{ battle, player, state, options }; // TODO
1533
- }
1981
+ // Pokémon Showdown should fail when the enemy has no boosts/unboosts but doesn't
1982
+ if (!showdown and @as(u32, @bitCast(foe.active.boosts)) == 0) {
1983
+ return options.log.fail(battle.active(player), .None);
1984
+ }
1534
1985
 
1535
- fn rage(battle: anytype, player: Player, state: *State, options: anytype) !void {
1536
- _ = .{ battle, player, state, options }; // TODO
1986
+ side.active.boosts = foe.active.boosts;
1987
+ try options.log.copyboost(battle.active(player), battle.active(player.foe()));
1537
1988
  }
1538
1989
 
1539
- fn rainDance(battle: anytype, player: Player, state: *State, options: anytype) !void {
1540
- _ = .{ battle, player, state, options }; // TODO
1990
+ pub fn pursuit(battle: anytype, player: Player, state: *State, _: anytype) !void {
1991
+ // TODO: remove switching variable and just look at Choice saved in Battle struct?
1992
+ if (battle.foe(player).active.volatiles.switching) state.damage *|= 2;
1541
1993
  }
1542
1994
 
1543
- fn rapidSpin(battle: anytype, player: Player, state: *State, options: anytype) !void {
1544
- _ = .{ battle, player, state, options }; // TODO
1545
- }
1995
+ pub const rage = struct {
1996
+ pub fn start(battle: anytype, player: Player, _: *State, options: anytype) !void {
1997
+ battle.side(player).active.volatiles.Rage = true;
1998
+ try options.log.singlemove(battle.active(player), Move.Rage);
1999
+ }
1546
2000
 
1547
- fn razorWind(battle: anytype, player: Player, state: *State, options: anytype) !void {
1548
- _ = .{ battle, player, state, options }; // TODO
1549
- }
2001
+ pub fn damage(battle: anytype, player: Player, state: *State, _: anytype) !void {
2002
+ var side = battle.side(player);
2003
+ var volatiles = &side.active.volatiles;
1550
2004
 
1551
- fn recoil(battle: anytype, player: Player, state: *State, options: anytype) !void {
1552
- _ = .{ battle, player, state, options }; // TODO
2005
+ assert(volatiles.Rage);
2006
+ state.damage *|= (volatiles.rage +| 1);
2007
+ }
2008
+ };
2009
+
2010
+ pub fn rainDance(battle: anytype, _: Player, _: *State, options: anytype) !void {
2011
+ return weather(battle, .Rain, options);
1553
2012
  }
1554
2013
 
1555
- fn reversal(battle: anytype, player: Player, state: *State, options: anytype) !void {
1556
- _ = .{ battle, player, state, options }; // TODO
2014
+ pub fn rapidSpin(battle: anytype, player: Player, _: *State, options: anytype) !void {
2015
+ var side = battle.side(player);
2016
+
2017
+ if (side.active.volatiles.LeechSeed) {
2018
+ try options.log.end(battle.active(player), .LeechSeedFrom);
2019
+ side.active.volatiles.LeechSeed = false;
2020
+ }
2021
+
2022
+ if (side.conditions.Spikes) {
2023
+ try options.log.spikesend(battle.active(player));
2024
+ side.conditions.Spikes = false;
2025
+ }
2026
+
2027
+ if (side.active.volatiles.bind.duration > 0) {
2028
+ const reason = @intFromEnum(End.Bind) + side.active.volatiles.bind.reason - 1;
2029
+ try options.log.end(battle.active(player), @enumFromInt(reason));
2030
+ side.active.volatiles.bind = .{};
2031
+ }
1557
2032
  }
1558
2033
 
1559
- fn rollout(battle: anytype, player: Player, state: *State, options: anytype) !void {
1560
- _ = .{ battle, player, state, options }; // TODO
2034
+ pub fn recoil(battle: anytype, player: Player, state: *State, options: anytype) !void {
2035
+ var side = battle.side(player);
2036
+ var stored = side.stored();
2037
+
2038
+ assert(state.damage > 0);
2039
+
2040
+ const damage: i16 = @intCast(@max(state.damage / 4, 1));
2041
+ stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
2042
+
2043
+ try options.log.damageOf(
2044
+ battle.active(player),
2045
+ stored,
2046
+ .RecoilOf,
2047
+ battle.active(player.foe()),
2048
+ );
1561
2049
  }
1562
2050
 
1563
- fn sacredFire(battle: anytype, player: Player, state: *State, options: anytype) !void {
1564
- _ = .{ battle, player, state, options }; // TODO
2051
+ pub fn reversal(battle: anytype, player: Player, state: *State, options: anytype) !void {
2052
+ const stored = battle.side(player).stored();
2053
+
2054
+ // Pokémon Showdown Gen II implementation incorrectly inherits Gen IV's table instead
2055
+ state.bp = if (showdown) power: {
2056
+ const x = @max(1, stored.hp * 64 / stored.stats.hp);
2057
+ if (x < 2) break :power 200;
2058
+ if (x < 6) break :power 150;
2059
+ if (x < 13) break :power 100;
2060
+ if (x < 22) break :power 80;
2061
+ if (x < 43) break :power 40;
2062
+ break :power 20;
2063
+ } else power: {
2064
+ const x = @max(1, stored.hp * 48 / stored.stats.hp);
2065
+ if (x < 1) break :power 200;
2066
+ if (x < 4) break :power 150;
2067
+ if (x < 9) break :power 100;
2068
+ if (x < 16) break :power 80;
2069
+ if (x < 32) break :power 40;
2070
+ break :power 20;
2071
+ };
2072
+
2073
+ try calcDamage(battle, player, state, options);
1565
2074
  }
1566
2075
 
1567
- fn safeguard(battle: anytype, player: Player, state: *State, options: anytype) !void {
1568
- _ = .{ battle, player, state, options }; // TODO
2076
+ pub const rollout = struct {
2077
+ pub fn check(battle: anytype, player: Player, _: *State, _: anytype) !void {
2078
+ var side = battle.side(player);
2079
+
2080
+ if (!side.active.volatiles.Rollout) {
2081
+ side.active.volatiles.attacks = 0;
2082
+ } else {
2083
+ // TODO SkipToBattleCommand(doturn_command)
2084
+ }
2085
+ }
2086
+
2087
+ pub fn power(battle: anytype, player: Player, state: *State, _: anytype) !void {
2088
+ var side = battle.side(player);
2089
+
2090
+ if (Status.is(side.stored().status, .SLP)) return;
2091
+
2092
+ if (side.active.volatiles.attacks > 0) {
2093
+ // TODO set wSomeoneIsRampaging
2094
+ }
2095
+
2096
+ if (state.miss) {
2097
+ side.active.volatiles.Rollout = false;
2098
+ return;
2099
+ }
2100
+
2101
+ assert(side.active.volatiles.attacks < 5);
2102
+ side.active.volatiles.attacks += 1;
2103
+ side.active.volatiles.Rollout = side.active.volatiles.attacks != 5;
2104
+
2105
+ if (side.active.volatiles.DefenseCurl) state.damage *|= 2;
2106
+ }
2107
+ };
2108
+
2109
+ pub fn sandstorm(battle: anytype, player: Player, _: *State, options: anytype) !void {
2110
+ if (battle.field.weather != .Sandstorm) return weather(battle, .Sandstorm, options);
2111
+ return options.log.fail(battle.active(player), .None);
1569
2112
  }
1570
2113
 
1571
- fn sandstorm(battle: anytype, player: Player, state: *State, options: anytype) !void {
1572
- _ = .{ battle, player, state, options }; // TODO
2114
+ pub fn safeguard(battle: anytype, player: Player, _: *State, options: anytype) !void {
2115
+ return screens(battle, player, "Safeguard", "safeguard", options);
1573
2116
  }
1574
2117
 
1575
- fn screens(battle: anytype, player: Player, state: *State, options: anytype) !void {
1576
- _ = .{ battle, player, state, options }; // TODO
2118
+ pub fn reflect(battle: anytype, player: Player, _: *State, options: anytype) !void {
2119
+ return screens(battle, player, "Reflect", "reflect", options);
1577
2120
  }
1578
2121
 
1579
- fn sketch(battle: anytype, player: Player, state: *State, options: anytype) !void {
1580
- _ = .{ battle, player, state, options }; // TODO
2122
+ pub fn lightScreen(battle: anytype, player: Player, _: *State, options: anytype) !void {
2123
+ return screens(battle, player, "LightScreen", "light_screen", options);
1581
2124
  }
1582
2125
 
1583
- fn skullBash(battle: anytype, player: Player, state: *State, options: anytype) !void {
1584
- _ = .{ battle, player, state, options }; // TODO
2126
+ fn screens(
2127
+ battle: anytype,
2128
+ player: Player,
2129
+ comptime key: []const u8,
2130
+ comptime value: []const u8,
2131
+ options: anytype,
2132
+ ) !void {
2133
+ var side = battle.side(player);
2134
+
2135
+ if (@field(side.conditions, key)) return options.log.fail(battle.active(player), .None);
2136
+
2137
+ @field(side.conditions, key) = true;
2138
+ @field(side.conditions, value) = 5;
2139
+ try options.log.sidestart(player, @field(protocol.Side, key));
1585
2140
  }
1586
2141
 
1587
- fn skyAttack(battle: anytype, player: Player, state: *State, options: anytype) !void {
1588
- _ = .{ battle, player, state, options }; // TODO
2142
+ pub fn sketch(battle: anytype, player: Player, _: *State, options: anytype) !void {
2143
+ var side = battle.side(player);
2144
+
2145
+ side.last_used_move = .None;
2146
+ side.active.volatiles.dirty = false;
2147
+
2148
+ if (showdown) {
2149
+ // yes, actually "Splash". '|nothing' gets blindly mapped to '|-activate||move:Splash'
2150
+ try options.log.activate(battle.active(player), .Splash);
2151
+ } else {
2152
+ try options.log.fail(battle.active(player), .None);
2153
+ }
1589
2154
  }
1590
2155
 
1591
- fn sleep(battle: anytype, player: Player, state: *State, options: anytype) !void {
1592
- _ = .{ battle, player, state, options }; // TODO
2156
+ pub fn sleep(battle: anytype, player: Player, state: *State, options: anytype) !void {
2157
+ var foe = battle.foe(player);
2158
+ var foe_stored = foe.stored();
2159
+ const foe_ident = battle.active(player.foe());
2160
+
2161
+ if (Status.any(foe_stored.status)) {
2162
+ return options.log.fail(
2163
+ foe_ident,
2164
+ if (Status.is(foe_stored.status, .SLP)) .Sleep else .None,
2165
+ );
2166
+ }
2167
+
2168
+ if (foe.active.volatiles.Substitute) {
2169
+ return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
2170
+ }
2171
+
2172
+ // Sleep Clause Mod
2173
+ if (showdown) {
2174
+ for (foe.pokemon) |p| {
2175
+ if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
2176
+ }
2177
+ }
2178
+
2179
+ foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2180
+ try options.log.statusFrom(foe_ident, foe_stored.status, state.move);
1593
2181
  }
1594
2182
 
1595
- fn sleepTalk(battle: anytype, player: Player, state: *State, options: anytype) !void {
2183
+ pub fn sleepTalk(battle: anytype, player: Player, state: *State, options: anytype) !void {
1596
2184
  const side = battle.side(player);
1597
2185
 
1598
2186
  if (!Status.is(side.stored().status, .SLP)) {
@@ -1604,91 +2192,372 @@ pub const Effects = struct {
1604
2192
  for (side.active.moves, 0..) |m, i| {
1605
2193
  if (state.mslot - 1 != i and Move.get(m.id).extra.sleep_talk) n += 1;
1606
2194
  }
1607
- if (n == 0) {
1608
- try options.log.fail(battle.active(player), .None);
1609
- return;
1610
- }
2195
+ if (n == 0) return options.log.fail(battle.active(player), .None);
1611
2196
 
1612
2197
  _ = try Rolls.sleepTalk(battle, player, n, state.mslot, options); // TODO
1613
2198
  }
1614
2199
 
1615
- fn snore(battle: anytype, player: Player, state: *State, options: anytype) !void {
1616
- _ = .{ battle, player, state, options }; // TODO
2200
+ pub fn snore(battle: anytype, player: Player, state: *State, options: anytype) !bool {
2201
+ if (Status.is(battle.side(player).stored().status, .SLP)) return true;
2202
+ state.damage = 0;
2203
+ state.miss = true; // TODO is this necessary?
2204
+ if (!showdown) try options.log.fail(battle.active(player), .None);
2205
+ return false;
1617
2206
  }
1618
2207
 
1619
- fn solarBeam(battle: anytype, player: Player, state: *State, options: anytype) !void {
1620
- _ = .{ battle, player, state, options }; // TODO
1621
- }
2208
+ pub fn spikes(battle: anytype, player: Player, _: *State, options: anytype) !void {
2209
+ var foe = battle.foe(player);
2210
+ if (foe.conditions.Spikes) return options.log.fail(battle.active(player), .None);
1622
2211
 
1623
- fn spikes(battle: anytype, player: Player, state: *State, options: anytype) !void {
1624
- _ = .{ battle, player, state, options }; // TODO
2212
+ foe.conditions.Spikes = true;
2213
+ try options.log.sidestart(player.foe(), .Spikes);
1625
2214
  }
1626
2215
 
1627
- fn spite(battle: anytype, player: Player, state: *State, options: anytype) !void {
2216
+ pub fn spite(battle: anytype, player: Player, state: *State, options: anytype) !void {
1628
2217
  _ = .{ battle, player, state, options }; // TODO
1629
- }
2218
+ // var foe = battle.foe(player);
1630
2219
 
1631
- fn splash(battle: anytype, player: Player, state: *State, options: anytype) !void {
1632
- _ = .{ battle, player, state, options }; // TODO
1633
- }
2220
+ // if (state.miss) return options.log.fail(battle.active(player), .None);
1634
2221
 
1635
- fn stomp(battle: anytype, player: Player, state: *State, options: anytype) !void {
1636
- _ = .{ battle, player, state, options }; // TODO
2222
+ // const last = foe.lastMove(false);
2223
+ // if (showdown) {
2224
+ // const roll = try Rolls.spite(battle, player, options);
2225
+ // } else {
2226
+ // if (last != .None) {}
2227
+ // }
1637
2228
  }
1638
2229
 
1639
- fn substitute(battle: anytype, player: Player, state: *State, options: anytype) !void {
1640
- _ = .{ battle, player, state, options }; // TODO
2230
+ pub fn splash(battle: anytype, player: Player, _: *State, options: anytype) !void {
2231
+ try options.log.activate(battle.active(player), .Splash);
1641
2232
  }
1642
2233
 
1643
- fn sunnyDay(battle: anytype, player: Player, state: *State, options: anytype) !void {
1644
- _ = .{ battle, player, state, options }; // TODO
2234
+ pub fn substitute(battle: anytype, player: Player, _: *State, options: anytype) !void {
2235
+ var side = battle.side(player);
2236
+ if (side.active.volatiles.Substitute) {
2237
+ return options.log.fail(battle.active(player), .Substitute);
2238
+ }
2239
+
2240
+ assert(side.stored().stats.hp <= 1023);
2241
+ const hp: u8 = @intCast(side.stored().stats.hp / 4);
2242
+
2243
+ if (hp == 0 or side.stored().hp <= hp) {
2244
+ return options.log.fail(battle.active(player), .Weak);
2245
+ }
2246
+
2247
+ side.stored().hp -= hp;
2248
+ side.active.volatiles.substitute = hp;
2249
+ side.active.volatiles.Substitute = true;
2250
+ side.active.volatiles.bind = .{};
2251
+ try options.log.start(battle.active(player), .Substitute);
2252
+ try options.log.damage(battle.active(player), side.stored(), .None);
1645
2253
  }
1646
2254
 
1647
- fn swagger(battle: anytype, player: Player, state: *State, options: anytype) !void {
1648
- _ = .{ battle, player, state, options }; // TODO
2255
+ pub fn sunnyDay(battle: anytype, _: Player, _: *State, options: anytype) !void {
2256
+ return weather(battle, .Sun, options);
1649
2257
  }
1650
2258
 
1651
- fn synthesis(battle: anytype, player: Player, state: *State, options: anytype) !void {
1652
- _ = .{ battle, player, state, options }; // TODO
2259
+ fn weather(battle: anytype, w: Weather, options: anytype) !void {
2260
+ battle.field.weather = w;
2261
+ battle.field.weather_duration = 5;
2262
+ try options.log.weather(w, .None);
1653
2263
  }
1654
2264
 
1655
- fn teleport(battle: anytype, player: Player, state: *State, options: anytype) !void {
1656
- _ = .{ battle, player, state, options }; // TODO
2265
+ pub fn teleport(battle: anytype, player: Player, _: *State, options: anytype) !void {
2266
+ if (showdown) return;
2267
+
2268
+ try options.log.fail(battle.active(player), .None);
2269
+ try options.log.laststill();
1657
2270
  }
1658
2271
 
1659
- fn thief(battle: anytype, player: Player, state: *State, options: anytype) !void {
1660
- _ = .{ battle, player, state, options }; // TODO
2272
+ pub fn thief(battle: anytype, player: Player, state: *State, options: anytype) !void {
2273
+ const side = battle.side(player);
2274
+ const foe = battle.foe(player);
2275
+
2276
+ if (side.stored().item != .None or foe.stored().item == .None) return;
2277
+ // Pokémon Showdown incorrectly allows you steal Mail
2278
+ if (!showdown and foe.stored().item.isMail()) return;
2279
+ if (!state.proc) return;
2280
+
2281
+ side.stored().item = foe.stored().item;
2282
+ foe.stored().item = .None;
2283
+
2284
+ try options.log.item(
2285
+ battle.active(player),
2286
+ side.stored().item,
2287
+ battle.active(player.foe()),
2288
+ );
1661
2289
  }
1662
2290
 
1663
- fn thrashing(battle: anytype, player: Player, state: *State, options: anytype) !void {
1664
- _ = .{ battle, player, state, options }; // TODO
2291
+ pub fn thrashing(battle: anytype, player: Player, _: *State, options: anytype) !void {
2292
+ var side = battle.side(player);
2293
+ if (Status.is(side.stored().status, .SLP)) return;
2294
+
2295
+ assert(!side.active.volatiles.Thrashing);
2296
+ side.active.volatiles.Thrashing = true;
2297
+ side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
2298
+ // TODO wSomeoneIsRampaging = true;
1665
2299
  }
1666
2300
 
1667
- fn toxic(battle: anytype, player: Player, state: *State, options: anytype) !void {
1668
- _ = .{ battle, player, state, options }; // TODO
2301
+ pub fn transform(battle: anytype, player: Player, _: *State, options: anytype) !void {
2302
+ var side = battle.side(player);
2303
+ const foe = battle.foe(player);
2304
+ const foe_ident = battle.active(player.foe());
2305
+
2306
+ side.last_used_move = .None;
2307
+ side.active.volatiles.dirty = false;
2308
+
2309
+ if (foe.active.volatiles.Transform) {
2310
+ return options.log.fail(battle.active(player), .None);
2311
+ } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2312
+ if (!showdown) return options.log.fail(battle.active(player), .None);
2313
+ try options.log.lastmiss();
2314
+ return options.log.miss(battle.active(player));
2315
+ }
2316
+
2317
+ side.active.volatiles.Transform = true;
2318
+ side.active.volatiles.transform = foe_ident.int();
2319
+ side.active.volatiles.disable = .{};
2320
+
2321
+ // HP is not copied by Transform
2322
+ side.active.stats.atk = foe.active.stats.atk;
2323
+ side.active.stats.def = foe.active.stats.def;
2324
+ side.active.stats.spe = foe.active.stats.spe;
2325
+ side.active.stats.spa = foe.active.stats.spa;
2326
+ side.active.stats.spd = foe.active.stats.spd;
2327
+
2328
+ side.active.species = foe.active.species;
2329
+ side.active.types = foe.active.types;
2330
+ side.active.boosts = foe.active.boosts;
2331
+ for (foe.active.moves, 0..) |m, i| {
2332
+ side.active.moves[i].id = m.id;
2333
+ side.active.moves[i].pp = if (m.id != .None) if (m.id == .Sketch) 1 else 5 else 0;
2334
+ }
2335
+
2336
+ try options.log.transform(battle.active(player), foe_ident);
1669
2337
  }
1670
2338
 
1671
- fn transform(battle: anytype, player: Player, state: *State, options: anytype) !void {
1672
- _ = .{ battle, player, state, options }; // TODO
2339
+ pub fn triAttack(battle: anytype, player: Player, state: *State, options: anytype) !void {
2340
+ // FIXME only commit effectchance and triattack rolls if actually does something...
2341
+ try effectChance(battle, player, state, options);
2342
+ if (state.proc) {
2343
+ try switch (try Rolls.triAttack(battle, player, options)) {
2344
+ .PAR => paralyzeChance(battle, player, state, options),
2345
+ .FRZ => freezeChance(battle, player, state, options),
2346
+ .BRN => burnChance(battle, player, state, options),
2347
+ };
2348
+ } else if (!showdown) battle.rng.advance(1);
1673
2349
  }
1674
2350
 
1675
- fn tripleKick(battle: anytype, player: Player, state: *State, options: anytype) !void {
2351
+ pub fn tripleKick(battle: anytype, player: Player, state: *State, options: anytype) !void {
1676
2352
  _ = .{ battle, player, state, options }; // TODO
1677
2353
  }
1678
2354
 
1679
- fn twister(battle: anytype, player: Player, state: *State, options: anytype) !void {
1680
- _ = .{ battle, player, state, options }; // TODO
2355
+ const ALL_STAT_UP_STATS = [_][]const u8{ "atk", "def", "spe", "spa", "spd" };
2356
+ const ALL_STAT_UP_REASONS =
2357
+ [_]Boost{ .Attack, .Defense, .Speed, .SpecialAttack, .SpecialDefense };
2358
+
2359
+ pub fn allStatUpChance(battle: anytype, player: Player, state: *State, options: anytype) !void {
2360
+ var side = battle.side(player);
2361
+
2362
+ var stats = &side.active.stats;
2363
+ var boosts = &side.active.boosts;
2364
+
2365
+ if (!state.proc) return;
2366
+ inline for (ALL_STAT_UP_STATS, ALL_STAT_UP_REASONS) |s, r| {
2367
+ assert(@field(boosts, s) >= -6 and @field(boosts, s) <= 6);
2368
+ if (@field(boosts, s) == 6 or @field(stats, s) == MAX_STAT_VALUE) {
2369
+ try options.log.boost(battle.active(player), r, 0);
2370
+ } else {
2371
+ @field(boosts, s) = @intCast(@min(6, @as(i8, @field(boosts, s)) + 1));
2372
+ var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, @field(boosts, s)) + 6))];
2373
+ const stat = @field(unmodifiedStats(battle, side), s);
2374
+ @field(stats, s) = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2375
+ try options.log.boost(battle.active(player), r, 1);
2376
+ }
2377
+ }
2378
+
2379
+ statusModify(side.stored().status, stats);
1681
2380
  }
1682
2381
 
1683
- fn boost(battle: anytype, player: Player, state: *State, options: anytype) !void {
1684
- _ = .{ battle, player, state, options }; // TODO
2382
+ pub fn boost(battle: anytype, player: Player, state: *State, options: anytype) !void {
2383
+ var log = options.log;
2384
+ var side = battle.side(player);
2385
+ const ident = battle.active(player);
2386
+
2387
+ if (state.miss) return log.fail(ident, .None);
2388
+
2389
+ var stats = &side.active.stats;
2390
+ var boosts = &side.active.boosts;
2391
+
2392
+ const move = Move.get(state.move);
2393
+ switch (move.effect) {
2394
+ .AttackUp1, .AttackUp2, .Rage, .Swagger => {
2395
+ assert(boosts.atk >= -6 and boosts.atk <= 6);
2396
+ if (boosts.atk == 6 or stats.atk == MAX_STAT_VALUE) return log.fail(ident, .None);
2397
+ const n: u2 = if (move.effect == .AttackUp2 or move.effect == .Swagger) 2 else 1;
2398
+ boosts.atk = @as(i4, @intCast(@min(6, @as(i8, boosts.atk) + n)));
2399
+ const reason = if (showdown and move.effect == .Rage) Boost.Rage else Boost.Attack;
2400
+ var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2401
+ const stat = unmodifiedStats(battle, side).atk;
2402
+ stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2403
+ try log.boost(ident, reason, n);
2404
+ },
2405
+ .DefenseUp1, .DefenseUp2, .SkullBash => {
2406
+ assert(boosts.def >= -6 and boosts.def <= 6);
2407
+ if (boosts.def == 6 or stats.def == MAX_STAT_VALUE) return log.fail(ident, .None);
2408
+ const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
2409
+ boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
2410
+ var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2411
+ const stat = unmodifiedStats(battle, side).def;
2412
+ stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2413
+ try log.boost(ident, .Defense, n);
2414
+ },
2415
+ .SpeedUp2 => {
2416
+ assert(boosts.spe >= -6 and boosts.spe <= 6);
2417
+ if (boosts.spe == 6 or stats.spe == MAX_STAT_VALUE) return log.fail(ident, .None);
2418
+ boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
2419
+ var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2420
+ const stat = unmodifiedStats(battle, side).spe;
2421
+ stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2422
+ try log.boost(ident, .Speed, 2);
2423
+ },
2424
+ .SpAtkUp1 => {
2425
+ assert(boosts.spa >= -6 and boosts.spa <= 6);
2426
+ if (boosts.spa == 6 or stats.spa == MAX_STAT_VALUE) return log.fail(ident, .None);
2427
+ boosts.spa = @intCast(@min(6, @as(i8, boosts.spa) + 1));
2428
+ var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.spa) + 6))];
2429
+ const stat = unmodifiedStats(battle, side).spa;
2430
+ stats.spa = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2431
+ try log.boost(ident, .SpecialAttack, 1);
2432
+ },
2433
+ .SpDefUp2 => {
2434
+ assert(boosts.spd >= -6 and boosts.spd <= 6);
2435
+ if (boosts.spd == 6 or stats.spd == MAX_STAT_VALUE) return log.fail(ident, .None);
2436
+ boosts.spd = @intCast(@min(6, @as(i8, boosts.spd) + 2));
2437
+ var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.spd) + 6))];
2438
+ const stat = unmodifiedStats(battle, side).spd;
2439
+ stats.spd = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2440
+ try log.boost(ident, .SpecialDefense, 2);
2441
+ },
2442
+ .EvasionUp1 => {
2443
+ assert(boosts.evasion >= -6 and boosts.evasion <= 6);
2444
+ if (boosts.evasion == 6) return log.fail(ident, .None);
2445
+ boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
2446
+ try log.boost(ident, .Evasion, 1);
2447
+ if (state.move == .Minimize) side.active.volatiles.minimized = true;
2448
+ },
2449
+ else => unreachable,
2450
+ }
2451
+
2452
+ statusModify(side.stored().status, stats);
1685
2453
  }
1686
2454
 
1687
- fn unboost(battle: anytype, player: Player, state: *State, options: anytype) !void {
1688
- _ = .{ battle, player, state, options }; // TODO
2455
+ pub fn unboost(battle: anytype, player: Player, state: *State, options: anytype) !void {
2456
+ var log = options.log;
2457
+ var foe = battle.foe(player);
2458
+ const foe_ident = battle.active(player.foe());
2459
+
2460
+ if (foe.active.volatiles.Mist) {
2461
+ try log.activate(foe_ident, .Mist);
2462
+ return log.fail(foe_ident, .None);
2463
+ }
2464
+
2465
+ const fail = foe.active.volatiles.Substitute or state.miss or !state.proc;
2466
+
2467
+ var stats = &foe.active.stats;
2468
+ var boosts = &foe.active.boosts;
2469
+
2470
+ const move = Move.get(state.move);
2471
+ switch (move.effect) {
2472
+ .AttackDown1, .AttackDown2, .AttackDownChance => {
2473
+ assert(boosts.atk >= -6 and boosts.atk <= 6);
2474
+ if (fail or boosts.atk == -6) {
2475
+ return if (move.effect != .AttackDownChance) try log.fail(foe_ident, .None);
2476
+ } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2477
+ try options.log.lastmiss();
2478
+ return options.log.miss(battle.active(player));
2479
+ } else if (stats.atk == 1) {
2480
+ return if (move.effect != .AttackDownChance) try log.fail(foe_ident, .None);
2481
+ }
2482
+ const n: u2 = if (move.effect == .AttackDown2) 2 else 1;
2483
+ boosts.atk = @intCast(@max(-6, @as(i8, boosts.atk) - n));
2484
+ var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2485
+ const stat = unmodifiedStats(battle, foe).atk;
2486
+ stats.atk = @max(1, stat * mod[0] / mod[1]);
2487
+ try log.boost(foe_ident, .Attack, -@as(i8, n));
2488
+ },
2489
+ .DefenseDown1, .DefenseDown2, .DefenseDownChance => {
2490
+ assert(boosts.def >= -6 and boosts.def <= 6);
2491
+ if (fail or boosts.def == -6) {
2492
+ return if (move.effect != .DefenseDownChance) try log.fail(foe_ident, .None);
2493
+ } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2494
+ try options.log.lastmiss();
2495
+ return options.log.miss(battle.active(player));
2496
+ } else if (stats.def == 1) {
2497
+ return if (move.effect != .DefenseDownChance) try log.fail(foe_ident, .None);
2498
+ }
2499
+ const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
2500
+ boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
2501
+ var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2502
+ const stat = unmodifiedStats(battle, foe).def;
2503
+ stats.def = @max(1, stat * mod[0] / mod[1]);
2504
+ try log.boost(foe_ident, .Defense, -@as(i8, n));
2505
+ },
2506
+ .SpeedDown1, .SpeedDown2, .SpeedDownChance => {
2507
+ assert(boosts.spe >= -6 and boosts.spe <= 6);
2508
+ if (fail or boosts.spe == -6) {
2509
+ return if (move.effect != .SpeedDownChance) try log.fail(foe_ident, .None);
2510
+ } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2511
+ try options.log.lastmiss();
2512
+ return options.log.miss(battle.active(player));
2513
+ } else if (stats.spe == 1) {
2514
+ return if (move.effect != .SpeedDownChance) try log.fail(foe_ident, .None);
2515
+ }
2516
+ const n: u2 = if (move.effect == .SpeedDown2) 2 else 1;
2517
+ boosts.spe = @intCast(@max(-6, @as(i8, boosts.spe) - 1));
2518
+ var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2519
+ const stat = unmodifiedStats(battle, foe).spe;
2520
+ stats.spe = @max(1, stat * mod[0] / mod[1]);
2521
+ try log.boost(foe_ident, .Speed, -@as(i8, n));
2522
+ assert(boosts.spe >= -6);
2523
+ },
2524
+ .SpDefDownChance => {
2525
+ assert(boosts.spd >= -6 and boosts.spd <= 6);
2526
+ if (fail or boosts.spd == -6) return;
2527
+ if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2528
+ try options.log.lastmiss();
2529
+ return options.log.miss(battle.active(player));
2530
+ } else if (stats.spd == 1) return;
2531
+ boosts.spd = @intCast(@max(-6, @as(i8, boosts.spd) - 1));
2532
+ var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.spd) + 6))];
2533
+ const stat = unmodifiedStats(battle, foe).spd;
2534
+ stats.spd = @max(1, stat * mod[0] / mod[1]);
2535
+ try log.boost(foe_ident, .SpecialDefense, -1);
2536
+ },
2537
+ .AccuracyDown1, .AccuracyDownChance => {
2538
+ assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
2539
+ if (fail or boosts.accuracy == -6) {
2540
+ return if (move.effect != .AccuracyDownChance) try log.fail(foe_ident, .None);
2541
+ } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2542
+ try options.log.lastmiss();
2543
+ return options.log.miss(battle.active(player));
2544
+ }
2545
+ boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
2546
+ try log.boost(foe_ident, .Accuracy, -1);
2547
+ },
2548
+ else => unreachable,
2549
+ }
2550
+
2551
+ statusModify(foe.stored().status, stats);
1689
2552
  }
1690
2553
  };
1691
2554
 
2555
+ fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
2556
+ if (!side.active.volatiles.Transform) return &side.stored().stats;
2557
+ const id = ID.from(side.active.volatiles.transform);
2558
+ return &battle.side(id.player).pokemon[id.id - 1].stats;
2559
+ }
2560
+
1692
2561
  fn statusModify(status: u8, stats: *Stats(u16)) void {
1693
2562
  if (Status.is(status, .PAR)) {
1694
2563
  stats.spe = @max(stats.spe / 4, 1);
@@ -1697,20 +2566,20 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
1697
2566
  }
1698
2567
  }
1699
2568
 
1700
- inline fn isForced(active: anytype) bool {
2569
+ fn isForced(active: anytype) bool {
1701
2570
  return active.volatiles.Recharging or active.volatiles.Rage or
1702
2571
  active.volatiles.Thrashing or active.volatiles.Charging or
1703
2572
  active.volatiles.Rollout;
1704
2573
  }
1705
2574
 
1706
- inline fn convertible(active: ActivePokemon, m: Move) bool {
2575
+ fn convertible(active: ActivePokemon, m: Move) bool {
1707
2576
  if (m == .None) return false;
1708
2577
  const t = Move.get(m).type;
1709
2578
  return t != .@"???" and !active.types.includes(t);
1710
2579
  }
1711
2580
 
1712
2581
  pub const Rolls = struct {
1713
- inline fn speedTie(battle: anytype, options: anytype) !bool {
2582
+ fn speedTie(battle: anytype, options: anytype) !bool {
1714
2583
  const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
1715
2584
  player == .P1
1716
2585
  else if (showdown)
@@ -1722,7 +2591,7 @@ pub const Rolls = struct {
1722
2591
  return p1;
1723
2592
  }
1724
2593
 
1725
- inline fn quickClaw(battle: anytype, player: Player, options: anytype) !bool {
2594
+ fn quickClaw(battle: anytype, player: Player, options: anytype) !bool {
1726
2595
  const qkc = if (options.calc.overridden(player, .quick_claw)) |val|
1727
2596
  val == .true
1728
2597
  else if (showdown)
@@ -1734,7 +2603,7 @@ pub const Rolls = struct {
1734
2603
  return qkc;
1735
2604
  }
1736
2605
 
1737
- inline fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) !bool {
2606
+ fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) !bool {
1738
2607
  const ok = if (options.calc.overridden(player, .hit)) |val|
1739
2608
  val == .true
1740
2609
  else if (showdown)
@@ -1746,7 +2615,7 @@ pub const Rolls = struct {
1746
2615
  return ok;
1747
2616
  }
1748
2617
 
1749
- inline fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2618
+ fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
1750
2619
  const crit = if (options.calc.overridden(player, .critical_hit)) |val|
1751
2620
  val == .true
1752
2621
  else if (showdown)
@@ -1758,7 +2627,7 @@ pub const Rolls = struct {
1758
2627
  return crit;
1759
2628
  }
1760
2629
 
1761
- inline fn damage(battle: anytype, player: anytype, options: anytype) !u8 {
2630
+ fn damage(battle: anytype, player: Player, options: anytype) !u8 {
1762
2631
  const roll = if (options.calc.overridden(player, .damage)) |val|
1763
2632
  val
1764
2633
  else roll: {
@@ -1774,7 +2643,7 @@ pub const Rolls = struct {
1774
2643
  return roll;
1775
2644
  }
1776
2645
 
1777
- inline fn confused(battle: anytype, player: Player, options: anytype) !bool {
2646
+ fn confused(battle: anytype, player: Player, options: anytype) !bool {
1778
2647
  const cfz = if (options.calc.overridden(player, .confused)) |val|
1779
2648
  val == .true
1780
2649
  else if (showdown)
@@ -1786,7 +2655,7 @@ pub const Rolls = struct {
1786
2655
  return cfz;
1787
2656
  }
1788
2657
 
1789
- inline fn attract(battle: anytype, player: Player, options: anytype) !bool {
2658
+ fn attract(battle: anytype, player: Player, options: anytype) !bool {
1790
2659
  const cant = if (options.calc.overridden(player, .attract)) |val|
1791
2660
  val == .true
1792
2661
  else if (showdown)
@@ -1798,7 +2667,7 @@ pub const Rolls = struct {
1798
2667
  return cant;
1799
2668
  }
1800
2669
 
1801
- inline fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2670
+ fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
1802
2671
  const par = if (options.calc.overridden(player, .paralyzed)) |val|
1803
2672
  val == .true
1804
2673
  else if (showdown)
@@ -1810,7 +2679,7 @@ pub const Rolls = struct {
1810
2679
  return par;
1811
2680
  }
1812
2681
 
1813
- inline fn defrost(battle: anytype, player: Player, options: anytype) !bool {
2682
+ fn defrost(battle: anytype, player: Player, options: anytype) !bool {
1814
2683
  const thaw = if (options.calc.overridden(player, .defrost)) |val|
1815
2684
  val == .true
1816
2685
  else if (showdown)
@@ -1822,7 +2691,7 @@ pub const Rolls = struct {
1822
2691
  return thaw;
1823
2692
  }
1824
2693
 
1825
- inline fn secondaryChance(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2694
+ fn secondaryChance(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
1826
2695
  // GLITCH: 100% secondary chance will still fail 1/256 of the time
1827
2696
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
1828
2697
  val == .true
@@ -1835,7 +2704,7 @@ pub const Rolls = struct {
1835
2704
  return proc;
1836
2705
  }
1837
2706
 
1838
- inline fn item(battle: anytype, player: Player, options: anytype) !bool {
2707
+ fn item(battle: anytype, player: Player, options: anytype) !bool {
1839
2708
  const proc = if (options.calc.overridden(player, .item)) |val|
1840
2709
  val == .true
1841
2710
  else if (showdown)
@@ -1847,23 +2716,23 @@ pub const Rolls = struct {
1847
2716
  return proc;
1848
2717
  }
1849
2718
 
1850
- inline fn triAttack(battle: anytype, player: Player, options: anytype) !u8 {
2719
+ fn triAttack(battle: anytype, player: Player, options: anytype) !TriAttack {
1851
2720
  const status: TriAttack = if (options.calc.overridden(player, .tri_attack)) |val|
1852
- val
2721
+ @enumFromInt(@intFromEnum(val) - 1)
1853
2722
  else if (showdown)
1854
- @enumFromInt(battle.rng.range(u8, 0, 2))
2723
+ @enumFromInt(battle.rng.range(u2, 0, 2))
1855
2724
  else loop: {
1856
2725
  while (true) {
1857
- const r = std.math.rotr(u8, std.battle.rng.next(), 4) & 3;
2726
+ const r = std.math.rotr(u8, battle.rng.next(), 4) & 3;
1858
2727
  if (r != 0) break :loop @enumFromInt(r - 1);
1859
2728
  }
1860
2729
  };
1861
2730
 
1862
2731
  try options.chance.triAttack(player, status);
1863
- return status.status();
2732
+ return status;
1864
2733
  }
1865
2734
 
1866
- inline fn present(battle: anytype, player: Player, options: anytype) !u8 {
2735
+ fn present(battle: anytype, player: Player, options: anytype) !u8 {
1867
2736
  const power = if (options.calc.overridden(player, .present)) |val|
1868
2737
  (val - 1) * 40
1869
2738
  else if (showdown) power: {
@@ -1885,7 +2754,7 @@ pub const Rolls = struct {
1885
2754
  return power;
1886
2755
  }
1887
2756
 
1888
- inline fn magnitude(battle: anytype, player: Player, options: anytype) !u8 {
2757
+ fn magnitude(battle: anytype, player: Player, options: anytype) !u8 {
1889
2758
  const num = if (options.calc.overridden(player, .magnitude)) |val|
1890
2759
  val + 3
1891
2760
  else if (showdown) num: {
@@ -1913,7 +2782,7 @@ pub const Rolls = struct {
1913
2782
  return num;
1914
2783
  }
1915
2784
 
1916
- inline fn tripleKick(battle: anytype, player: Player, options: anytype) !u2 {
2785
+ fn tripleKick(battle: anytype, player: Player, options: anytype) !u2 {
1917
2786
  const hits = if (options.calc.overridden(player, .triple_kick)) |val|
1918
2787
  val
1919
2788
  else if (showdown)
@@ -1930,7 +2799,7 @@ pub const Rolls = struct {
1930
2799
  return hits;
1931
2800
  }
1932
2801
 
1933
- inline fn spite(battle: anytype, player: Player, options: anytype) !u8 {
2802
+ fn spite(battle: anytype, player: Player, options: anytype) !u8 {
1934
2803
  const pp = if (options.calc.overridden(player, .spite)) |val|
1935
2804
  val + 1
1936
2805
  else if (showdown)
@@ -1946,7 +2815,7 @@ pub const Rolls = struct {
1946
2815
  // Conversion 2 can at most choose between 7 types (vs. a Grass-type attack)
1947
2816
  var CONVERSION_2: [7]Type = [_]Type{.@"???"} ** 7;
1948
2817
 
1949
- inline fn conversion2(battle: anytype, player: Player, mtype: Type, options: anytype) !Type {
2818
+ fn conversion2(battle: anytype, player: Player, mtype: Type, options: anytype) !Type {
1950
2819
  var i: u8 = 0;
1951
2820
  const neutral = @intFromEnum(Effectiveness.Neutral);
1952
2821
  assert(mtype != .@"???");
@@ -1980,7 +2849,7 @@ pub const Rolls = struct {
1980
2849
  return ty;
1981
2850
  }
1982
2851
 
1983
- inline fn protect(battle: anytype, player: Player, count: u8, options: anytype) !bool {
2852
+ fn protect(battle: anytype, player: Player, count: u8, options: anytype) !bool {
1984
2853
  assert(count <= 8);
1985
2854
  const num = if (count == 8) 0 else @as(u8, 255) >> @intCast(count);
1986
2855
 
@@ -2074,7 +2943,7 @@ pub const Rolls = struct {
2074
2943
  return slot;
2075
2944
  }
2076
2945
 
2077
- inline fn forceSwitch(
2946
+ fn forceSwitch(
2078
2947
  battle: anytype,
2079
2948
  player: Player,
2080
2949
  slots: []u4,
@@ -2112,7 +2981,7 @@ pub const Rolls = struct {
2112
2981
 
2113
2982
  const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
2114
2983
 
2115
- inline fn multiHit(battle: anytype, player: Player, options: anytype) !u3 {
2984
+ fn multiHit(battle: anytype, player: Player, options: anytype) !u3 {
2116
2985
  const n: u3 = if (options.calc.overridden(player, .multi_hit)) |val|
2117
2986
  @intCast(val)
2118
2987
  else if (showdown)
@@ -2127,30 +2996,30 @@ pub const Rolls = struct {
2127
2996
  return n;
2128
2997
  }
2129
2998
 
2130
- inline fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2999
+ fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2131
3000
  const power = if (options.calc.overridden(player, .psywave)) |val|
2132
- val - 1
3001
+ val
2133
3002
  else if (showdown)
2134
- battle.rng.range(u8, 0, max)
3003
+ battle.rng.range(u8, 1, max)
2135
3004
  else power: {
2136
3005
  while (true) {
2137
3006
  const r = battle.rng.next();
2138
- if (r < max) break :power r;
3007
+ if (r != 0 and r < max) break :power r;
2139
3008
  }
2140
3009
  };
2141
3010
 
2142
- assert(power < max);
3011
+ assert(power != 0 and power < max);
2143
3012
  try options.chance.psywave(player, power, max);
2144
3013
  return power;
2145
3014
  }
2146
3015
 
2147
- inline fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2148
- const moves = battle.get(player).active.moves;
3016
+ fn metronome(battle: anytype, player: Player, options: anytype) !Move {
3017
+ const moves = battle.side(player).active.moves;
2149
3018
 
2150
3019
  var n: u2 = 0;
2151
3020
  for (moves) |m| {
2152
- if (m == .None) break;
2153
- if (Move.get(m).metronome) n += 1;
3021
+ if (m.id == .None) break;
3022
+ if (Move.get(m.id).extra.metronome) n += 1;
2154
3023
  }
2155
3024
 
2156
3025
  // TODO: consider throwing error instead of rerolling?
@@ -2161,10 +3030,11 @@ pub const Rolls = struct {
2161
3030
 
2162
3031
  const move: Move = overridden orelse move: {
2163
3032
  if (showdown) {
2164
- var r = battle.rng.range(u8, 0, Move.METRONOME.len - n);
3033
+ var r = battle.rng.range(u8, 0, @intCast(Move.METRONOME.len - n));
2165
3034
  for (moves) |m| {
2166
- if (m == .None or @intFromEnum(m) > @intFromEnum(Move.METRONOME[r])) break;
2167
- if (Move.get(m).metronome) r -= 1;
3035
+ if (m.id == .None) break;
3036
+ if (@intFromEnum(m.id) > @intFromEnum(Move.METRONOME[r])) break;
3037
+ if (Move.get(m.id).extra.metronome) r -= 1;
2168
3038
  }
2169
3039
  break :move Move.METRONOME[r];
2170
3040
  } else {
@@ -2179,12 +3049,12 @@ pub const Rolls = struct {
2179
3049
  }
2180
3050
  };
2181
3051
 
2182
- assert(Move.get(move).metronome);
3052
+ assert(Move.get(move).extra.metronome);
2183
3053
  try options.chance.metronome(player, move, n);
2184
3054
  return move;
2185
3055
  }
2186
3056
 
2187
- inline fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
3057
+ fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2188
3058
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2189
3059
  @intCast(val)
2190
3060
  else if (showdown)
@@ -2201,7 +3071,7 @@ pub const Rolls = struct {
2201
3071
  return duration + 1;
2202
3072
  }
2203
3073
 
2204
- inline fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
3074
+ fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
2205
3075
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2206
3076
  @intCast(val)
2207
3077
  else if (showdown)
@@ -2215,7 +3085,7 @@ pub const Rolls = struct {
2215
3085
  return duration;
2216
3086
  }
2217
3087
 
2218
- inline fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
3088
+ fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2219
3089
  const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
2220
3090
  @intCast(val)
2221
3091
  else if (showdown)
@@ -2233,7 +3103,7 @@ pub const Rolls = struct {
2233
3103
  return duration;
2234
3104
  }
2235
3105
 
2236
- inline fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
3106
+ fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2237
3107
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2238
3108
  @intCast(val)
2239
3109
  else if (showdown)
@@ -2246,7 +3116,7 @@ pub const Rolls = struct {
2246
3116
  return duration;
2247
3117
  }
2248
3118
 
2249
- inline fn bindingDuration(battle: anytype, player: Player, options: anytype) u3 {
3119
+ fn bindingDuration(battle: anytype, player: Player, options: anytype) u3 {
2250
3120
  const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
2251
3121
  @intCast(val)
2252
3122
  else if (showdown)
@@ -2261,7 +3131,7 @@ pub const Rolls = struct {
2261
3131
  }
2262
3132
 
2263
3133
  // TODO: consider sharing implementation with bindingDuration
2264
- inline fn encoreDuration(battle: anytype, player: Player, options: anytype) u3 {
3134
+ fn encoreDuration(battle: anytype, player: Player, options: anytype) u3 {
2265
3135
  const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
2266
3136
  @intCast(val)
2267
3137
  else if (showdown)