@pkmn/engine 0.1.0-dev.305a932f → 0.1.0-dev.30c63015

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