@pkmn/engine 0.1.0-dev.89cc1c98 → 0.1.0-dev.8b192003

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