@pkmn/engine 0.1.0-dev.dc33f678 → 0.1.0-dev.deda8593

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