@pkmn/engine 0.1.0-dev.bcb16a6c → 0.1.0-dev.be951af1

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,22 +6,26 @@ const common = @import("../common/data.zig");
6
6
  const DEBUG = @import("../common/debug.zig").print;
7
7
  const protocol = @import("../common/protocol.zig");
8
8
  const rng = @import("../common/rng.zig");
9
+ const util = @import("../common/util.zig");
9
10
 
10
11
  const data = @import("data.zig");
12
+ const generated = @import("generated.zig");
11
13
 
12
14
  const assert = std.debug.assert;
13
15
 
14
16
  const expectEqual = std.testing.expectEqual;
15
17
  const expectEqualSlices = std.testing.expectEqualSlices;
18
+ const expectEqualStrings = std.testing.expectEqualStrings;
19
+
20
+ const showdown = pkmn.options.showdown;
16
21
 
17
22
  const Choice = common.Choice;
18
23
  const ID = common.ID;
19
24
  const Player = common.Player;
20
25
  const Result = common.Result;
21
26
 
22
- const showdown = pkmn.options.showdown;
23
-
24
27
  const Activate = protocol.Activate;
28
+ const Boost = protocol.Boost;
25
29
  const Cant = protocol.Cant;
26
30
  const Damage = protocol.Damage;
27
31
  const End = protocol.End;
@@ -30,6 +34,8 @@ const Start = protocol.Start;
30
34
 
31
35
  const Gen12 = rng.Gen12;
32
36
 
37
+ const PointerType = util.PointerType;
38
+
33
39
  const ActivePokemon = data.ActivePokemon;
34
40
  const Effectiveness = data.Effectiveness;
35
41
  const Item = data.Item;
@@ -89,7 +95,25 @@ const CRITICAL_HITS = &[_]u8{
89
95
 
90
96
  const MAX_STAT_VALUE = 999;
91
97
 
92
- const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
98
+ pub const State = struct {
99
+ damage: u16 = 0, // wCurDamage
100
+ effectiveness: u16 = Effectiveness.neutral, // wTypeModifier
101
+ bp: u8 = 0, // wPlayerMoveStructPower
102
+ move: Move = .None, // wCurPlayerMove
103
+ mslot: u4 = 0, // wCurMoveNum
104
+ first: bool = false, // wEnemyGoesFirst
105
+ miss: bool = false, // wAttackMissed
106
+ crit: bool = false, // wCriticalHit (NB: used on device for OHKO as well)
107
+ proc: bool = true, // wEffectFailed
108
+
109
+ comptime {
110
+ assert(@sizeOf(State) == 12);
111
+ }
112
+
113
+ inline fn immune(self: *State) bool {
114
+ return self.effectiveness == 0;
115
+ }
116
+ };
93
117
 
94
118
  pub fn update(battle: anytype, c1: Choice, c2: Choice, options: anytype) !Result {
95
119
  assert(c1.type != .Pass or c2.type != .Pass or battle.turn == 0);
@@ -169,14 +193,14 @@ fn selectMove(battle: anytype, player: Player, choice: Choice) void {
169
193
  if (choice.data == 0) {
170
194
  const struggle = ok: {
171
195
  for (side.active.moves, 0..) |move, i| {
172
- if (move.pp > 0 and volatiles.disabled_move != i + 1) break :ok false;
196
+ if (move.pp > 0 and volatiles.disable.move != i + 1) break :ok false;
173
197
  }
174
198
  break :ok true;
175
199
  };
176
200
  assert(struggle);
177
201
  // TODO wCurPlayerMove = .Struggle;
178
202
  } else {
179
- assert(showdown or side.active.volatiles.disabled_move != choice.data);
203
+ assert(showdown or side.active.volatiles.disable.move != choice.data);
180
204
  const move = side.active.move(choice.data);
181
205
  // You cannot *select* a move with 0 PP
182
206
  assert(move.pp != 0);
@@ -316,22 +340,42 @@ fn doTurn(
316
340
  const volatiles = &battle.side(player).active.volatiles;
317
341
  const foe_volatiles = &battle.foe(player).active.volatiles;
318
342
 
343
+ // Technically this happens at the very beginning of update but its unused until now
344
+ // TODO: move into state and pass into betweenTurns
345
+ volatiles.frozen = false;
346
+ foe_volatiles.frozen = false;
347
+
319
348
  volatiles.DestinyBond = false;
320
- if (try executeMove(battle, player, player_choice, options)) |r| return r;
349
+ if (try executeMove(battle, player, player_choice, true, options)) |r| return r;
321
350
  foe_volatiles.Protect = false;
322
351
  foe_volatiles.Endure = false;
323
352
  foe_volatiles.DestinyBond = false;
353
+ if (try checkFaint(battle, foe_player, options)) |r| return r;
354
+ if (try handleResidual(battle, player, options)) {
355
+ if (try checkFaint(battle, player, options)) |r| return r;
356
+ }
324
357
 
325
358
  // foe_volatiles.DestinyBond = false;
326
- if (try executeMove(battle, foe_player, foe_choice, options)) |r| return r;
359
+ if (try executeMove(battle, foe_player, foe_choice, false, options)) |r| return r;
327
360
  volatiles.Protect = false;
328
361
  volatiles.Endure = false;
329
362
  volatiles.DestinyBond = false;
363
+ if (try checkFaint(battle, player, options)) |r| return r;
364
+ if (try handleResidual(battle, foe_player, options)) {
365
+ if (try checkFaint(battle, foe_player, options)) |r| return r;
366
+ }
330
367
 
331
- return null;
368
+ // FIXME
369
+ return betweenTurns(battle, 0, options);
332
370
  }
333
371
 
334
- fn executeMove(battle: anytype, player: Player, choice: Choice, options: anytype) !?Result {
372
+ fn executeMove(
373
+ battle: anytype,
374
+ player: Player,
375
+ choice: Choice,
376
+ first: bool,
377
+ options: anytype,
378
+ ) !?Result {
335
379
  if (choice.type == .Switch) {
336
380
  try switchIn(battle, player, choice.data, false, options);
337
381
  return null;
@@ -339,15 +383,17 @@ fn executeMove(battle: anytype, player: Player, choice: Choice, options: anytype
339
383
 
340
384
  assert(choice.type == .Move);
341
385
  var mslot: u4 = @intCast(choice.data);
386
+ var move = Move.Pound; // TODO
342
387
 
343
- // const move = Move.get(side.last_selected_move);
388
+ if (!try beforeMove(battle, player, move, options)) return null;
344
389
 
345
- // CheckTurn
346
-
347
- return doMove(battle, player, options, mslot);
390
+ // FIXME: need damage from doTurn
391
+ var state: State = .{ .move = move, .mslot = mslot, .first = first };
392
+ _ = try TODO(battle, player, &state, options);
393
+ return generated.doMove(battle, player, &state, options);
348
394
  }
349
395
 
350
- fn beforeMove(battle: anytype, player: Player, options: anytype) !bool {
396
+ fn beforeMove(battle: anytype, player: Player, move: Move, options: anytype) !bool {
351
397
  var log = options.log;
352
398
  var side = battle.side(player);
353
399
  // const foe = battle.foe(player);
@@ -356,8 +402,6 @@ fn beforeMove(battle: anytype, player: Player, options: anytype) !bool {
356
402
  const ident = battle.active(player);
357
403
  var volatiles = &active.volatiles;
358
404
 
359
- const move = Move.Pound; // TODO
360
-
361
405
  if (volatiles.Recharging) {
362
406
  volatiles.Recharging = false;
363
407
  cantMove(volatiles);
@@ -408,16 +452,16 @@ fn beforeMove(battle: anytype, player: Player, options: anytype) !bool {
408
452
  return false;
409
453
  }
410
454
 
411
- if (volatiles.disabled_duration > 0) {
455
+ if (volatiles.disable.duration > 0) {
412
456
  // if (options.calc.modify(player, .disable)) |extend| {
413
- // if (!extend) volatiles.disabled_duration = 0;
457
+ // if (!extend) volatiles.disable.duration = 0;
414
458
  // } else {
415
- volatiles.disabled_duration -= 1;
459
+ volatiles.disable.duration -= 1;
416
460
  // }
417
- try options.chance.disable(player, volatiles.disabled_duration);
461
+ try options.chance.disable(player, volatiles.disable.duration);
418
462
 
419
- if (volatiles.disabled_duration == 0) {
420
- volatiles.disabled_move = 0;
463
+ if (volatiles.disable.duration == 0) {
464
+ volatiles.disable.move = 0;
421
465
  try log.end(ident, .Disable);
422
466
  }
423
467
  }
@@ -451,7 +495,7 @@ fn beforeMove(battle: anytype, player: Player, options: anytype) !bool {
451
495
  }
452
496
 
453
497
  if (volatiles.Attract) {
454
- try log.atrract(ident, .Attract);
498
+ try log.activate(ident, .Attract);
455
499
 
456
500
  if (try Rolls.attract(battle, player, options)) {
457
501
  cantMove(volatiles);
@@ -460,9 +504,9 @@ fn beforeMove(battle: anytype, player: Player, options: anytype) !bool {
460
504
  }
461
505
  }
462
506
 
463
- if (side.active.volatiles.disabled_move != 0) {
464
- // A Pokémon that transforms after being disabled may end up with less move slots
465
- const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
507
+ if (side.active.volatiles.disable.move != 0) {
508
+ // A Pokémon that transforms after being disable may end up with less move slots
509
+ const m = side.active.moves[side.active.volatiles.disable.move - 1].id;
466
510
  if (m != .None and m == move) {
467
511
  side.active.volatiles.Charging = false;
468
512
  cantMove(volatiles);
@@ -480,7 +524,7 @@ fn beforeMove(battle: anytype, player: Player, options: anytype) !bool {
480
524
  return true;
481
525
  }
482
526
 
483
- fn cantMove(volatiles: *Volatiles) void {
527
+ fn cantMove(volatiles: *align(8) Volatiles) void {
484
528
  volatiles.Bide = false;
485
529
  volatiles.Thrashing = false;
486
530
  volatiles.Charging = false;
@@ -491,47 +535,82 @@ fn cantMove(volatiles: *Volatiles) void {
491
535
  volatiles.fury_cutter = 0;
492
536
  }
493
537
 
494
- fn canMove(battle: anytype, player: Player, options: anytype) !bool {
538
+ fn canMove(battle: anytype, player: Player, options: anytype) !void {
495
539
  _ = battle;
496
540
  _ = player;
497
541
  _ = options;
498
542
  return false;
499
543
  }
500
544
 
501
- fn decrementPP(side: *Side, mslot: u4) void {
545
+ pub fn decrementPP(side: *Side, move: Move, mslot: u4) bool {
502
546
  var active = &side.active;
503
547
  const volatiles = &active.volatiles;
504
548
 
505
- // TODO assert(side.last_selected_move != .Struggle);
549
+ assert(move != .Struggle);
506
550
  assert(!volatiles.Charging and !volatiles.BeatUp and !volatiles.Thrashing and !volatiles.Bide);
507
551
 
508
- assert(active.move(mslot).pp > 0);
509
- active.move(mslot).pp = @as(u6, @intCast(active.move(mslot).pp)) -% 1;
510
- if (volatiles.Transform) return;
552
+ var move_slot = active.move(mslot);
553
+ if (move_slot.pp == 0) return false;
511
554
 
512
- // FIXME mimic
555
+ move_slot.pp = @as(u6, @intCast(move_slot.pp)) -% 1;
556
+ if (volatiles.Transform) return true;
513
557
 
514
- assert(side.stored().move(mslot).pp > 0);
515
- side.stored().move(mslot).pp = @as(u6, @intCast(side.stored().move(mslot).pp)) -% 1;
558
+ move_slot = side.stored().move(mslot);
559
+ assert(move_slot.pp > 0);
560
+
561
+ if (move_slot.id == .Mimic and move != .Mimic) return true;
562
+
563
+ move_slot.pp = @as(u6, @intCast(move_slot.pp)) -% 1;
516
564
  assert(active.move(mslot).pp == side.stored().move(mslot).pp);
565
+
566
+ return true;
517
567
  }
518
568
 
519
- fn doMove(battle: anytype, player: Player, options: anytype, mslot: u4) !?Result {
520
- _ = options;
521
- var side = battle.side(player);
522
- const move = Move.get(.Teleport); // side.last_selected_move
523
- switch (move.effect) {
524
- .Teleport => {
525
- // side.useMove();
526
- decrementPP(side, mslot);
527
- },
528
- else => @panic("TODO"),
529
- }
569
+ fn TODO(battle: anytype, player: Player, state: *State, options: anytype) !?Result {
570
+ try checkHit(battle, player, state, options);
571
+ try checkCriticalHit(battle, player, state, options);
572
+ try calcDamage(battle, player, state, options);
573
+ try adjustDamage(battle, player, state, options);
574
+ try randomizeDamage(battle, player, state, options);
575
+ try applyDamage(battle, player, state, options);
576
+ try buildRage(battle, player, state, options);
577
+ try rageDamage(battle, player, state, options);
578
+ try kingsRock(battle, player, state, options);
579
+ _ = try destinyBond(battle, player, state, options);
580
+
581
+ try Effects.attract(battle, player, state, options);
582
+ try Effects.boost(battle, player, state, options);
583
+ try Effects.burnChance(battle, player, state, options);
584
+ try Effects.conversion(battle, player, state, options);
585
+ try Effects.conversion2(battle, player, state, options);
586
+ try Effects.defrost(battle, player, state, options);
587
+ try Effects.destinyBond(battle, player, state, options);
588
+ try Effects.disable(battle, player, state, options);
589
+ try Effects.drainHP(battle, player, state, options);
590
+ try Effects.encore(battle, player, state, options);
591
+ try Effects.falseSwipe(battle, player, state, options);
592
+ try Effects.fixedDamage(battle, player, state, options);
593
+ try Effects.focusEnergy(battle, player, state, options);
594
+ try Effects.forceSwitch(battle, player, state, options);
595
+ try Effects.foresight(battle, player, state, options);
596
+ try Effects.freezeChance(battle, player, state, options);
597
+ try Effects.happiness(battle, player, state, options);
598
+ try Effects.paralyzeChance(battle, player, state, options);
599
+ try Effects.poison(battle, player, state, options);
600
+ try Effects.poisonChance(battle, player, state, options);
601
+ try Effects.protect(battle, player, state, options);
602
+ try Effects.rage(battle, player, state, options);
603
+ try Effects.reversal(battle, player, state, options);
604
+ try Effects.sleep(battle, player, state, options);
605
+ try Effects.unboost(battle, player, state, options);
606
+ try Effects.weatherHeal(battle, player, state, options);
607
+
530
608
  return null;
531
609
  }
532
610
 
533
- fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !bool {
534
- if (move.bp == 0) return false;
611
+ pub fn checkCriticalHit(battle: anytype, player: Player, state: *State, options: anytype) !void {
612
+ const move = Move.get(state.move);
613
+ if (move.bp == 0) return;
535
614
 
536
615
  const side = battle.side(player);
537
616
  const pokemon = side.stored();
@@ -546,41 +625,38 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
546
625
  }
547
626
  assert(stage <= 4);
548
627
 
549
- return Rolls.criticalHit(battle, player, CRITICAL_HITS[stage], options);
628
+ state.crit = try Rolls.criticalHit(battle, player, CRITICAL_HITS[stage], options);
550
629
  }
551
630
 
552
- fn calcDamage(
631
+ pub fn calcDamage(
553
632
  battle: anytype,
554
633
  player: Player,
555
- target_player: Player,
556
- m: ?Move.Data,
557
- crit: bool,
558
- ) u16 {
634
+ state: *State,
635
+ options: anytype,
636
+ ) !void {
559
637
  _ = battle;
560
638
  _ = player;
561
- _ = target_player;
562
- _ = m;
563
- _ = crit;
564
- return 0;
639
+ _ = state;
640
+ _ = options;
565
641
  }
566
642
 
567
643
  const FORESIGHT: Types = .{ .type1 = .Normal, .type2 = .Fighting };
568
644
 
569
- fn adjustDamage(battle: anytype, player: Player, m: Move, damage: u16, effectiveness: *u16) u16 {
570
- if (m == .Struggle) return;
645
+ pub fn adjustDamage(battle: anytype, player: Player, state: *State, _: anytype) !void {
646
+ if (state.move == .Struggle) return;
571
647
 
572
648
  const side = battle.side(player);
573
649
  const foe = battle.foe(player);
574
650
  const types = foe.active.types;
575
- const move = Move.get(side.m);
651
+ const move = Move.get(state.move);
576
652
 
577
- var d = damage;
653
+ var d = state.damage;
578
654
  if ((move.type == .Water and battle.field.weather == .Rain) or
579
655
  (move.type == .Fire and battle.field.weather == .Sun))
580
656
  {
581
657
  d = d *% @intFromEnum(Effectiveness.Super) / 10;
582
658
  } else if ((move.type == .Water and battle.field.weather == .Sun) or
583
- ((move.type == .Fire or m == .SolarBeam) and battle.field.weather == .Rain))
659
+ ((move.type == .Fire or state.move == .SolarBeam) and battle.field.weather == .Rain))
584
660
  {
585
661
  d = d *% @intFromEnum(Effectiveness.Resisted) / 10;
586
662
  }
@@ -590,17 +666,17 @@ fn adjustDamage(battle: anytype, player: Player, m: Move, damage: u16, effective
590
666
  const neutral = @intFromEnum(Effectiveness.Neutral);
591
667
  const foresight = foe.active.volatiles.Foresight and FORESIGHT.includes(move.type);
592
668
  const eff1: u16 = if (foresight and types.type1 == .Ghost)
593
- 10
669
+ neutral
594
670
  else
595
671
  @intFromEnum(move.type.effectiveness(types.type1));
596
672
  const eff2: u16 = if (foresight and types.type2 == .Ghost)
597
- 10
673
+ neutral
598
674
  else
599
675
  @intFromEnum(move.type.effectiveness(types.type2));
600
676
 
601
677
  // Type effectiveness matchup precedence only matters with (NVE, SE)
602
678
  if (!showdown and (eff1 + eff2) == Effectiveness.mismatch and
603
- Type.precedence(move.type, types.type1) > Type.precedence(move.type, types.type2))
679
+ types.type1.precedence() > types.type2.precedence())
604
680
  {
605
681
  assert(eff2 != neutral);
606
682
  d = d *% eff2 / 10;
@@ -612,65 +688,79 @@ fn adjustDamage(battle: anytype, player: Player, m: Move, damage: u16, effective
612
688
  if (types.type1 != types.type2 and eff2 != neutral) d = d *% eff2 / 10;
613
689
  }
614
690
 
615
- effectiveness.* = if (types.type1 == types.type2) eff1 * neutral else eff1 * eff2;
616
- return d;
691
+ state.effectiveness = if (types.type1 == types.type2) eff1 * neutral else eff1 * eff2;
692
+ state.damage = d;
617
693
  }
618
694
 
619
- fn randomizeDamage(battle: anytype, player: Player, damage: u16, options: anytype) !u16 {
620
- if (damage <= 1) return;
695
+ pub fn randomizeDamage(battle: anytype, player: Player, state: *State, options: anytype) !void {
696
+ if (state.damage <= 1) return;
621
697
  const random = try Rolls.damage(battle, player, options);
622
- options.calc.base(player.foe(), damage);
623
- damage = @intCast(@as(u32, damage) *% random / 255);
624
- options.calc.final(player.foe(), damage);
625
- return damage;
698
+ options.calc.base(player.foe(), state.damage);
699
+ state.damage = @intCast(@as(u32, state.damage) *% random / 255);
700
+ options.calc.final(player.foe(), state.damage);
626
701
  }
627
702
 
628
- fn applyDamage(
703
+ pub fn applyDamage(
629
704
  battle: anytype,
630
705
  player: Player,
706
+ state: *State,
631
707
  options: anytype,
632
- ) !bool {
708
+ ) !void {
633
709
  _ = battle;
634
710
  _ = player;
711
+ _ = state;
635
712
  _ = options;
636
- return false;
637
- }
638
-
639
- fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !bool {
640
- if (moveHit(battle, player, move, options)) return true;
641
-
642
- // try options.chance.commit(player, .miss);
643
- try options.log.lastmiss();
644
- try options.log.miss(battle.active(player));
645
-
646
- return false;
647
713
  }
648
714
 
649
715
  // TODO: set damage = 0 if missed and not (High) Jump Kick
650
- fn moveHit(
716
+ pub fn checkHit(
651
717
  battle: anytype,
652
718
  player: Player,
653
- move: Move.Data,
719
+ state: *State,
654
720
  options: anytype,
655
- ) !bool {
721
+ ) !void {
656
722
  const side = battle.side(player);
657
723
  const foe = battle.foe(player);
658
724
 
659
- if (move.effect == .DreamEater and !Status.is(foe.stored().status, .SLP)) return false;
660
- if (foe.active.volatiles.Protect) return false;
725
+ const move = Move.get(state.move);
726
+
727
+ if (move.effect == .DreamEater and !Status.is(foe.stored().status, .SLP)) {
728
+ state.miss = true;
729
+ return;
730
+ }
731
+ if (foe.active.volatiles.Protect) {
732
+ state.miss = true;
733
+ return;
734
+ }
661
735
  const drain = move.effect == .DrainHP or move.effect == .DreamEater;
662
- if (drain and foe.active.volatiles.Substitute) return false;
736
+ if (drain and foe.active.volatiles.Substitute) {
737
+ state.miss = true;
738
+ return;
739
+ }
663
740
  if (side.active.volatiles.LockOn) {
664
- return !(foe.active.volatiles.Flying and move.extra.undeground);
741
+ state.miss = !(foe.active.volatiles.Flying and move.extra.underground);
742
+ return;
665
743
  }
666
744
  assert(!(foe.active.volatiles.Flying and foe.active.volatiles.Underground));
667
745
  if (foe.active.volatiles.Flying) {
668
- if (!move.extra.flying) return false;
746
+ if (!move.extra.flying) {
747
+ state.miss = true;
748
+ return;
749
+ }
669
750
  } else if (foe.active.volatiles.Underground) {
670
- if (!move.extra.underground) return false;
751
+ if (!move.extra.underground) {
752
+ state.miss = true;
753
+ return;
754
+ }
755
+ }
756
+ if (move.effect == .Thunder and battle.field.weather == .Rain) {
757
+ state.miss = false;
758
+ return;
759
+ }
760
+ if (move.effect == .AlwaysHit) {
761
+ state.miss = false;
762
+ return;
671
763
  }
672
- if (move.effect == .Thunder and battle.field.weather == .Rain) return true;
673
- if (move.effect == .AlwaysHit) return true;
674
764
 
675
765
  var accuracy: u16 = move.accuracy;
676
766
  if (foe.active.boosts.evasion <= side.active.boosts.accuracy or
@@ -688,8 +778,10 @@ fn moveHit(
688
778
  }
689
779
 
690
780
  // The accuracy roll is skipped entirely if maxed
691
- if (accuracy == 255) return true;
692
- return !try Rolls.hit(battle, player, @intCast(accuracy), options);
781
+ state.miss = if (accuracy == 255)
782
+ false
783
+ else
784
+ !try Rolls.hit(battle, player, @intCast(accuracy), options);
693
785
  }
694
786
 
695
787
  fn checkFaint(
@@ -697,20 +789,62 @@ fn checkFaint(
697
789
  player: Player,
698
790
  options: anytype,
699
791
  ) @TypeOf(options.log).Error!?Result {
700
- _ = battle;
701
- _ = player;
702
- return null;
792
+ const side = battle.side(player);
793
+ const foe = battle.foe(player);
794
+
795
+ const player_fainted = side.stored().hp == 0;
796
+ const foe_fainted = foe.stored().hp == 0;
797
+ if (!(player_fainted or foe_fainted)) return null;
798
+
799
+ const player_out = player_fainted and findFirstAlive(side) == 0;
800
+ const foe_out = foe_fainted and findFirstAlive(foe) == 0;
801
+ const more = player_out or foe_out;
802
+
803
+ if (player_fainted) try faint(battle, player, !(more or foe_fainted), options);
804
+ if (foe_fainted) try faint(battle, player.foe(), !more, options);
805
+
806
+ if (player_out and foe_out) {
807
+ try options.log.tie();
808
+ return Result.Tie;
809
+ } else if (player_out) {
810
+ try options.log.win(player.foe());
811
+ return if (player == .P1) Result.Lose else Result.Win;
812
+ } else if (foe_out) {
813
+ try options.log.win(player);
814
+ return if (player == .P1) Result.Win else Result.Lose;
815
+ }
816
+
817
+ const foe_choice: Choice.Type = if (foe_fainted) .Switch else .Pass;
818
+ if (player == .P1) return .{ .p1 = .Switch, .p2 = foe_choice };
819
+ return .{ .p1 = foe_choice, .p2 = .Switch };
703
820
  }
704
821
 
705
- fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
706
- _ = battle;
707
- _ = player;
708
- _ = done;
709
- _ = options;
710
- return null;
822
+ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !void {
823
+ var side = battle.side(player);
824
+ var foe = battle.foe(player);
825
+ assert(side.stored().hp == 0);
826
+
827
+ var foe_volatiles = &foe.active.volatiles;
828
+ foe_volatiles.BeatUp = false;
829
+
830
+ // We always need to clear status for Pokémon Showdown's Sleep/Freeze Clause Mod
831
+ const status = side.stored().status;
832
+
833
+ side.stored().status = 0;
834
+ // This shouldn't matter, but Pokémon Showdown decides double switching priority based on speed,
835
+ // and resets a Pokémon's stats when it faints... only it still factors in paralysis -_-
836
+ if (showdown) {
837
+ side.active.stats.spe = if (Status.is(status, .PAR))
838
+ @max(side.stored().stats.spe / 4, 1)
839
+ else
840
+ side.stored().stats.spe;
841
+ }
842
+
843
+ try options.log.faint(battle.active(player), done);
844
+ options.calc.capped(player);
711
845
  }
712
846
 
713
- fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
847
+ fn handleResidual(battle: anytype, player: Player, options: anytype) !bool {
714
848
  var side = battle.side(player);
715
849
  var stored = side.stored();
716
850
  const ident = battle.active(player);
@@ -732,7 +866,7 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
732
866
  stored.hp -|= damage;
733
867
  // Pokémon Showdown uses damageOf here but its not relevant in Generation II
734
868
  try options.log.damage(ident, stored, if (brn) Damage.Burn else Damage.Poison);
735
- if (stored.hp == 0) return;
869
+ if (stored.hp == 0) return true;
736
870
  }
737
871
 
738
872
  if (volatiles.LeechSeed) {
@@ -754,28 +888,30 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
754
888
  foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
755
889
  // Pokémon Showdown uses the less specific heal here instead of drain... because reasons?
756
890
  if (foe_stored.hp > before) try options.log.heal(foe_ident, foe_stored, .Silent);
757
- if (stored.hp == 0) return;
891
+ if (stored.hp == 0) return true;
758
892
  }
759
893
 
760
894
  if (volatiles.Nightmare) {
761
895
  stored.hp -= @max(stored.stats.hp / 4, 1);
762
896
  // ibid
763
- try options.log.damage(ident, stored, Damage.Nightmare);
764
- if (stored.hp == 0) return;
897
+ try options.log.damage(ident, stored, .Nightmare);
898
+ if (stored.hp == 0) return true;
765
899
  }
766
900
 
767
901
  if (volatiles.Curse) {
768
902
  stored.hp -= @max(stored.stats.hp / 4, 1);
769
903
  // ibid
770
- try options.log.damage(ident, stored, Damage.Curse);
771
- if (stored.hp == 0) return;
904
+ try options.log.damage(ident, stored, .Curse);
905
+ if (stored.hp == 0) return true;
772
906
  }
907
+
908
+ return false;
773
909
  }
774
910
 
775
- fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !void {
776
- const players = std.enums.values(Player);
911
+ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !?Result {
912
+ const players = comptime std.enums.values(Player);
777
913
 
778
- // TODO checkFaintThen(p1, p2);
914
+ if (try checkFaint(battle, .P1, options)) |r| return r;
779
915
 
780
916
  // Future Sight
781
917
  inline for (players) |player| {
@@ -791,7 +927,7 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !void {
791
927
  }
792
928
  }
793
929
  }
794
- // TODO checkFaintThen(p1, p2);
930
+ if (try checkFaint(battle, .P1, options)) |r| return r;
795
931
 
796
932
  // Weather
797
933
  if (battle.field.weather != .None) {
@@ -807,12 +943,12 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !void {
807
943
  inline for (players) |player| {
808
944
  var side = battle.side(player);
809
945
  const active = side.active;
810
- if (!(active.volatiles.underground or active.types.sandstormImmune())) {
946
+ if (!(active.volatiles.Underground or active.types.sandstormImmune())) {
811
947
  side.stored().hp -|= @max(side.stored().stats.hp / 8, 1);
812
948
  try options.log.damage(battle.active(player), side.stored(), .Sandstorm);
813
949
  }
814
950
  }
815
- // TODO checkFaintThen(p1, p2);
951
+ if (try checkFaint(battle, .P1, options)) |r| return r;
816
952
  }
817
953
  }
818
954
  }
@@ -822,22 +958,22 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !void {
822
958
  var side = battle.side(player);
823
959
  var volatiles = &side.active.volatiles;
824
960
 
825
- if (volatiles.bind > 0 and !volatiles.Substitute) {
826
- volatiles.bind -= 1;
827
- if (volatiles.bind == 0) {
828
- const reason = @intFromEnum(End.Bind) + volatiles.bind_reason - 1;
961
+ if (volatiles.bind.duration > 0 and !volatiles.Substitute) {
962
+ volatiles.bind.duration -= 1;
963
+ if (volatiles.bind.duration == 0) {
964
+ const reason = @intFromEnum(End.Bind) + volatiles.bind.reason - 1;
829
965
  try options.log.end(battle.active(player), @enumFromInt(reason));
830
966
  } else {
831
- var reason = @intFromEnum(Activate.Bind) + volatiles.bind_reason - 1;
967
+ var reason = @intFromEnum(Activate.Bind) + volatiles.bind.reason - 1;
832
968
  try options.log.activate(battle.active(player), @enumFromInt(reason));
833
969
 
834
- reason = @intFromEnum(Activate.Damage) + volatiles.bind_reason - 1;
970
+ reason = @intFromEnum(Damage.Bind) + volatiles.bind.reason - 1;
835
971
  side.stored().hp -|= @max(side.stored().stats.hp / 16, 1);
836
- try options.log.damage(battle.active(player), side.stored(), reason);
972
+ try options.log.damage(battle.active(player), side.stored(), @enumFromInt(reason));
837
973
  }
838
974
  }
839
975
  }
840
- // TODO checkFaintThen(p1, p2);
976
+ if (try checkFaint(battle, .P1, options)) |r| return r;
841
977
 
842
978
  // Perish Song
843
979
  inline for (players) |player| {
@@ -850,7 +986,7 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !void {
850
986
 
851
987
  assert(volatiles.perish_song < 3);
852
988
  const reason = @intFromEnum(Start.PerishSong0) + volatiles.perish_song;
853
- try options.log.start(battle.active(player), reason);
989
+ try options.log.start(battle.active(player), @enumFromInt(reason));
854
990
 
855
991
  if (volatiles.perish_song == 0) {
856
992
  volatiles.PerishSong = false;
@@ -858,7 +994,7 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !void {
858
994
  }
859
995
  }
860
996
  }
861
- // TODO checkFaintThen(p1, p2);
997
+ if (try checkFaint(battle, .P1, options)) |r| return r;
862
998
 
863
999
  // Leftovers
864
1000
  inline for (players) |player| {
@@ -896,22 +1032,21 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !void {
896
1032
  // Side Conditions
897
1033
  if (showdown) {
898
1034
  inline for (players) |player| {
899
- updateSideCondition(battle, player, "Reflect", "reflect");
1035
+ try updateSideCondition(battle, player, "Reflect", "reflect", options);
900
1036
  }
901
1037
  inline for (players) |player| {
902
- updateSideCondition(battle, player, "LightScreen", "light_screen");
1038
+ try updateSideCondition(battle, player, "LightScreen", "light_screen", options);
903
1039
  }
904
1040
  inline for (players) |player| {
905
- updateSideCondition(battle, player, "Safeguard", "safeguard");
1041
+ try updateSideCondition(battle, player, "Safeguard", "safeguard", options);
906
1042
  }
907
1043
  } else {
908
1044
  inline for (players) |player| {
909
- updateSideCondition(battle, player, "Safeguard", "safeguard");
1045
+ try updateSideCondition(battle, player, "Safeguard", "safeguard", options);
910
1046
  }
911
-
912
1047
  inline for (players) |player| {
913
- updateSideCondition(battle, player, "LightScreen", "light_screen");
914
- updateSideCondition(battle, player, "Reflect", "reflect");
1048
+ try updateSideCondition(battle, player, "LightScreen", "light_screen", options);
1049
+ try updateSideCondition(battle, player, "Reflect", "reflect", options);
915
1050
  }
916
1051
  }
917
1052
 
@@ -984,12 +1119,14 @@ fn betweenTurns(battle: anytype, mslot: u4, options: anytype) !void {
984
1119
  assert(volatiles.encore > 0);
985
1120
  volatiles.encore -= 1;
986
1121
 
987
- if (volatiles.encore == 0 or side.active.moves(mslot).pp == 0) {
1122
+ if (volatiles.encore == 0 or side.active.move(mslot).pp == 0) {
988
1123
  volatiles.Encore = false;
989
1124
  try options.log.end(battle.active(player), .Encore);
990
1125
  }
991
1126
  }
992
1127
  }
1128
+
1129
+ return null;
993
1130
  }
994
1131
 
995
1132
  inline fn updateSideCondition(
@@ -1006,7 +1143,7 @@ inline fn updateSideCondition(
1006
1143
 
1007
1144
  if (@field(side.conditions, value) == 0) {
1008
1145
  @field(side.conditions, key) = false;
1009
- try options.log.sideend(battle.active(player), @field(protocol.Side, key));
1146
+ try options.log.sideend(player, @field(protocol.Side, key));
1010
1147
  }
1011
1148
  }
1012
1149
  }
@@ -1026,206 +1163,1178 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
1026
1163
  return Result.Default;
1027
1164
  }
1028
1165
 
1029
- pub const Effects = struct {
1030
- fn protect(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1031
- var side = battle.side(player);
1032
- var volatiles = &side.active.volatiles;
1033
-
1034
- // TODO or if we didn't go first
1035
- if (volatiles.Substitute) return try options.log.fail(battle.active(player), .None);
1036
-
1037
- // TODO
1038
-
1039
- if (move.effect == .Endure) {
1040
- volatiles.Endure = true;
1166
+ pub fn buildRage(battle: anytype, player: Player, state: *State, options: anytype) !void {
1167
+ var foe = battle.foe(player);
1168
+ if (foe.active.volatiles.Rage) {
1169
+ if (showdown) {
1170
+ // TODO if showdown boost
1171
+ if (foe.active.boosts.atk < 6) {
1172
+ try Effects.boost(battle, player, state, options);
1173
+ }
1041
1174
  } else {
1042
- volatiles.Protect = true;
1175
+ foe.active.volatiles.rage +|= 1;
1176
+ try options.log.activate(battle.active(player), .Rage);
1043
1177
  }
1044
- try options.log.singleturn(battle.active(player), move.id);
1045
1178
  }
1179
+ }
1046
1180
 
1047
- const MAGNITUDE_POWER = [_]u8{ 10, 30, 50, 70, 90, 110, 150 };
1048
- };
1181
+ pub fn rageDamage(battle: anytype, player: Player, state: *State, _: anytype) !void {
1182
+ const volatiles = battle.side(player).active.volatiles;
1183
+ assert(volatiles.Rage);
1184
+ state.damage *|= (volatiles.rage +| 1);
1185
+ }
1049
1186
 
1050
- fn statusModify(status: u8, stats: *Stats(u16)) void {
1051
- if (Status.is(status, .PAR)) {
1052
- stats.spe = @max(stats.spe / 4, 1);
1053
- } else if (Status.is(status, .BRN)) {
1054
- stats.atk = @max(stats.atk / 2, 1);
1187
+ pub fn kingsRock(battle: anytype, player: Player, _: *State, options: anytype) !void {
1188
+ var foe = battle.foe(player);
1189
+ const flinch = battle.side(player).stored().item == .KingsRock and
1190
+ !foe.active.volatiles.Substitute and
1191
+ try Rolls.item(battle, player, options);
1192
+ if (flinch) {
1193
+ foe.active.volatiles.Recharging = false;
1194
+ foe.active.volatiles.Flinch = true;
1055
1195
  }
1056
1196
  }
1057
1197
 
1058
- inline fn isForced(active: anytype) bool {
1059
- return active.volatiles.Recharging or active.volatiles.Rage or
1060
- active.volatiles.Thrashing or active.volatiles.Charging or
1061
- active.volatiles.Rollout;
1062
- }
1198
+ pub fn destinyBond(battle: anytype, player: Player, _: *State, options: anytype) !bool {
1199
+ const foe = battle.foe(player);
1200
+ if (foe.stored().hp > 0) return false;
1063
1201
 
1064
- pub const Rolls = struct {
1065
- inline fn speedTie(battle: anytype, options: anytype) !bool {
1066
- const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
1067
- player == .P1
1068
- else if (showdown)
1069
- battle.rng.range(u8, 0, 2) == 0
1070
- else
1071
- battle.rng.next() < Gen12.percent(50) + 1;
1202
+ if (foe.active.volatiles.DestinyBond) {
1203
+ battle.side(player).stored().hp = 0;
1204
+ try options.log.activate(battle.active(player.foe()), .DestinyBond);
1205
+ }
1206
+ return true;
1207
+ }
1072
1208
 
1073
- try options.chance.speedTie(p1);
1074
- return p1;
1209
+ pub const Effects = struct {
1210
+ pub fn alwaysHit(battle: anytype, player: Player, state: *State, options: anytype) !void {
1211
+ _ = .{ battle, player, state, options }; // TODO
1075
1212
  }
1076
1213
 
1077
- inline fn quickClaw(battle: anytype, player: Player, options: anytype) !bool {
1078
- const qkc = if (options.calc.overridden(player, .quick_claw)) |val|
1079
- val == .true
1080
- else if (showdown)
1081
- battle.rng.chance(u8, 60, 256)
1082
- else
1083
- battle.rng.next() < 60;
1214
+ pub fn attract(battle: anytype, player: Player, _: *State, options: anytype) !void {
1215
+ const side = battle.side(player);
1216
+ var foe = battle.foe(player);
1084
1217
 
1085
- try options.chance.quickClaw(player, qkc);
1086
- return qkc;
1218
+ const gender: u8 = @intFromEnum(side.stored().dvs.gender);
1219
+ const foe_gender: u8 = @intFromEnum(foe.stored().dvs.gender);
1220
+ if (gender + foe_gender != 1) {
1221
+ try options.log.immune(battle.active(player.foe()), .None);
1222
+ return;
1223
+ } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
1224
+ try options.log.lastmiss();
1225
+ try options.log.miss(battle.active(player));
1226
+ return;
1227
+ }
1228
+
1229
+ foe.active.volatiles.Attract = true;
1230
+ try options.log.start(battle.active(player.foe()), .Attract);
1087
1231
  }
1088
1232
 
1089
- inline fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) !bool {
1090
- const ok = if (options.calc.overridden(player, .hit)) |val|
1091
- val == .true
1092
- else if (showdown)
1093
- battle.rng.chance(u8, accuracy, 256)
1094
- else
1095
- battle.rng.next() < accuracy;
1233
+ pub fn batonPass(battle: anytype, player: Player, state: *State, options: anytype) !void {
1234
+ _ = .{ battle, player, state, options }; // TODO
1235
+ }
1096
1236
 
1097
- try options.chance.hit(player, ok, accuracy);
1098
- return ok;
1237
+ pub fn beatUp(battle: anytype, player: Player, state: *State, options: anytype) !void {
1238
+ _ = .{ battle, player, state, options }; // TODO
1099
1239
  }
1100
1240
 
1101
- inline fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
1102
- const crit = if (options.calc.overridden(player, .critical_hit)) |val|
1103
- val == .true
1104
- else if (showdown)
1105
- battle.rng.chance(u8, rate, 256)
1106
- else
1107
- battle.rng.next() < rate;
1241
+ pub fn bellyDrum(battle: anytype, player: Player, state: *State, options: anytype) !void {
1242
+ _ = .{ battle, player, state, options }; // TODO
1243
+ }
1108
1244
 
1109
- try options.chance.criticalHit(player, crit, rate);
1110
- return crit;
1245
+ pub fn bide(battle: anytype, player: Player, state: *State, options: anytype) !void {
1246
+ _ = .{ battle, player, state, options }; // TODO
1111
1247
  }
1112
1248
 
1113
- inline fn damage(battle: anytype, player: anytype, options: anytype) !u8 {
1114
- const roll = if (options.calc.overridden(player, .damage)) |val|
1115
- val
1116
- else roll: {
1117
- if (showdown) break :roll battle.rng.range(u8, 217, 256);
1118
- while (true) {
1119
- const r = std.math.rotr(u8, battle.rng.next(), 1);
1120
- if (r >= 217) break :roll r;
1249
+ pub fn binding(battle: anytype, player: Player, state: *State, options: anytype) !void {
1250
+ _ = .{ battle, player, state, options }; // TODO
1251
+ }
1252
+
1253
+ pub fn burnChance(battle: anytype, player: Player, state: *State, options: anytype) !void {
1254
+ var foe = battle.foe(player);
1255
+ var foe_stored = foe.stored();
1256
+
1257
+ if (foe.active.volatiles.Substitute) return;
1258
+ if (Status.any(foe_stored.status)) {
1259
+ if (Status.is(foe_stored.status, .FRZ)) {
1260
+ assert(Move.get(state.move).type == .Fire);
1261
+ try options.log.curestatus(
1262
+ battle.active(player.foe()),
1263
+ foe_stored.status,
1264
+ .Message,
1265
+ );
1266
+ foe_stored.status = 0;
1121
1267
  }
1122
- };
1268
+ return;
1269
+ }
1270
+ if (!state.proc) return;
1271
+ if (foe.conditions.Safeguard) return;
1272
+ // No need to check for immunity because nothing is immune to Fire-type
1273
+ assert(!state.immune());
1274
+ if (foe.active.types.includes(Move.get(state.move).type)) return;
1123
1275
 
1124
- assert(roll >= 217 and roll <= 255);
1125
- try options.chance.damage(player, roll);
1126
- return roll;
1127
- }
1276
+ foe_stored.status = Status.init(.BRN);
1277
+ foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
1278
+ try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
1128
1279
 
1129
- inline fn confused(battle: anytype, player: Player, options: anytype) !bool {
1130
- const cfz = if (options.calc.overridden(player, .confused)) |val|
1131
- val == .true
1132
- else if (showdown)
1133
- !battle.rng.chance(u8, 128, 256)
1134
- else
1135
- battle.rng.next() >= Gen12.percent(50) + 1;
1280
+ // TODO: check for status berry
1281
+ }
1136
1282
 
1137
- try options.chance.confused(player, cfz);
1138
- return cfz;
1283
+ pub fn confusion(battle: anytype, player: Player, state: *State, options: anytype) !void {
1284
+ _ = .{ battle, player, state, options }; // TODO
1139
1285
  }
1140
1286
 
1141
- inline fn attract(battle: anytype, player: Player, options: anytype) !bool {
1142
- const cant = if (options.calc.overridden(player, .attract)) |val|
1143
- val == .true
1144
- else if (showdown)
1145
- battle.rng.chance(u8, 1, 2)
1146
- else
1147
- battle.rng.next() > Gen12.percent(50) + 1;
1287
+ pub fn conversion(battle: anytype, player: Player, _: *State, options: anytype) !void {
1288
+ var side = battle.side(player);
1148
1289
 
1149
- try options.chance.attract(player, cant);
1150
- return cant;
1290
+ var n: u3 = 0;
1291
+ for (side.active.moves) |m| {
1292
+ if (convertible(side.active, m.id)) n += 1;
1293
+ }
1294
+ if (n == 0) {
1295
+ try options.log.fail(battle.active(player), .None);
1296
+ return;
1297
+ }
1298
+
1299
+ side.active.types.type1 = try Rolls.conversion(battle, player, n, options);
1300
+ side.active.types.type2 = side.active.types.type1;
1151
1301
  }
1152
1302
 
1153
- inline fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
1154
- const par = if (options.calc.overridden(player, .paralyzed)) |val|
1155
- val == .true
1156
- else if (showdown)
1157
- battle.rng.chance(u8, 63, 256)
1158
- else
1159
- battle.rng.next() < Gen12.percent(25);
1303
+ pub fn conversion2(battle: anytype, player: Player, _: *State, options: anytype) !void {
1304
+ var side = battle.side(player);
1160
1305
 
1161
- try options.chance.paralyzed(player, par);
1162
- return par;
1163
- }
1306
+ const last = battle.foe(player).lastMove(false);
1307
+ if (last == .None) {
1308
+ try options.log.fail(battle.active(player), .None);
1309
+ return;
1310
+ }
1311
+ const move = Move.get(last);
1312
+ if (move.type == .@"???") {
1313
+ try options.log.fail(battle.active(player), .None);
1314
+ return;
1315
+ }
1164
1316
 
1165
- inline fn defrost(battle: anytype, player: Player, options: anytype) !bool {
1166
- const thaw = if (options.calc.overridden(player, .defrost)) |val|
1167
- val == .true
1168
- else if (showdown)
1169
- battle.rng.chance(u8, 25, 256)
1170
- else
1171
- battle.rng.next() < Gen12.percent(10);
1317
+ side.active.types.type1 = try Rolls.conversion2(battle, player, move.type, options);
1318
+ side.active.types.type2 = side.active.types.type1;
1319
+ }
1172
1320
 
1173
- try options.chance.defrost(player, thaw);
1174
- return thaw;
1321
+ pub fn counter(battle: anytype, player: Player, state: *State, options: anytype) !void {
1322
+ _ = .{ battle, player, state, options }; // TODO
1175
1323
  }
1176
1324
 
1177
- inline fn secondaryChance(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
1178
- // GLITCH: 100% secondary chance will still fail 1/256 of the time
1179
- const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
1180
- val == .true
1181
- else if (showdown)
1182
- battle.rng.chance(u8, rate, 256)
1183
- else
1184
- battle.rng.next() < rate;
1325
+ pub fn curse(battle: anytype, player: Player, state: *State, options: anytype) !void {
1326
+ _ = .{ battle, player, state, options }; // TODO
1327
+ }
1185
1328
 
1186
- try options.chance.secondaryChance(player, proc, rate);
1187
- return proc;
1329
+ pub fn defenseCurl(battle: anytype, player: Player, state: *State, options: anytype) !void {
1330
+ _ = .{ battle, player, state, options }; // TODO
1188
1331
  }
1189
1332
 
1190
- inline fn focusBand(battle: anytype, player: Player, options: anytype) !bool {
1191
- const proc = if (options.calc.overridden(player, .focus_band)) |val|
1192
- val == .true
1193
- else if (showdown)
1194
- battle.rng.chance(u8, 30, 256)
1195
- else
1196
- battle.rng.next() < 30;
1333
+ pub fn defrost(battle: anytype, player: Player, _: *State, options: anytype) !void {
1334
+ var side = battle.side(player);
1335
+ assert(Status.is(side.stored().status, .FRZ));
1197
1336
 
1198
- try options.chance.focusBand(player, proc);
1199
- return proc;
1337
+ try options.log.curestatus(battle.active(player), side.stored().status, .Message);
1338
+ side.stored().status = 0;
1200
1339
  }
1201
1340
 
1202
- inline fn kingsRock(battle: anytype, player: Player, options: anytype) !bool {
1203
- const proc = if (options.calc.overridden(player, .kings_rock)) |val|
1204
- val == .true
1205
- else if (showdown)
1206
- battle.rng.chance(u8, 30, 256)
1207
- else
1208
- battle.rng.next() < 30;
1209
-
1210
- try options.chance.kingsRock(player, proc);
1211
- return proc;
1341
+ pub fn destinyBond(battle: anytype, player: Player, _: *State, options: anytype) !void {
1342
+ battle.side(player).active.volatiles.DestinyBond = true;
1343
+ try options.log.singlemove(battle.active(player), Move.DestinyBond);
1212
1344
  }
1213
1345
 
1214
- inline fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
1215
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
1216
- @intCast(val)
1217
- else if (showdown)
1218
- @intCast(battle.rng.range(u8, 1, 7))
1219
- else duration: {
1220
- while (true) {
1221
- const r = battle.rng.next() & 7;
1222
- if (r != 0 and r != 7) break :duration @intCast(r);
1346
+ pub fn disable(battle: anytype, player: Player, _: *State, options: anytype) !void {
1347
+ var foe = battle.foe(player);
1348
+ var volatiles = &foe.active.volatiles;
1349
+ const foe_ident = battle.active(player.foe());
1350
+
1351
+ if (volatiles.disable.move != 0) {
1352
+ try options.log.fail(foe_ident, .None);
1353
+ return;
1354
+ }
1355
+
1356
+ const last = battle.foe(player).lastMove(false);
1357
+ if (last == .None or last == .Struggle) {
1358
+ try options.log.fail(foe_ident, .None);
1359
+ return;
1360
+ }
1361
+
1362
+ var slot: u3 = 0;
1363
+ for (foe.active.moves) |m| {
1364
+ assert(m.id != .None);
1365
+ slot += 1;
1366
+ if (m.id == last) break;
1367
+ }
1368
+ const move = foe.active.move(slot);
1369
+ if (move.pp == 0) {
1370
+ try options.log.fail(foe_ident, .None);
1371
+ return;
1372
+ }
1373
+
1374
+ volatiles.disable.move = slot;
1375
+ volatiles.disable.duration = Rolls.disableDuration(battle, player, options);
1376
+
1377
+ try options.log.startEffect(foe_ident, .Disable, move.id);
1378
+ }
1379
+
1380
+ pub fn doubleHit(battle: anytype, player: Player, state: *State, options: anytype) !void {
1381
+ _ = .{ battle, player, state, options }; // TODO
1382
+ }
1383
+
1384
+ pub fn drainHP(battle: anytype, player: Player, state: *State, options: anytype) !void {
1385
+ var stored = battle.side(player).stored();
1386
+
1387
+ assert(state.damage != 0);
1388
+
1389
+ if (stored.hp == stored.stats.hp) return;
1390
+ stored.hp = @min(stored.stats.hp, stored.hp + @max(state.damage / 2, 1));
1391
+
1392
+ try options.log.drain(battle.active(player), stored, battle.active(player.foe()));
1393
+ }
1394
+
1395
+ pub fn encore(battle: anytype, player: Player, state: *State, options: anytype) !void {
1396
+ var foe = battle.foe(player);
1397
+ var volatiles = &foe.active.volatiles;
1398
+ const foe_ident = battle.active(player.foe());
1399
+
1400
+ const last = battle.foe(player).lastMove(true);
1401
+ const failed = volatiles.Encore or
1402
+ (last == .None or last == .Struggle or
1403
+ last == .Encore or .last == .MirrorMove);
1404
+ if (failed) {
1405
+ try options.log.fail(foe_ident, .None);
1406
+ return;
1407
+ }
1408
+
1409
+ var slot: u3 = 0;
1410
+ for (foe.active.moves) |m| {
1411
+ assert(m.id != .None);
1412
+ slot += 1;
1413
+ if (m.id == last) break;
1414
+ }
1415
+ const move = foe.active.move(slot);
1416
+ if (move.pp == 0) {
1417
+ try options.log.fail(foe_ident, .None);
1418
+ return;
1419
+ }
1420
+
1421
+ volatiles.Encore = true;
1422
+ volatiles.encore = Rolls.encoreDuration(battle, player, options);
1423
+
1424
+ // TODO ???
1425
+ _ = state;
1426
+
1427
+ try options.log.startEffect(foe_ident, .Encore, move.id);
1428
+ }
1429
+
1430
+ pub fn explode(battle: anytype, player: Player, state: *State, options: anytype) !void {
1431
+ _ = .{ battle, player, state, options }; // TODO
1432
+ }
1433
+
1434
+ pub fn falseSwipe(battle: anytype, player: Player, state: *State, _: anytype) !void {
1435
+ const foe = battle.foe(player);
1436
+ assert(foe.stored().hp > 0);
1437
+ // FIXME crit?
1438
+ if (state.damage > foe.stored().hp) state.damage = foe.stored().hp - 1;
1439
+ }
1440
+
1441
+ pub fn fixedDamage(battle: anytype, player: Player, state: *State, options: anytype) !void {
1442
+ const side = battle.side(player);
1443
+ const foe = battle.foe(player);
1444
+
1445
+ state.damage = switch (Move.get(state.move).effect) {
1446
+ .SuperFang => @max(foe.stored().hp / 2, 1),
1447
+ .LevelDamage => side.stored().level,
1448
+ .FixedDamage => if (state.move == .SonicBoom) 20 else 40,
1449
+ .Psywave => try Rolls.psywave(
1450
+ battle,
1451
+ player,
1452
+ @intCast(@as(u16, side.stored().level) * 3 / 2),
1453
+ options,
1454
+ ),
1455
+ else => unreachable,
1456
+ };
1457
+ }
1458
+
1459
+ pub fn flameWheel(battle: anytype, player: Player, state: *State, options: anytype) !void {
1460
+ _ = .{ battle, player, state, options }; // TODO
1461
+ }
1462
+
1463
+ pub fn flinchChance(battle: anytype, player: Player, state: *State, options: anytype) !void {
1464
+ _ = .{ battle, player, state, options }; // TODO
1465
+ }
1466
+
1467
+ pub fn flyDig(battle: anytype, player: Player, state: *State, options: anytype) !void {
1468
+ _ = .{ battle, player, state, options }; // TODO
1469
+ }
1470
+
1471
+ pub fn focusEnergy(battle: anytype, player: Player, _: *State, options: anytype) !void {
1472
+ var side = battle.side(player);
1473
+
1474
+ if (side.active.volatiles.FocusEnergy) {
1475
+ if (!showdown) try options.log.fail(battle.active(player), .None);
1476
+ return;
1477
+ }
1478
+
1479
+ side.active.volatiles.FocusEnergy = true;
1480
+ try options.log.start(battle.active(player), .FocusEnergy);
1481
+ }
1482
+
1483
+ var SLOTS: [5]u4 = .{ 0, 0, 0, 0, 0 };
1484
+
1485
+ pub fn forceSwitch(battle: anytype, player: Player, _: *State, options: anytype) !void {
1486
+ const foe = battle.foe(player);
1487
+
1488
+ var n: u4 = 0;
1489
+ var i: u4 = 0;
1490
+ for (foe.pokemon, 0..) |pokemon, j| {
1491
+ if (pokemon.species == .None) break;
1492
+ n += 1;
1493
+ if (pokemon.position == 1) continue;
1494
+ if (pokemon.hp > 0) {
1495
+ SLOTS[i] = @intCast(foe.pokemon[j].position);
1496
+ i += 1;
1497
+ }
1498
+ }
1499
+
1500
+ if (n == 0) {
1501
+ try options.log.fail(battle.active(player), .None);
1502
+ return;
1503
+ }
1504
+
1505
+ _ = try Rolls.forceSwitch(battle, player, SLOTS[0..i], n, options); // TODO
1506
+ }
1507
+
1508
+ pub fn foresight(battle: anytype, player: Player, _: *State, options: anytype) !void {
1509
+ var foe = battle.foe(player);
1510
+
1511
+ if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
1512
+ try options.log.lastmiss();
1513
+ try options.log.miss(battle.active(player));
1514
+ return;
1515
+ } else if (foe.active.volatiles.Foresight) {
1516
+ try options.log.fail(battle.active(player), .None);
1517
+ return;
1518
+ }
1519
+
1520
+ foe.active.volatiles.Foresight = true;
1521
+ try options.log.start(battle.active(player.foe()), .FocusEnergy);
1522
+ }
1523
+
1524
+ pub fn freezeChance(battle: anytype, player: Player, state: *State, options: anytype) !void {
1525
+ var foe = battle.foe(player);
1526
+ var foe_stored = foe.stored();
1527
+ const foe_ident = battle.active(player.foe());
1528
+
1529
+ if (foe.active.volatiles.Substitute) return;
1530
+ if (Status.any(foe_stored.status)) return;
1531
+
1532
+ // No need to check for immunity because nothing is immune to Ice-type
1533
+ assert(!state.immune());
1534
+ if (battle.field.weather == .Sun) return;
1535
+ if (foe.active.types.includes(Move.get(state.move).type)) return;
1536
+ if (!state.proc) return;
1537
+ if (foe.conditions.Safeguard) return;
1538
+ // Freeze Clause Mod
1539
+ if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1540
+
1541
+ foe_stored.status = Status.init(.FRZ);
1542
+ try options.log.status(foe_ident, foe_stored.status, .None);
1543
+
1544
+ // TODO: check for status berry
1545
+
1546
+ foe.active.volatiles.Recharging = false;
1547
+ foe.active.volatiles.frozen = true;
1548
+ }
1549
+
1550
+ pub fn furyCutter(battle: anytype, player: Player, state: *State, options: anytype) !void {
1551
+ _ = .{ battle, player, state, options }; // TODO
1552
+ }
1553
+
1554
+ pub fn futureSight(battle: anytype, player: Player, state: *State, options: anytype) !void {
1555
+ _ = .{ battle, player, state, options }; // TODO
1556
+ }
1557
+
1558
+ pub fn happiness(battle: anytype, player: Player, state: *State, _: anytype) !void {
1559
+ const val = battle.side(player).stored().happiness;
1560
+ const frustration = Move.get(state.move).effect == .Frustration;
1561
+ state.bp = ((if (frustration) 255 - val else val) * 10) / 25;
1562
+ }
1563
+
1564
+ pub fn haze(battle: anytype, player: Player, state: *State, options: anytype) !void {
1565
+ _ = .{ battle, player, state, options }; // TODO
1566
+ }
1567
+
1568
+ pub fn heal(battle: anytype, player: Player, state: *State, options: anytype) !void {
1569
+ _ = .{ battle, player, state, options }; // TODO
1570
+ }
1571
+
1572
+ pub fn healBell(battle: anytype, player: Player, state: *State, options: anytype) !void {
1573
+ _ = .{ battle, player, state, options }; // TODO
1574
+ }
1575
+
1576
+ pub fn hiddenPower(battle: anytype, player: Player, state: *State, options: anytype) !void {
1577
+ _ = .{ battle, player, state, options }; // TODO
1578
+ }
1579
+
1580
+ pub fn hyperBeam(battle: anytype, player: Player, state: *State, options: anytype) !void {
1581
+ _ = .{ battle, player, state, options }; // TODO
1582
+ }
1583
+
1584
+ pub fn jumpKick(battle: anytype, player: Player, state: *State, options: anytype) !void {
1585
+ _ = .{ battle, player, state, options }; // TODO
1586
+ }
1587
+
1588
+ pub fn leechSeed(battle: anytype, player: Player, state: *State, options: anytype) !void {
1589
+ _ = .{ battle, player, state, options }; // TODO
1590
+ }
1591
+
1592
+ pub fn lockOn(battle: anytype, player: Player, state: *State, options: anytype) !void {
1593
+ _ = .{ battle, player, state, options }; // TODO
1594
+ }
1595
+
1596
+ const MAGNITUDE_POWER = [_]u8{ 10, 30, 50, 70, 90, 110, 150 };
1597
+
1598
+ pub fn magnitude(battle: anytype, player: Player, state: *State, options: anytype) !void {
1599
+ _ = .{ battle, player, state, options }; // TODO
1600
+ }
1601
+
1602
+ pub fn meanLook(battle: anytype, player: Player, state: *State, options: anytype) !void {
1603
+ _ = .{ battle, player, state, options }; // TODO
1604
+ }
1605
+
1606
+ pub fn metronome(battle: anytype, player: Player, state: *State, options: anytype) !void {
1607
+ _ = .{ battle, player, state, options }; // TODO
1608
+ }
1609
+
1610
+ pub fn mimic(battle: anytype, player: Player, state: *State, options: anytype) !void {
1611
+ _ = .{ battle, player, state, options }; // TODO
1612
+ }
1613
+
1614
+ pub fn mirrorCoat(battle: anytype, player: Player, state: *State, options: anytype) !void {
1615
+ _ = .{ battle, player, state, options }; // TODO
1616
+ }
1617
+
1618
+ pub fn mirrorMove(battle: anytype, player: Player, state: *State, options: anytype) !void {
1619
+ _ = .{ battle, player, state, options }; // TODO
1620
+ }
1621
+
1622
+ pub fn mist(battle: anytype, player: Player, state: *State, options: anytype) !void {
1623
+ _ = .{ battle, player, state, options }; // TODO
1624
+ }
1625
+
1626
+ pub fn weatherHeal(battle: anytype, player: Player, _: *State, options: anytype) !void {
1627
+ var stored = battle.side(player).stored();
1628
+ const ident = battle.active(player);
1629
+
1630
+ if (stored.hp == stored.stats.hp) return try options.log.fail(ident, .None);
1631
+
1632
+ stored.hp = switch (battle.field.weather) {
1633
+ .Sun => stored.stats.hp,
1634
+ .None => @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2)),
1635
+ else => @min(stored.stats.hp, stored.hp + (stored.stats.hp / 4)),
1636
+ };
1637
+
1638
+ try options.log.heal(ident, stored, Heal.None);
1639
+ }
1640
+
1641
+ pub fn multiHit(battle: anytype, player: Player, state: *State, options: anytype) !void {
1642
+ _ = .{ battle, player, state, options }; // TODO
1643
+ }
1644
+
1645
+ pub fn nightmare(battle: anytype, player: Player, state: *State, options: anytype) !void {
1646
+ _ = .{ battle, player, state, options }; // TODO
1647
+ }
1648
+
1649
+ pub fn ohko(battle: anytype, player: Player, state: *State, options: anytype) !void {
1650
+ _ = .{ battle, player, state, options }; // TODO
1651
+ }
1652
+
1653
+ pub fn painSplit(battle: anytype, player: Player, state: *State, options: anytype) !void {
1654
+ _ = .{ battle, player, state, options }; // TODO
1655
+ }
1656
+
1657
+ pub fn paralyze(battle: anytype, player: Player, state: *State, options: anytype) !void {
1658
+ _ = .{ battle, player, state, options }; // TODO
1659
+ }
1660
+
1661
+ pub fn paralyzeChance(battle: anytype, player: Player, state: *State, options: anytype) !void {
1662
+ var foe = battle.foe(player);
1663
+ var foe_stored = foe.stored();
1664
+
1665
+ if (foe.active.volatiles.Substitute) return;
1666
+ if (Status.any(foe_stored.status)) return;
1667
+ if (state.immune()) return;
1668
+ if (!state.proc) return;
1669
+ if (foe.conditions.Safeguard) return;
1670
+
1671
+ foe_stored.status = Status.init(.PAR);
1672
+ foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
1673
+ try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
1674
+
1675
+ // TODO: check for status berry
1676
+ }
1677
+
1678
+ pub fn payDay(battle: anytype, player: Player, state: *State, options: anytype) !void {
1679
+ _ = .{ battle, player, state, options }; // TODO
1680
+ }
1681
+
1682
+ pub fn perishSong(battle: anytype, player: Player, state: *State, options: anytype) !void {
1683
+ _ = .{ battle, player, state, options }; // TODO
1684
+ }
1685
+
1686
+ pub fn poison(battle: anytype, player: Player, state: *State, options: anytype) !void {
1687
+ var foe = battle.foe(player);
1688
+ var foe_stored = foe.stored();
1689
+ const foe_ident = battle.active(player.foe());
1690
+
1691
+ const toxic = state.move == .Toxic;
1692
+ if (state.immune() or foe.active.types.includes(.Poison)) {
1693
+ return options.log.immune(foe_ident, .None);
1694
+ } else if (Status.any(foe_stored.status)) {
1695
+ // Pokémon Showdown considers Toxic to be a status even in Generation II and so
1696
+ // will not include a fail reason for Toxic vs. Poison or vice-versa...
1697
+ return options.log.fail(foe_ident, if (Status.is(foe_stored.status, .PSN))
1698
+ if (!showdown)
1699
+ .Poison
1700
+ else if (toxic == (foe_stored.status == Status.TOX))
1701
+ if (toxic) .Toxic else .Poison
1702
+ else
1703
+ .None
1704
+ else
1705
+ .None);
1706
+ } else if (foe.active.volatiles.Substitute) {
1707
+ return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
1708
+ } else if (state.miss) {
1709
+ try options.log.lastmiss();
1710
+ try options.log.miss(battle.active(player));
1711
+ }
1712
+
1713
+ foe_stored.status = Status.init(.PSN);
1714
+ if (toxic) {
1715
+ if (showdown) foe_stored.status = Status.TOX;
1716
+ foe.active.volatiles.Toxic = true;
1717
+ foe.active.volatiles.toxic = 0;
1718
+ }
1719
+
1720
+ try options.log.status(foe_ident, foe_stored.status, .None);
1721
+
1722
+ // TODO: check for status berry
1723
+ }
1724
+
1725
+ pub fn poisonChance(battle: anytype, player: Player, state: *State, options: anytype) !void {
1726
+ var foe = battle.foe(player);
1727
+ var foe_stored = foe.stored();
1728
+
1729
+ if (foe.active.volatiles.Substitute) return;
1730
+ if (Status.any(foe_stored.status)) return if (showdown) battle.rng.advance(1);
1731
+ if (state.immune() or foe.active.types.includes(.Poison)) return;
1732
+ if (!state.proc) return;
1733
+ if (foe.conditions.Safeguard) return;
1734
+
1735
+ foe_stored.status = Status.init(.PSN);
1736
+ try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
1737
+
1738
+ // TODO: check for status berry
1739
+ }
1740
+
1741
+ pub fn present(battle: anytype, player: Player, state: *State, options: anytype) !void {
1742
+ _ = .{ battle, player, state, options }; // TODO
1743
+ }
1744
+
1745
+ pub fn protect(battle: anytype, player: Player, state: *State, options: anytype) !void {
1746
+ var side = battle.side(player);
1747
+ var volatiles = &side.active.volatiles;
1748
+
1749
+ const failed = !state.first or volatiles.Substitute or
1750
+ !try Rolls.protect(battle, player, volatiles.protect, options);
1751
+ if (failed) {
1752
+ try options.log.fail(battle.active(player), .None);
1753
+ return;
1754
+ }
1755
+
1756
+ volatiles.protect = @max(8, volatiles.protect + 1);
1757
+ if (state.move == .Endure) {
1758
+ volatiles.Endure = true;
1759
+ } else {
1760
+ volatiles.Protect = true;
1761
+ }
1762
+ try options.log.singleturn(battle.active(player), state.move);
1763
+ }
1764
+
1765
+ pub fn psychUp(battle: anytype, player: Player, state: *State, options: anytype) !void {
1766
+ _ = .{ battle, player, state, options }; // TODO
1767
+ }
1768
+
1769
+ pub fn pursuit(battle: anytype, player: Player, state: *State, options: anytype) !void {
1770
+ _ = .{ battle, player, state, options }; // TODO
1771
+ }
1772
+
1773
+ pub fn rage(battle: anytype, player: Player, _: *State, options: anytype) !void {
1774
+ battle.side(player).active.volatiles.Rage = true;
1775
+ try options.log.singlemove(battle.active(player), Move.Rage);
1776
+ }
1777
+
1778
+ pub fn rainDance(battle: anytype, player: Player, state: *State, options: anytype) !void {
1779
+ _ = .{ battle, player, state, options }; // TODO
1780
+ }
1781
+
1782
+ pub fn rapidSpin(battle: anytype, player: Player, state: *State, options: anytype) !void {
1783
+ _ = .{ battle, player, state, options }; // TODO
1784
+ }
1785
+
1786
+ pub fn razorWind(battle: anytype, player: Player, state: *State, options: anytype) !void {
1787
+ _ = .{ battle, player, state, options }; // TODO
1788
+ }
1789
+
1790
+ pub fn recoil(battle: anytype, player: Player, state: *State, options: anytype) !void {
1791
+ _ = .{ battle, player, state, options }; // TODO
1792
+ }
1793
+
1794
+ pub fn reversal(battle: anytype, player: Player, state: *State, options: anytype) !void {
1795
+ const stored = battle.side(player).stored();
1796
+
1797
+ // Pokémon Showdown Gen II implementation incorrectly inherits Gen IV's table instead
1798
+ state.bp = if (showdown) power: {
1799
+ const x = @max(1, stored.hp * 64 / stored.stats.hp);
1800
+ if (x < 2) break :power 200;
1801
+ if (x < 6) break :power 150;
1802
+ if (x < 13) break :power 100;
1803
+ if (x < 22) break :power 80;
1804
+ if (x < 43) break :power 40;
1805
+ break :power 20;
1806
+ } else power: {
1807
+ const x = @max(1, stored.hp * 48 / stored.stats.hp);
1808
+ if (x < 1) break :power 200;
1809
+ if (x < 4) break :power 150;
1810
+ if (x < 9) break :power 100;
1811
+ if (x < 16) break :power 80;
1812
+ if (x < 32) break :power 40;
1813
+ break :power 20;
1814
+ };
1815
+
1816
+ try calcDamage(battle, player, state, options);
1817
+ }
1818
+
1819
+ pub fn rollout(battle: anytype, player: Player, state: *State, options: anytype) !void {
1820
+ _ = .{ battle, player, state, options }; // TODO
1821
+ }
1822
+
1823
+ pub fn sacredFire(battle: anytype, player: Player, state: *State, options: anytype) !void {
1824
+ _ = .{ battle, player, state, options }; // TODO
1825
+ }
1826
+
1827
+ pub fn safeguard(battle: anytype, player: Player, state: *State, options: anytype) !void {
1828
+ _ = .{ battle, player, state, options }; // TODO
1829
+ }
1830
+
1831
+ pub fn sandstorm(battle: anytype, player: Player, state: *State, options: anytype) !void {
1832
+ _ = .{ battle, player, state, options }; // TODO
1833
+ }
1834
+
1835
+ pub fn screens(battle: anytype, player: Player, state: *State, options: anytype) !void {
1836
+ _ = .{ battle, player, state, options }; // TODO
1837
+ }
1838
+
1839
+ pub fn sketch(battle: anytype, player: Player, state: *State, options: anytype) !void {
1840
+ _ = .{ battle, player, state, options }; // TODO
1841
+ }
1842
+
1843
+ pub fn skullBash(battle: anytype, player: Player, state: *State, options: anytype) !void {
1844
+ _ = .{ battle, player, state, options }; // TODO
1845
+ }
1846
+
1847
+ pub fn skyAttack(battle: anytype, player: Player, state: *State, options: anytype) !void {
1848
+ _ = .{ battle, player, state, options }; // TODO
1849
+ }
1850
+
1851
+ pub fn sleep(battle: anytype, player: Player, state: *State, options: anytype) !void {
1852
+ var foe = battle.foe(player);
1853
+ var foe_stored = foe.stored();
1854
+ const foe_ident = battle.active(player.foe());
1855
+
1856
+ if (Status.any(foe_stored.status)) {
1857
+ return options.log.fail(
1858
+ foe_ident,
1859
+ if (Status.is(foe_stored.status, .SLP)) .Sleep else .None,
1860
+ );
1861
+ }
1862
+
1863
+ if (foe.active.volatiles.Substitute) {
1864
+ return options.log.activateMove(foe_ident, .SubstituteBlock, state.move);
1865
+ }
1866
+
1867
+ // Sleep Clause Mod
1868
+ if (showdown) {
1869
+ for (foe.pokemon) |p| {
1870
+ if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
1871
+ }
1872
+ }
1873
+
1874
+ foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
1875
+ try options.log.statusFrom(foe_ident, foe_stored.status, state.move);
1876
+ }
1877
+
1878
+ pub fn sleepTalk(battle: anytype, player: Player, state: *State, options: anytype) !void {
1879
+ const side = battle.side(player);
1880
+
1881
+ if (!Status.is(side.stored().status, .SLP)) {
1882
+ if (!showdown) try options.log.fail(battle.active(player), .None);
1883
+ return;
1884
+ }
1885
+
1886
+ var n: u3 = 0;
1887
+ for (side.active.moves, 0..) |m, i| {
1888
+ if (state.mslot - 1 != i and Move.get(m.id).extra.sleep_talk) n += 1;
1889
+ }
1890
+ if (n == 0) {
1891
+ try options.log.fail(battle.active(player), .None);
1892
+ return;
1893
+ }
1894
+
1895
+ _ = try Rolls.sleepTalk(battle, player, n, state.mslot, options); // TODO
1896
+ }
1897
+
1898
+ pub fn snore(battle: anytype, player: Player, state: *State, options: anytype) !void {
1899
+ _ = .{ battle, player, state, options }; // TODO
1900
+ }
1901
+
1902
+ pub fn solarBeam(battle: anytype, player: Player, state: *State, options: anytype) !void {
1903
+ _ = .{ battle, player, state, options }; // TODO
1904
+ }
1905
+
1906
+ pub fn spikes(battle: anytype, player: Player, state: *State, options: anytype) !void {
1907
+ _ = .{ battle, player, state, options }; // TODO
1908
+ }
1909
+
1910
+ pub fn spite(battle: anytype, player: Player, state: *State, options: anytype) !void {
1911
+ _ = .{ battle, player, state, options }; // TODO
1912
+ }
1913
+
1914
+ pub fn splash(battle: anytype, player: Player, state: *State, options: anytype) !void {
1915
+ _ = .{ battle, player, state, options }; // TODO
1916
+ }
1917
+
1918
+ pub fn stomp(battle: anytype, player: Player, state: *State, options: anytype) !void {
1919
+ _ = .{ battle, player, state, options }; // TODO
1920
+ }
1921
+
1922
+ pub fn substitute(battle: anytype, player: Player, state: *State, options: anytype) !void {
1923
+ _ = .{ battle, player, state, options }; // TODO
1924
+ }
1925
+
1926
+ pub fn sunnyDay(battle: anytype, player: Player, state: *State, options: anytype) !void {
1927
+ _ = .{ battle, player, state, options }; // TODO
1928
+ }
1929
+
1930
+ pub fn swagger(battle: anytype, player: Player, state: *State, options: anytype) !void {
1931
+ _ = .{ battle, player, state, options }; // TODO
1932
+ }
1933
+
1934
+ pub fn synthesis(battle: anytype, player: Player, state: *State, options: anytype) !void {
1935
+ _ = .{ battle, player, state, options }; // TODO
1936
+ }
1937
+
1938
+ pub fn teleport(battle: anytype, player: Player, state: *State, options: anytype) !void {
1939
+ _ = .{ battle, player, state, options }; // TODO
1940
+ }
1941
+
1942
+ pub fn thief(battle: anytype, player: Player, state: *State, options: anytype) !void {
1943
+ _ = .{ battle, player, state, options }; // TODO
1944
+ }
1945
+
1946
+ pub fn thrashing(battle: anytype, player: Player, state: *State, options: anytype) !void {
1947
+ _ = .{ battle, player, state, options }; // TODO
1948
+ }
1949
+
1950
+ pub fn transform(battle: anytype, player: Player, state: *State, options: anytype) !void {
1951
+ _ = .{ battle, player, state, options }; // TODO
1952
+ }
1953
+
1954
+ pub fn tripleKick(battle: anytype, player: Player, state: *State, options: anytype) !void {
1955
+ _ = .{ battle, player, state, options }; // TODO
1956
+ }
1957
+
1958
+ pub fn twister(battle: anytype, player: Player, state: *State, options: anytype) !void {
1959
+ _ = .{ battle, player, state, options }; // TODO
1960
+ }
1961
+
1962
+ const ALL_STAT_UP_STATS = [_][]const u8{ "atk", "def", "spe", "spa", "spd" };
1963
+ const ALL_STAT_UP_REASONS =
1964
+ [_]Boost{ .Attack, .Defense, .Speed, .SpecialAttack, .SpecialDefense };
1965
+
1966
+ pub fn boost(battle: anytype, player: Player, state: *State, options: anytype) !void {
1967
+ var log = options.log;
1968
+ var side = battle.side(player);
1969
+ const ident = battle.active(player);
1970
+
1971
+ if (state.miss) return try log.fail(ident, .None);
1972
+
1973
+ var stats = &side.active.stats;
1974
+ var boosts = &side.active.boosts;
1975
+
1976
+ const move = Move.get(state.move);
1977
+ switch (move.effect) {
1978
+ .AttackUp1, .AttackUp2, .Rage => {
1979
+ assert(state.proc);
1980
+ assert(boosts.atk >= -6 and boosts.atk <= 6);
1981
+ if (boosts.atk == 6 or stats.atk == MAX_STAT_VALUE) {
1982
+ return try log.fail(ident, .None);
1983
+ }
1984
+ const n: u2 = if (move.effect == .AttackUp2) 2 else 1;
1985
+ boosts.atk = @as(i4, @intCast(@min(6, @as(i8, boosts.atk) + n)));
1986
+ const reason = if (showdown and move.effect == .Rage) Boost.Rage else Boost.Attack;
1987
+ var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
1988
+ const stat = unmodifiedStats(battle, side).atk;
1989
+ stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
1990
+ try log.boost(ident, reason, n);
1991
+ },
1992
+ .DefenseUp1, .DefenseUp2 => {
1993
+ assert(state.proc);
1994
+ assert(boosts.def >= -6 and boosts.def <= 6);
1995
+ if (boosts.def == 6 or stats.def == MAX_STAT_VALUE) {
1996
+ return try log.fail(ident, .None);
1997
+ }
1998
+ const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
1999
+ boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
2000
+ var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2001
+ const stat = unmodifiedStats(battle, side).def;
2002
+ stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2003
+ try log.boost(ident, .Defense, n);
2004
+ },
2005
+ .SpeedUp2 => {
2006
+ assert(state.proc);
2007
+ assert(boosts.spe >= -6 and boosts.spe <= 6);
2008
+ if (boosts.spe == 6 or stats.spe == MAX_STAT_VALUE) {
2009
+ return try log.fail(ident, .None);
2010
+ }
2011
+ boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
2012
+ var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2013
+ const stat = unmodifiedStats(battle, side).spe;
2014
+ stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2015
+ try log.boost(ident, .Speed, 2);
2016
+ },
2017
+ .SpAtkUp1 => {
2018
+ assert(state.proc);
2019
+ assert(boosts.spa >= -6 and boosts.spa <= 6);
2020
+ if (boosts.spa == 6 or stats.spa == MAX_STAT_VALUE) {
2021
+ return try log.fail(ident, .None);
2022
+ }
2023
+ boosts.spa = @intCast(@min(6, @as(i8, boosts.spa) + 1));
2024
+ var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.spa) + 6))];
2025
+ const stat = unmodifiedStats(battle, side).spa;
2026
+ stats.spa = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2027
+ try log.boost(ident, .SpecialAttack, 1);
2028
+ },
2029
+ .SpDefUp2 => {
2030
+ assert(state.proc);
2031
+ assert(boosts.spd >= -6 and boosts.spd <= 6);
2032
+ if (boosts.spd == 6 or stats.spd == MAX_STAT_VALUE) {
2033
+ return try log.fail(ident, .None);
2034
+ }
2035
+ boosts.spd = @intCast(@min(6, @as(i8, boosts.spd) + 2));
2036
+ var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.spd) + 6))];
2037
+ const stat = unmodifiedStats(battle, side).spd;
2038
+ stats.spd = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2039
+ try log.boost(ident, .SpecialDefense, 2);
2040
+ },
2041
+ .EvasionUp1 => {
2042
+ assert(state.proc);
2043
+ assert(boosts.evasion >= -6 and boosts.evasion <= 6);
2044
+ if (boosts.evasion == 6) {
2045
+ return try log.fail(ident, .None);
2046
+ }
2047
+ boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
2048
+ try log.boost(ident, .Evasion, 1);
2049
+ },
2050
+ .AllStatUpChance => {
2051
+ if (!state.proc) return;
2052
+ inline for (ALL_STAT_UP_STATS, ALL_STAT_UP_REASONS) |s, r| {
2053
+ assert(@field(boosts, s) >= -6 and @field(boosts, s) <= 6);
2054
+ if (@field(boosts, s) == 6 or @field(stats, s) == MAX_STAT_VALUE) {
2055
+ try log.boost(ident, r, 0);
2056
+ } else {
2057
+ @field(boosts, s) = @intCast(@min(6, @as(i8, @field(boosts, s)) + 1));
2058
+ var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, @field(boosts, s)) + 6))];
2059
+ const stat = @field(unmodifiedStats(battle, side), s);
2060
+ @field(stats, s) = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2061
+ try log.boost(ident, r, 1);
2062
+ }
2063
+ }
2064
+ },
2065
+ else => unreachable,
2066
+ }
2067
+
2068
+ statusModify(side.stored().status, stats);
2069
+ }
2070
+
2071
+ pub fn unboost(battle: anytype, player: Player, state: *State, options: anytype) !void {
2072
+ var log = options.log;
2073
+ var foe = battle.foe(player);
2074
+ const foe_ident = battle.active(player.foe());
2075
+
2076
+ if (foe.active.volatiles.Mist) {
2077
+ try log.activate(foe_ident, .Mist);
2078
+ return try log.fail(foe_ident, .None);
2079
+ }
2080
+
2081
+ const fail = foe.active.volatiles.Substitute or state.miss or !state.proc;
2082
+
2083
+ var stats = &foe.active.stats;
2084
+ var boosts = &foe.active.boosts;
2085
+
2086
+ const move = Move.get(state.move);
2087
+ switch (move.effect) {
2088
+ .AttackDown1, .AttackDown2, .AttackDownChance => {
2089
+ assert(boosts.atk >= -6 and boosts.atk <= 6);
2090
+ if (fail or boosts.atk == -6) {
2091
+ return if (move.effect != .AttackDownChance) try log.fail(foe_ident, .None);
2092
+ } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2093
+ try options.log.lastmiss();
2094
+ try options.log.miss(battle.active(player));
2095
+ return;
2096
+ } else if (stats.atk == 1) {
2097
+ return if (move.effect != .AttackDownChance) try log.fail(foe_ident, .None);
2098
+ }
2099
+ const n: u2 = if (move.effect == .AttackDown2) 2 else 1;
2100
+ boosts.atk = @intCast(@max(-6, @as(i8, boosts.atk) - n));
2101
+ var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2102
+ const stat = unmodifiedStats(battle, foe).atk;
2103
+ stats.atk = @max(1, stat * mod[0] / mod[1]);
2104
+ try log.boost(foe_ident, .Attack, -@as(i8, n));
2105
+ },
2106
+ .DefenseDown1, .DefenseDown2, .DefenseDownChance => {
2107
+ assert(boosts.def >= -6 and boosts.def <= 6);
2108
+ if (fail or boosts.def == -6) {
2109
+ return if (move.effect != .DefenseDownChance) try log.fail(foe_ident, .None);
2110
+ } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2111
+ try options.log.lastmiss();
2112
+ try options.log.miss(battle.active(player));
2113
+ return;
2114
+ } else if (stats.def == 1) {
2115
+ return if (move.effect != .DefenseDownChance) try log.fail(foe_ident, .None);
2116
+ }
2117
+ const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
2118
+ boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
2119
+ var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2120
+ const stat = unmodifiedStats(battle, foe).def;
2121
+ stats.def = @max(1, stat * mod[0] / mod[1]);
2122
+ try log.boost(foe_ident, .Defense, -@as(i8, n));
2123
+ },
2124
+ .SpeedDown1, .SpeedDown2, .SpeedDownChance => {
2125
+ assert(boosts.spe >= -6 and boosts.spe <= 6);
2126
+ if (fail or boosts.spe == -6) {
2127
+ return if (move.effect != .SpeedDownChance) try log.fail(foe_ident, .None);
2128
+ } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2129
+ try options.log.lastmiss();
2130
+ try options.log.miss(battle.active(player));
2131
+ return;
2132
+ } else if (stats.spe == 1) {
2133
+ return if (move.effect != .SpeedDownChance) try log.fail(foe_ident, .None);
2134
+ }
2135
+ const n: u2 = if (move.effect == .SpeedDown2) 2 else 1;
2136
+ boosts.spe = @intCast(@max(-6, @as(i8, boosts.spe) - 1));
2137
+ var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2138
+ const stat = unmodifiedStats(battle, foe).spe;
2139
+ stats.spe = @max(1, stat * mod[0] / mod[1]);
2140
+ try log.boost(foe_ident, .Speed, -@as(i8, n));
2141
+ assert(boosts.spe >= -6);
2142
+ },
2143
+ .SpDefDownChance => {
2144
+ assert(boosts.spd >= -6 and boosts.spd <= 6);
2145
+ if (fail or boosts.spd == -6) return;
2146
+ if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2147
+ try options.log.lastmiss();
2148
+ try options.log.miss(battle.active(player));
2149
+ return;
2150
+ } else if (stats.spd == 1) return;
2151
+ boosts.spd = @intCast(@max(-6, @as(i8, boosts.spd) - 1));
2152
+ var mod = STAT_BOOSTS[@as(u4, @intCast(@as(i8, boosts.spd) + 6))];
2153
+ const stat = unmodifiedStats(battle, foe).spd;
2154
+ stats.spd = @max(1, stat * mod[0] / mod[1]);
2155
+ try log.boost(foe_ident, .SpecialDefense, -1);
2156
+ },
2157
+ .AccuracyDown1, .AccuracyDownChance => {
2158
+ assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
2159
+ if (fail or boosts.accuracy == -6) {
2160
+ return if (move.effect != .AccuracyDownChance) try log.fail(foe_ident, .None);
2161
+ } else if (foe.active.volatiles.Flying or foe.active.volatiles.Underground) {
2162
+ try options.log.lastmiss();
2163
+ try options.log.miss(battle.active(player));
2164
+ return;
2165
+ }
2166
+ boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
2167
+ try log.boost(foe_ident, .Accuracy, -1);
2168
+ },
2169
+ else => unreachable,
2170
+ }
2171
+
2172
+ statusModify(foe.stored().status, stats);
2173
+ }
2174
+ };
2175
+
2176
+ fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
2177
+ if (!side.active.volatiles.Transform) return &side.stored().stats;
2178
+ const id = ID.from(side.active.volatiles.transform);
2179
+ return &battle.side(id.player).pokemon[id.id - 1].stats;
2180
+ }
2181
+
2182
+ fn statusModify(status: u8, stats: *Stats(u16)) void {
2183
+ if (Status.is(status, .PAR)) {
2184
+ stats.spe = @max(stats.spe / 4, 1);
2185
+ } else if (Status.is(status, .BRN)) {
2186
+ stats.atk = @max(stats.atk / 2, 1);
2187
+ }
2188
+ }
2189
+
2190
+ inline fn isForced(active: anytype) bool {
2191
+ return active.volatiles.Recharging or active.volatiles.Rage or
2192
+ active.volatiles.Thrashing or active.volatiles.Charging or
2193
+ active.volatiles.Rollout;
2194
+ }
2195
+
2196
+ inline fn convertible(active: ActivePokemon, m: Move) bool {
2197
+ if (m == .None) return false;
2198
+ const t = Move.get(m).type;
2199
+ return t != .@"???" and !active.types.includes(t);
2200
+ }
2201
+
2202
+ pub const Rolls = struct {
2203
+ inline fn speedTie(battle: anytype, options: anytype) !bool {
2204
+ const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
2205
+ player == .P1
2206
+ else if (showdown)
2207
+ battle.rng.range(u8, 0, 2) == 0
2208
+ else
2209
+ battle.rng.next() < Gen12.percent(50) + 1;
2210
+
2211
+ try options.chance.speedTie(p1);
2212
+ return p1;
2213
+ }
2214
+
2215
+ inline fn quickClaw(battle: anytype, player: Player, options: anytype) !bool {
2216
+ const qkc = if (options.calc.overridden(player, .quick_claw)) |val|
2217
+ val == .true
2218
+ else if (showdown)
2219
+ battle.rng.chance(u8, 60, 256)
2220
+ else
2221
+ battle.rng.next() < 60;
2222
+
2223
+ try options.chance.quickClaw(player, qkc);
2224
+ return qkc;
2225
+ }
2226
+
2227
+ inline fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) !bool {
2228
+ const ok = if (options.calc.overridden(player, .hit)) |val|
2229
+ val == .true
2230
+ else if (showdown)
2231
+ battle.rng.chance(u8, accuracy, 256)
2232
+ else
2233
+ battle.rng.next() < accuracy;
2234
+
2235
+ try options.chance.hit(player, ok, accuracy);
2236
+ return ok;
2237
+ }
2238
+
2239
+ inline fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2240
+ const crit = if (options.calc.overridden(player, .critical_hit)) |val|
2241
+ val == .true
2242
+ else if (showdown)
2243
+ battle.rng.chance(u8, rate, 256)
2244
+ else
2245
+ battle.rng.next() < rate;
2246
+
2247
+ try options.chance.criticalHit(player, crit, rate);
2248
+ return crit;
2249
+ }
2250
+
2251
+ inline fn damage(battle: anytype, player: Player, options: anytype) !u8 {
2252
+ const roll = if (options.calc.overridden(player, .damage)) |val|
2253
+ val
2254
+ else roll: {
2255
+ if (showdown) break :roll battle.rng.range(u8, 217, 256);
2256
+ while (true) {
2257
+ const r = std.math.rotr(u8, battle.rng.next(), 1);
2258
+ if (r >= 217) break :roll r;
1223
2259
  }
1224
2260
  };
1225
2261
 
1226
- assert(duration >= 1 and duration <= 6);
1227
- options.chance.duration(.sleep, player, player.foe(), duration);
1228
- return duration + 1;
2262
+ assert(roll >= 217 and roll <= 255);
2263
+ try options.chance.damage(player, roll);
2264
+ return roll;
2265
+ }
2266
+
2267
+ inline fn confused(battle: anytype, player: Player, options: anytype) !bool {
2268
+ const cfz = if (options.calc.overridden(player, .confused)) |val|
2269
+ val == .true
2270
+ else if (showdown)
2271
+ !battle.rng.chance(u8, 128, 256)
2272
+ else
2273
+ battle.rng.next() >= Gen12.percent(50) + 1;
2274
+
2275
+ try options.chance.confused(player, cfz);
2276
+ return cfz;
2277
+ }
2278
+
2279
+ inline fn attract(battle: anytype, player: Player, options: anytype) !bool {
2280
+ const cant = if (options.calc.overridden(player, .attract)) |val|
2281
+ val == .true
2282
+ else if (showdown)
2283
+ battle.rng.chance(u8, 1, 2)
2284
+ else
2285
+ battle.rng.next() > Gen12.percent(50) + 1;
2286
+
2287
+ try options.chance.attract(player, cant);
2288
+ return cant;
2289
+ }
2290
+
2291
+ inline fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2292
+ const par = if (options.calc.overridden(player, .paralyzed)) |val|
2293
+ val == .true
2294
+ else if (showdown)
2295
+ battle.rng.chance(u8, 63, 256)
2296
+ else
2297
+ battle.rng.next() < Gen12.percent(25);
2298
+
2299
+ try options.chance.paralyzed(player, par);
2300
+ return par;
2301
+ }
2302
+
2303
+ inline fn defrost(battle: anytype, player: Player, options: anytype) !bool {
2304
+ const thaw = if (options.calc.overridden(player, .defrost)) |val|
2305
+ val == .true
2306
+ else if (showdown)
2307
+ battle.rng.chance(u8, 25, 256)
2308
+ else
2309
+ battle.rng.next() < Gen12.percent(10);
2310
+
2311
+ try options.chance.defrost(player, thaw);
2312
+ return thaw;
2313
+ }
2314
+
2315
+ inline fn secondaryChance(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2316
+ // GLITCH: 100% secondary chance will still fail 1/256 of the time
2317
+ const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2318
+ val == .true
2319
+ else if (showdown)
2320
+ battle.rng.chance(u8, rate, 256)
2321
+ else
2322
+ battle.rng.next() < rate;
2323
+
2324
+ try options.chance.secondaryChance(player, proc, rate);
2325
+ return proc;
2326
+ }
2327
+
2328
+ inline fn item(battle: anytype, player: Player, options: anytype) !bool {
2329
+ const proc = if (options.calc.overridden(player, .item)) |val|
2330
+ val == .true
2331
+ else if (showdown)
2332
+ battle.rng.chance(u8, 30, 256)
2333
+ else
2334
+ battle.rng.next() < 30;
2335
+
2336
+ try options.chance.item(player, proc);
2337
+ return proc;
1229
2338
  }
1230
2339
 
1231
2340
  inline fn triAttack(battle: anytype, player: Player, options: anytype) !u8 {
@@ -1236,7 +2345,7 @@ pub const Rolls = struct {
1236
2345
  else loop: {
1237
2346
  while (true) {
1238
2347
  const r = std.math.rotr(u8, std.battle.rng.next(), 4) & 3;
1239
- if (r != 0) break :loop @intCast(r - 1);
2348
+ if (r != 0) break :loop @enumFromInt(r - 1);
1240
2349
  }
1241
2350
  };
1242
2351
 
@@ -1294,11 +2403,11 @@ pub const Rolls = struct {
1294
2403
  return num;
1295
2404
  }
1296
2405
 
1297
- inline fn tripleKick(battle: anytype, player: Player, options: anytype) !u8 {
2406
+ inline fn tripleKick(battle: anytype, player: Player, options: anytype) !u2 {
1298
2407
  const hits = if (options.calc.overridden(player, .triple_kick)) |val|
1299
2408
  val
1300
2409
  else if (showdown)
1301
- battle.rng.range(u8, 1, 4)
2410
+ battle.rng.range(u2, 1, 4)
1302
2411
  else power: {
1303
2412
  while (true) {
1304
2413
  const r = battle.rng.next() & 3;
@@ -1325,7 +2434,7 @@ pub const Rolls = struct {
1325
2434
  }
1326
2435
 
1327
2436
  // Conversion 2 can at most choose between 7 types (vs. a Grass-type attack)
1328
- const CONVERSION_2: [7]Type = [_]Type{.{}} ** 7;
2437
+ var CONVERSION_2: [7]Type = [_]Type{.@"???"} ** 7;
1329
2438
 
1330
2439
  inline fn conversion2(battle: anytype, player: Player, mtype: Type, options: anytype) !Type {
1331
2440
  var i: u8 = 0;
@@ -1349,7 +2458,7 @@ pub const Rolls = struct {
1349
2458
  if (r > 9 and r < 20) continue; // UNUSED_TYPES
1350
2459
  if (r > 27) continue; // TYPES_END
1351
2460
 
1352
- const t = Types.conversion2(r);
2461
+ const t = Type.conversion2(r);
1353
2462
  if (@intFromEnum(mtype.effectiveness(t)) < neutral) {
1354
2463
  break :ty t;
1355
2464
  }
@@ -1361,47 +2470,299 @@ pub const Rolls = struct {
1361
2470
  return ty;
1362
2471
  }
1363
2472
 
1364
- // TODO thrashingDuration
1365
- // TODO confusionDuraiton (from thrash vs. from move)
1366
- // TODO forceSwitch
1367
- // TODO bindingDuration
1368
- // TODO bideDuration
1369
- // TODO conversion moveSlot
1370
- // TODO disable duration
1371
- // TODO encore duration
1372
- // TODO metronome
1373
- // TODO protect
1374
- // TODO sleeptalk move slot
2473
+ inline fn protect(battle: anytype, player: Player, count: u8, options: anytype) !bool {
2474
+ assert(count <= 8);
2475
+ const num = if (count == 8) 0 else @as(u8, 255) >> @intCast(count);
2476
+
2477
+ const ok = if (options.calc.overridden(player, .protect)) |val|
2478
+ val == .true
2479
+ else if (showdown)
2480
+ battle.rng.chance(u8, num, 255)
2481
+ else protected: {
2482
+ while (true) {
2483
+ const r = battle.rng.next();
2484
+ if (r != 0) break :protected !(num <= r - 1);
2485
+ }
2486
+ };
2487
+
2488
+ try options.chance.protect(player, num, ok);
2489
+ return ok;
2490
+ }
2491
+
2492
+ fn conversion(battle: anytype, player: Player, n: u4, options: anytype) !Type {
2493
+ assert(n > 0);
2494
+
2495
+ const active = battle.side(player).active;
2496
+ const moves = active.moves;
2497
+
2498
+ // TODO: consider throwing error instead of rerolling?
2499
+ const overridden = if (options.calc.overridden(player, .move_slot)) |val|
2500
+ if (convertible(active, moves[val - 1].id)) val else null
2501
+ else
2502
+ null;
2503
+
2504
+ const slot: u4 = overridden orelse slot: {
2505
+ if (showdown) {
2506
+ var r = battle.rng.range(u4, 0, n) + 1;
2507
+ var i: usize = 0;
2508
+ while (i < moves.len and r > 0) : (i += 1) {
2509
+ if (convertible(active, moves[i].id)) {
2510
+ r -= 1;
2511
+ if (r == 0) break :slot @intCast(i + 1);
2512
+ }
2513
+ }
2514
+ break :slot @intCast(i + 1);
2515
+ } else {
2516
+ while (true) {
2517
+ const r: u4 = @intCast(battle.rng.next() & 3);
2518
+ if (convertible(active, moves[r].id)) break :slot @intCast(r + 1);
2519
+ }
2520
+ }
2521
+ };
2522
+
2523
+ assert(n >= 1 and n <= 4);
2524
+ assert(slot >= 1 and slot <= 4);
2525
+ try options.chance.moveSlot(player, slot, n);
2526
+ return Move.get(active.move(slot).id).type;
2527
+ }
2528
+
2529
+ fn sleepTalk(battle: anytype, player: Player, n: u4, mslot: u4, options: anytype) !u4 {
2530
+ assert(n > 0);
2531
+
2532
+ const active = battle.side(player).active;
2533
+ const moves = active.moves;
2534
+
2535
+ // TODO: consider throwing error instead of rerolling?
2536
+ const overridden = if (options.calc.overridden(player, .move_slot)) |val|
2537
+ if (val != mslot and Move.get(moves[val - 1].id).extra.sleep_talk) val else null
2538
+ else
2539
+ null;
2540
+
2541
+ const slot: u4 = overridden orelse slot: {
2542
+ if (showdown) {
2543
+ var r = battle.rng.range(u4, 0, n) + 1;
2544
+ var i: usize = 0;
2545
+ while (i < moves.len and r > 0) : (i += 1) {
2546
+ if (i + 1 != mslot and Move.get(moves[i].id).extra.sleep_talk) {
2547
+ r -= 1;
2548
+ if (r == 0) break :slot @intCast(i + 1);
2549
+ }
2550
+ }
2551
+ break :slot @intCast(i + 1);
2552
+ } else {
2553
+ while (true) {
2554
+ const r: u4 = @intCast(battle.rng.next() & 3);
2555
+ if (r + 1 == mslot or !Move.get(moves[r].id).extra.sleep_talk) continue;
2556
+ break :slot @intCast(r + 1);
2557
+ }
2558
+ }
2559
+ };
2560
+
2561
+ assert(n >= 1 and n <= 4);
2562
+ assert(slot >= 1 and slot <= 4);
2563
+ try options.chance.moveSlot(player, slot, n);
2564
+ return slot;
2565
+ }
2566
+
2567
+ inline fn forceSwitch(
2568
+ battle: anytype,
2569
+ player: Player,
2570
+ slots: []u4,
2571
+ n: u4,
2572
+ options: anytype,
2573
+ ) !u4 {
2574
+ const foe = battle.foe(player);
2575
+
2576
+ assert(slots.len < 5);
2577
+ assert(n > 0 and n <= 6);
2578
+
2579
+ // TODO: consider throwing error instead of rerolling?
2580
+ const overridden = if (options.calc.overridden(player, .force_switch)) |val| val: {
2581
+ for (slots) |slot| if (slot == val) break :val if (foe.get(slot).hp > 0) val else null;
2582
+ break :val null;
2583
+ } else null;
2584
+
2585
+ const slot: u4 = overridden orelse slot: {
2586
+ if (showdown) {
2587
+ break :slot slots[battle.rng.range(u4, 0, @as(u4, @intCast(slots.len)))];
2588
+ } else {
2589
+ while (true) {
2590
+ const r = battle.rng.next() & 7;
2591
+ if (r > n or r == 1) continue;
2592
+ if (foe.get(r).hp > 0) break :slot @intCast(r);
2593
+ }
2594
+ }
2595
+ };
2596
+
2597
+ assert(n >= 1 and n <= 6);
2598
+ assert(slot >= 1 and slot <= 6);
2599
+ try options.chance.forceSwitch(player, slot, n);
2600
+ return slot;
2601
+ }
2602
+
2603
+ const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
2604
+
2605
+ inline fn multiHit(battle: anytype, player: Player, options: anytype) !u3 {
2606
+ const n: u3 = if (options.calc.overridden(player, .multi_hit)) |val|
2607
+ @intCast(val)
2608
+ else if (showdown)
2609
+ DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
2610
+ else n: {
2611
+ const r = (battle.rng.next() & 3);
2612
+ break :n @intCast((if (r < 2) r else battle.rng.next() & 3) + 2);
2613
+ };
2614
+
2615
+ assert(n >= 2 and n <= 5);
2616
+ try options.chance.multiHit(player, n);
2617
+ return n;
2618
+ }
1375
2619
 
1376
2620
  inline fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
1377
2621
  const power = if (options.calc.overridden(player, .psywave)) |val|
1378
- val - 1
2622
+ val
1379
2623
  else if (showdown)
1380
- battle.rng.range(u8, 0, max)
2624
+ battle.rng.range(u8, 1, max)
1381
2625
  else power: {
1382
2626
  while (true) {
1383
2627
  const r = battle.rng.next();
1384
- if (r < max) break :power r;
2628
+ if (r != 0 and r < max) break :power r;
1385
2629
  }
1386
2630
  };
1387
2631
 
1388
- assert(power < max);
2632
+ assert(power != 0 and power < max);
1389
2633
  try options.chance.psywave(player, power, max);
1390
2634
  return power;
1391
2635
  }
1392
2636
 
1393
- const METRONOME = init: {
1394
- var num = 0;
1395
- var moves: [238]Move = undefined;
1396
- for (1..Move.size) |i| {
1397
- if (Move.get(@enumFromInt(i)).extra.metronome) {
1398
- moves[num] = @enumFromInt(i);
1399
- num += 1;
1400
- }
2637
+ inline fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2638
+ const moves = battle.get(player).active.moves;
2639
+
2640
+ var n: u2 = 0;
2641
+ for (moves) |m| {
2642
+ if (m == .None) break;
2643
+ if (Move.get(m).metronome) n += 1;
1401
2644
  }
1402
- assert(num == moves.len);
1403
- break :init moves;
1404
- };
2645
+
2646
+ // TODO: consider throwing error instead of rerolling?
2647
+ const overridden = if (options.calc.overridden(player, .metronome)) |val| val: {
2648
+ for (moves) |m| if (m.id == val) break :val null;
2649
+ break :val val;
2650
+ } else null;
2651
+
2652
+ const move: Move = overridden orelse move: {
2653
+ if (showdown) {
2654
+ var r = battle.rng.range(u8, 0, Move.METRONOME.len - n);
2655
+ for (moves) |m| {
2656
+ if (m == .None or @intFromEnum(m) > @intFromEnum(Move.METRONOME[r])) break;
2657
+ if (Move.get(m).metronome) r -= 1;
2658
+ }
2659
+ break :move Move.METRONOME[r];
2660
+ } else {
2661
+ outer: while (true) {
2662
+ const r = battle.rng.next();
2663
+ if (r == 0 or r >= Move.size) continue;
2664
+ const selected: Move = @enumFromInt(r);
2665
+ if (!Move.get(selected).extra.metronome) continue;
2666
+ for (moves) |m| if (m.id == selected) continue :outer;
2667
+ break :move selected;
2668
+ }
2669
+ }
2670
+ };
2671
+
2672
+ assert(Move.get(move).metronome);
2673
+ try options.chance.metronome(player, move, n);
2674
+ return move;
2675
+ }
2676
+
2677
+ inline fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2678
+ const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2679
+ @intCast(val)
2680
+ else if (showdown)
2681
+ battle.rng.range(u3, 1, 7)
2682
+ else duration: {
2683
+ while (true) {
2684
+ const r = battle.rng.next() & 7;
2685
+ if (r != 0 and r != 7) break :duration @intCast(r);
2686
+ }
2687
+ };
2688
+
2689
+ assert(duration >= 1 and duration <= 6);
2690
+ options.chance.duration(.sleep, player, player.foe(), duration);
2691
+ return duration + 1;
2692
+ }
2693
+
2694
+ inline fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
2695
+ const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2696
+ @intCast(val)
2697
+ else if (showdown)
2698
+ // Pokémon Showdown incorrectly uses the same duration for self-confusion
2699
+ battle.rng.range(u3, 2, 6)
2700
+ else
2701
+ @intCast((battle.rng.next() & if (self) 1 else 3) + 2);
2702
+
2703
+ assert(duration >= 2 and duration <= if (!showdown and self) 3 else 5);
2704
+ options.chance.duration(.confusion, player, if (self) player else player.foe(), duration);
2705
+ return duration;
2706
+ }
2707
+
2708
+ inline fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2709
+ const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
2710
+ @intCast(val)
2711
+ else if (showdown)
2712
+ // Pokémon Showdown incorrectly inherits Generation III's Disable duration
2713
+ battle.rng.range(u4, 2, 6)
2714
+ else duration: {
2715
+ while (true) {
2716
+ const r = battle.rng.next() & 7;
2717
+ if (r != 0) break :duration @intCast(r + 1);
2718
+ }
2719
+ };
2720
+
2721
+ assert(duration >= 2 and duration <= (if (showdown) 5 else 7));
2722
+ options.chance.duration(.disable, player, player.foe(), duration);
2723
+ return duration;
2724
+ }
2725
+
2726
+ inline fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2727
+ const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2728
+ @intCast(val)
2729
+ else if (showdown)
2730
+ battle.rng.range(u3, 2, 4)
2731
+ else
2732
+ @intCast((battle.rng.next() & 1) + 2); // BUG: thrash is + 1 not +2
2733
+
2734
+ assert(duration >= 2 and duration <= 3);
2735
+ options.chance.duration(.attacking, player, player, duration);
2736
+ return duration;
2737
+ }
2738
+
2739
+ inline fn bindingDuration(battle: anytype, player: Player, options: anytype) u3 {
2740
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
2741
+ @intCast(val)
2742
+ else if (showdown)
2743
+ // TODO: PS does range(3, 6) here which after subtracting 1 is actually 2-4 not 2-5!
2744
+ battle.rng.range(u3, 2, 5)
2745
+ else
2746
+ @intCast(battle.rng.next() & 3 + 2));
2747
+
2748
+ assert(duration >= 2 and duration <= 5);
2749
+ options.chance.duration(.binding, player, player.foe(), duration);
2750
+ return duration + 1;
2751
+ }
2752
+
2753
+ // TODO: consider sharing implementation with bindingDuration
2754
+ inline fn encoreDuration(battle: anytype, player: Player, options: anytype) u3 {
2755
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
2756
+ @intCast(val)
2757
+ else if (showdown)
2758
+ battle.rng.range(u3, 2, 6)
2759
+ else
2760
+ @intCast(battle.rng.next() & 3 + 2));
2761
+
2762
+ assert(duration >= 2 and duration <= 5);
2763
+ options.chance.duration(.encore, player, player.foe(), duration);
2764
+ return duration + 1;
2765
+ }
1405
2766
  };
1406
2767
 
1407
2768
  test "RNG agreement" {
@@ -1466,6 +2827,28 @@ test "Roll probabilities" {
1466
2827
  try expectEqualSlices(u8, &.{ 14, 26, 50, 77, 51, 25, 13 }, &magnitude);
1467
2828
  }
1468
2829
 
2830
+ test "Reasons" {
2831
+ const moves = [_]Move{ .Bind, .Wrap, .FireSpin, .Clamp, .Whirlpool };
2832
+ for (moves) |move| {
2833
+ const reason = Move.get(move).extra.protocol - 1;
2834
+
2835
+ const activate: Activate = @enumFromInt(@intFromEnum(Activate.Bind) + reason);
2836
+ try expectEqualStrings(@tagName(move), @tagName(activate));
2837
+
2838
+ const damage: Damage = @enumFromInt(@intFromEnum(Damage.Bind) + reason);
2839
+ try expectEqualStrings(@tagName(move), @tagName(damage));
2840
+
2841
+ const end: End = @enumFromInt(@intFromEnum(End.Bind) + reason);
2842
+ try expectEqualStrings(@tagName(move), @tagName(end));
2843
+ }
2844
+
2845
+ const perish_song = [_]Start{ .PerishSong0, .PerishSong1, .PerishSong2, .PerishSong3 };
2846
+ for (perish_song, 0..) |ps, i| {
2847
+ const reason: Start = @enumFromInt(@intFromEnum(Start.PerishSong0) + i);
2848
+ try expectEqualStrings(@tagName(ps), @tagName(reason));
2849
+ }
2850
+ }
2851
+
1469
2852
  pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Choice) u8 {
1470
2853
  var n: u8 = 0;
1471
2854
  switch (request) {
@@ -1544,7 +2927,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
1544
2927
  if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
1545
2928
  // Pokémon Showdown displays Struggle if limited to Bide but unable to pick
1546
2929
  const struggle =
1547
- m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);
2930
+ m.id == .Bide and (m.pp == 0 or active.volatiles.disable.move == slot);
1548
2931
  const s = if (struggle) 0 else slot;
1549
2932
  out[n] = .{ .type = .Move, .data = s };
1550
2933
  n += 1;
@@ -1559,7 +2942,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
1559
2942
  const m = active.moves[slot - 1];
1560
2943
  if (m.id == .None) break;
1561
2944
  if (m.pp == 0) continue;
1562
- if (active.volatiles.disabled_move == slot) continue;
2945
+ if (active.volatiles.disable.move == slot) continue;
1563
2946
  out[n] = .{ .type = .Move, .data = slot };
1564
2947
  n += 1;
1565
2948
  }