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

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