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

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