@pkmn/engine 0.1.0-dev.305a932f → 0.1.0-dev.30895c5a

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