@pkmn/engine 0.1.0-dev.14ca6108 → 0.1.0-dev.15f64810

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