@pkmn/engine 0.1.0-dev.c42deb42 → 0.1.0-dev.c4f94762

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