@pkmn/engine 0.1.0-dev.cf6f5d2c → 0.1.0-dev.d3e796d5

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 (47) hide show
  1. package/README.md +55 -44
  2. package/build/pkg/addon.js +3 -2
  3. package/build/pkg/addon.js.map +1 -1
  4. package/build/pkg/data/layout.json +25 -3
  5. package/build/pkg/gen1.d.ts +2 -1
  6. package/build/pkg/gen1.js +36 -18
  7. package/build/pkg/gen1.js.map +1 -1
  8. package/build/pkg/index.d.ts +4 -2
  9. package/build/pkg/index.js.map +1 -1
  10. package/build/pkg/protocol.d.ts +4 -2
  11. package/build/pkg/protocol.js +4 -2
  12. package/build/pkg/protocol.js.map +1 -1
  13. package/build/test/display.js +7 -3
  14. package/build/test/display.js.map +1 -1
  15. package/build.zig +86 -62
  16. package/package.json +25 -16
  17. package/src/bin/install-pkmn-engine +10 -5
  18. package/src/lib/binding/node.zig +45 -140
  19. package/src/lib/binding/wasm.zig +4 -2
  20. package/src/lib/common/battle.zig +17 -0
  21. package/src/lib/common/data.zig +11 -11
  22. package/src/lib/common/debug.zig +20 -11
  23. package/src/lib/common/js.zig +118 -0
  24. package/src/lib/common/optional.zig +60 -0
  25. package/src/lib/common/options.zig +9 -0
  26. package/src/lib/common/protocol.zig +277 -159
  27. package/src/lib/common/rational.zig +353 -0
  28. package/src/lib/common/rng.zig +10 -10
  29. package/src/lib/common/util.zig +12 -0
  30. package/src/lib/gen1/README.md +239 -151
  31. package/src/lib/gen1/calc.zig +426 -0
  32. package/src/lib/gen1/chance.zig +1044 -0
  33. package/src/lib/gen1/data/moves.zig +10 -10
  34. package/src/lib/gen1/data/species.zig +2 -2
  35. package/src/lib/gen1/data/types.zig +3 -3
  36. package/src/lib/gen1/data.zig +64 -38
  37. package/src/lib/gen1/helpers.zig +592 -35
  38. package/src/lib/gen1/mechanics.zig +804 -500
  39. package/src/lib/gen2/README.md +87 -13
  40. package/src/lib/gen2/data/items.zig +3 -3
  41. package/src/lib/gen2/data/moves.zig +16 -3
  42. package/src/lib/gen2/data/species.zig +1 -1
  43. package/src/lib/gen2/data/types.zig +3 -3
  44. package/src/lib/gen2/data.zig +73 -38
  45. package/src/lib/gen2/helpers.zig +15 -16
  46. package/src/lib/gen2/mechanics.zig +476 -11
  47. package/src/lib/pkmn.zig +61 -2
@@ -1,11 +1,14 @@
1
1
  const std = @import("std");
2
2
 
3
+ const pkmn = @import("../pkmn.zig");
4
+
3
5
  const common = @import("../common/data.zig");
4
6
  const DEBUG = @import("../common/debug.zig").print;
5
- const options = @import("../common/options.zig");
6
7
  const protocol = @import("../common/protocol.zig");
7
8
  const rng = @import("../common/rng.zig");
8
9
 
10
+ const Action = @import("chance.zig").Action;
11
+
9
12
  const data = @import("data.zig");
10
13
 
11
14
  const assert = std.debug.assert;
@@ -17,7 +20,7 @@ const ID = common.ID;
17
20
  const Player = common.Player;
18
21
  const Result = common.Result;
19
22
 
20
- const showdown = options.showdown;
23
+ const showdown = pkmn.options.showdown;
21
24
 
22
25
  const Boost = protocol.Boost;
23
26
  const Damage = protocol.Damage;
@@ -58,9 +61,13 @@ const MAX_STAT_VALUE = 999;
58
61
 
59
62
  const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
60
63
 
61
- pub fn update(battle: anytype, c1: Choice, c2: Choice, log: anytype) !Result {
64
+ pub fn update(battle: anytype, c1: Choice, c2: Choice, options: anytype) !Result {
65
+ // FIXME
66
+ // std.debug.print("\n", .{});
67
+ // DEBUG(battle.turn);
68
+ // std.debug.print("// ", .{});
62
69
  assert(c1.type != .Pass or c2.type != .Pass or battle.turn == 0);
63
- if (battle.turn == 0) return start(battle, log);
70
+ if (battle.turn == 0) return start(battle, options);
64
71
 
65
72
  var s1 = false;
66
73
  var s2 = false;
@@ -76,19 +83,19 @@ pub fn update(battle: anytype, c1: Choice, c2: Choice, log: anytype) !Result {
76
83
  var r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
77
84
  var r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
78
85
 
79
- if (turnOrder(battle, c1, c2) == .P1) {
80
- if (try doTurn(battle, .P1, c1, r1, s1, f1, .P2, c2, r2, s2, f2, log)) |r| return r;
86
+ if (try turnOrder(battle, c1, c2, options) == .P1) {
87
+ if (try doTurn(battle, .P1, c1, r1, s1, f1, .P2, c2, r2, s2, f2, options)) |r| return r;
81
88
  } else {
82
- if (try doTurn(battle, .P2, c2, r2, s2, f2, .P1, c1, r1, s1, f1, log)) |r| return r;
89
+ if (try doTurn(battle, .P2, c2, r2, s2, f2, .P1, c1, r1, s1, f1, options)) |r| return r;
83
90
  }
84
91
 
85
92
  if (p1.active.volatiles.attacks == 0) p1.active.volatiles.Binding = false;
86
93
  if (p2.active.volatiles.attacks == 0) p2.active.volatiles.Binding = false;
87
94
 
88
- return endTurn(battle, log);
95
+ return endTurn(battle, options);
89
96
  }
90
97
 
91
- fn start(battle: anytype, log: anytype) !Result {
98
+ fn start(battle: anytype, options: anytype) !Result {
92
99
  const p1 = battle.side(.P1);
93
100
  const p2 = battle.side(.P2);
94
101
 
@@ -100,10 +107,10 @@ fn start(battle: anytype, log: anytype) !Result {
100
107
  assert(!showdown or p2_slot == 1);
101
108
  if (p2_slot == 0) return Result.Win;
102
109
 
103
- try switchIn(battle, .P1, p1_slot, true, log);
104
- try switchIn(battle, .P2, p2_slot, true, log);
110
+ try switchIn(battle, .P1, p1_slot, true, options);
111
+ try switchIn(battle, .P2, p2_slot, true, options);
105
112
 
106
- return endTurn(battle, log);
113
+ return endTurn(battle, options);
107
114
  }
108
115
 
109
116
  fn findFirstAlive(side: *const Side) u8 {
@@ -126,7 +133,7 @@ fn selectMove(
126
133
  const stored = side.stored();
127
134
 
128
135
  assert(!isForced(side.active) or
129
- (choice.type == .Move and choice.data == @boolToInt(showdown)));
136
+ (choice.type == .Move and choice.data == @intFromBool(showdown)));
130
137
 
131
138
  // pre-battle menu
132
139
  if (volatiles.Recharging) {
@@ -179,11 +186,7 @@ fn selectMove(
179
186
  assert(choice.data == 0);
180
187
  // GLITCH: https://glitchcity.wiki/Partial_trapping_move_Mirror_Move_link_battle_glitch
181
188
  if (foe_choice.type == .Switch) {
182
- const slot = if (player == .P1)
183
- battle.last_selected_indexes.p1
184
- else
185
- battle.last_selected_indexes.p2;
186
- const last = side.active.move(@intCast(u4, slot));
189
+ const last = side.active.move(battle.lastMove(player).index);
187
190
  if (last.id == .Metronome) side.last_selected_move = last.id;
188
191
  if (last.id == .MirrorMove) return Result.Error;
189
192
  }
@@ -211,12 +214,9 @@ fn selectMove(
211
214
  }
212
215
  break :ok true;
213
216
  };
214
-
215
217
  assert(struggle);
216
- saveMove(battle, player, choice);
217
- } else {
218
- saveMove(battle, player, choice);
219
218
  }
219
+ saveMove(battle, player, choice);
220
220
 
221
221
  return null;
222
222
  }
@@ -236,16 +236,12 @@ fn saveMove(battle: anytype, player: Player, choice: ?Choice) void {
236
236
  assert(showdown or move.pp != 0);
237
237
 
238
238
  side.last_selected_move = move.id;
239
- if (player == .P1) {
240
- battle.last_selected_indexes.p1 = @intCast(u4, c.data);
241
- } else {
242
- battle.last_selected_indexes.p2 = @intCast(u4, c.data);
243
- }
239
+ battle.lastMove(player).index = @intCast(c.data);
244
240
  }
245
241
  }
246
242
  }
247
243
 
248
- fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, log: anytype) !void {
244
+ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: anytype) !void {
249
245
  var side = battle.side(player);
250
246
  var foe = battle.foe(player);
251
247
  var active = &side.active;
@@ -258,11 +254,7 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, log: anyty
258
254
  side.order[0] = side.order[slot - 1];
259
255
  side.order[slot - 1] = out;
260
256
 
261
- if (player == .P1) {
262
- battle.last_selected_indexes.p1 = 1;
263
- } else {
264
- battle.last_selected_indexes.p2 = 1;
265
- }
257
+ battle.lastMove(player).index = 1;
266
258
 
267
259
  side.last_used_move = .None;
268
260
  foe.last_used_move = .None;
@@ -278,17 +270,18 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, log: anyty
278
270
 
279
271
  foe.active.volatiles.Binding = false;
280
272
 
281
- try log.switched(battle.active(player), incoming);
273
+ try options.log.switched(battle.active(player), incoming);
274
+ options.chance.switched(player, side.order[0], out);
282
275
 
283
276
  if (showdown and incoming.status == Status.TOX) {
284
277
  incoming.status = Status.init(.PSN);
285
278
  // Technically, Pokémon Showdown adds these after *both* Pokémon have switched, but we'd
286
279
  // rather not clutter up turnOrder just for this (incorrect) log message
287
- try log.status(battle.active(player), incoming.status, .Silent);
280
+ try options.log.status(battle.active(player), incoming.status, .Silent);
288
281
  }
289
282
  }
290
283
 
291
- fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
284
+ fn turnOrder(battle: anytype, c1: Choice, c2: Choice, options: anytype) !Player {
292
285
  assert(c1.type != .Pass or c2.type != .Pass);
293
286
 
294
287
  if (c1.type == .Pass) return .P2;
@@ -318,9 +311,11 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
318
311
  const spe2 = battle.side(.P2).active.stats.spe;
319
312
  if (spe1 == spe2) {
320
313
  // Pokémon Showdown's beforeTurnCallback shenanigans
321
- if (showdown and m1 == .Counter and m2 == .Counter) battle.rng.advance(1);
314
+ if (showdown and !double_switch and m1 == .Counter and m2 == .Counter) {
315
+ battle.rng.advance(1);
316
+ }
322
317
 
323
- const p1 = Rolls.speedTie(battle);
318
+ const p1 = try Rolls.speedTie(battle, options);
324
319
  if (!showdown) return if (p1) .P1 else .P2;
325
320
 
326
321
  // Pokémon Showdown's "lockedmove" volatile's onBeforeTurn uses BattleQueue#changeAction,
@@ -351,7 +346,7 @@ fn doTurn(
351
346
  foe_rewrap: bool,
352
347
  foe_skip: bool,
353
348
  foe_from: ?Move,
354
- log: anytype,
349
+ options: anytype,
355
350
  ) !?Result {
356
351
  assert(player_choice.type != .Pass);
357
352
 
@@ -365,19 +360,22 @@ fn doTurn(
365
360
  player_skip,
366
361
  player_from,
367
362
  &residual,
368
- log,
363
+ options,
369
364
  )) |r| return r;
370
365
  if (!replace) {
371
366
  if (player_choice.type != .Switch) {
372
- if (try checkFaint(battle, foe_player, log)) |r| return r;
367
+ if (try checkFaint(battle, foe_player, options)) |r| return r;
373
368
  }
374
- if (residual) try handleResidual(battle, player, log);
375
- if (try checkFaint(battle, player, log)) |r| return r;
369
+ if (residual) try handleResidual(battle, player, options);
370
+ if (try checkFaint(battle, player, options)) |r| return r;
376
371
  } else if (foe_choice.type == .Pass) return null;
377
372
 
373
+ options.chance.clearPending();
374
+
378
375
  residual = true;
379
376
  replace = battle.side(foe_player).stored().hp == 0;
380
- if (try executeMove(
377
+ const calc = pkmn.options.calc and foe_choice.type == .Pass;
378
+ if (if (calc) null else try executeMove(
381
379
  battle,
382
380
  foe_player,
383
381
  foe_choice,
@@ -385,14 +383,14 @@ fn doTurn(
385
383
  foe_skip,
386
384
  foe_from,
387
385
  &residual,
388
- log,
386
+ options,
389
387
  )) |r| return r;
390
388
  if (!replace) {
391
- if (foe_choice.type != .Switch) {
392
- if (try checkFaint(battle, player, log)) |r| return r;
389
+ if (!calc and foe_choice.type != .Switch) {
390
+ if (try checkFaint(battle, player, options)) |r| return r;
393
391
  }
394
- if (residual) try handleResidual(battle, foe_player, log);
395
- if (try checkFaint(battle, foe_player, log)) |r| return r;
392
+ if (residual) try handleResidual(battle, foe_player, options);
393
+ if (try checkFaint(battle, foe_player, options)) |r| return r;
396
394
  }
397
395
 
398
396
  // Flinch is bugged on Pokémon Showdown because it gets implemented with a duration which causes
@@ -413,12 +411,12 @@ fn executeMove(
413
411
  skip: bool,
414
412
  from: ?Move,
415
413
  residual: *bool,
416
- log: anytype,
414
+ options: anytype,
417
415
  ) !?Result {
418
416
  var side = battle.side(player);
419
417
 
420
418
  if (choice.type == .Switch) {
421
- try switchIn(battle, player, @intCast(u4, choice.data), false, log);
419
+ try switchIn(battle, player, choice.data, false, options);
422
420
  return null;
423
421
  }
424
422
 
@@ -428,13 +426,13 @@ fn executeMove(
428
426
  if (side.last_selected_move == .SKIP_TURN) {
429
427
  assert(!showdown);
430
428
  if (battle.foe(player).active.volatiles.Binding) {
431
- try log.cant(battle.active(player), .Bound);
429
+ try options.log.cant(battle.active(player), .Bound);
432
430
  }
433
431
  return null;
434
432
  }
435
433
 
436
434
  assert(choice.type == .Move);
437
- var mslot = @intCast(u4, choice.data);
435
+ var mslot: u4 = @intCast(choice.data);
438
436
  // Sadly, we can't even check `Move.get(side.last_selected_move).effect == .Binding` here
439
437
  // because Pokémon Showdown's Mirror Move implementation clobbers side.last_selected_move
440
438
  var auto = showdown and side.last_selected_move != .None;
@@ -443,10 +441,7 @@ fn executeMove(
443
441
  if (mslot == 0 and side.last_selected_move != .None and side.last_selected_move != .Struggle) {
444
442
  // choice.data == 0 only happens with Struggle on Pokémon Showdown
445
443
  assert(!showdown);
446
- mslot = @intCast(u4, if (player == .P1)
447
- battle.last_selected_indexes.p1
448
- else
449
- battle.last_selected_indexes.p2);
444
+ mslot = @intCast(battle.lastMove(player).index);
450
445
  const stored = side.stored();
451
446
  // GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
452
447
  auto = side.last_selected_move == .HyperBeam or side.active.volatiles.Bide or
@@ -467,15 +462,12 @@ fn executeMove(
467
462
  } else if (showdown and side.active.volatiles.Charging) {
468
463
  // Incorrect mslot with Pokémon Showdown choice semantics so we need to recover from index
469
464
  assert(mslot == 1);
470
- mslot = @intCast(u4, if (player == .P1)
471
- battle.last_selected_indexes.p1
472
- else
473
- battle.last_selected_indexes.p2);
465
+ mslot = @intCast(battle.lastMove(player).index);
474
466
  }
475
467
 
476
468
  var skip_can = false;
477
469
  var skip_pp = false;
478
- switch (try beforeMove(battle, player, skip, from, residual, log)) {
470
+ switch (try beforeMove(battle, player, skip, from, residual, options)) {
479
471
  .done => return null,
480
472
  .skip_can => skip_can = true,
481
473
  .skip_pp => skip_pp = true,
@@ -483,11 +475,11 @@ fn executeMove(
483
475
  .err => return @as(?Result, Result.Error),
484
476
  }
485
477
 
486
- if (!skip_can and !try canMove(battle, player, mslot, auto, skip_pp, from, residual, log)) {
478
+ if (!skip_can and !try canMove(battle, player, mslot, auto, skip_pp, from, residual, options)) {
487
479
  return null;
488
480
  }
489
481
 
490
- return doMove(battle, player, mslot, rewrap, auto, residual, log);
482
+ return doMove(battle, player, mslot, rewrap, auto, residual, options);
491
483
  }
492
484
 
493
485
  const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
@@ -498,8 +490,9 @@ fn beforeMove(
498
490
  skip: bool,
499
491
  from: ?Move,
500
492
  residual: *bool,
501
- log: anytype,
493
+ options: anytype,
502
494
  ) !BeforeMove {
495
+ var log = options.log;
503
496
  var side = battle.side(player);
504
497
  const foe = battle.foe(player);
505
498
  var active = &side.active;
@@ -508,11 +501,21 @@ fn beforeMove(
508
501
  var volatiles = &active.volatiles;
509
502
 
510
503
  if (Status.is(stored.status, .SLP)) {
511
- const status = stored.status;
504
+ const before = stored.status;
512
505
  // Even if the EXT bit is set this will still correctly modify the sleep duration
513
- stored.status -= 1;
514
- if (Status.duration(stored.status) == 0) {
515
- try log.curestatus(ident, status, .Message);
506
+ if (options.calc.modify(player, .sleep)) |extend| {
507
+ if (!extend) stored.status = 0;
508
+ } else {
509
+ stored.status -= 1;
510
+ }
511
+
512
+ const duration = Status.duration(stored.status);
513
+ if (!Status.is(stored.status, .EXT)) {
514
+ try options.chance.sleep(player, duration);
515
+ }
516
+
517
+ if (duration == 0) {
518
+ try log.curestatus(ident, before, .Message);
516
519
  stored.status = 0; // clears EXT if present
517
520
  } else {
518
521
  try log.cant(ident, .Sleep);
@@ -547,14 +550,20 @@ fn beforeMove(
547
550
  }
548
551
 
549
552
  if (volatiles.disabled_duration > 0) {
550
- volatiles.disabled_duration -= 1;
553
+ if (options.calc.modify(player, .disable)) |extend| {
554
+ if (!extend) volatiles.disabled_duration = 0;
555
+ } else {
556
+ volatiles.disabled_duration -= 1;
557
+ }
558
+ try options.chance.disable(player, volatiles.disabled_duration);
559
+
551
560
  if (volatiles.disabled_duration == 0) {
552
561
  volatiles.disabled_move = 0;
553
562
  try log.end(ident, .Disable);
554
563
  }
555
564
  }
556
565
  // Pokémon Showdown's disable condition has a single onBeforeMove handler
557
- if (showdown and try disabled(side, ident, log)) return .done;
566
+ if (showdown and try disabled(side, ident, options)) return .done;
558
567
 
559
568
  // This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
560
569
  // before the side had a selected a move - we simply need to assume this leads to a desync
@@ -566,14 +575,20 @@ fn beforeMove(
566
575
  if (volatiles.Confusion) {
567
576
  assert(volatiles.confusion > 0);
568
577
 
569
- volatiles.confusion -= 1;
578
+ if (options.calc.modify(player, .confusion)) |extend| {
579
+ if (!extend) volatiles.confusion = 0;
580
+ } else {
581
+ volatiles.confusion -= 1;
582
+ }
583
+ try options.chance.confusion(player, volatiles.confusion);
584
+
570
585
  if (volatiles.confusion == 0) {
571
586
  volatiles.Confusion = false;
572
587
  try log.end(ident, .Confusion);
573
588
  } else {
574
589
  try log.activate(ident, .Confusion);
575
590
 
576
- if (Rolls.confused(battle)) {
591
+ if (try Rolls.confused(battle, player, options)) {
577
592
  assert(!volatiles.MultiHit);
578
593
  if (!volatiles.Rage) volatiles.state = 0;
579
594
  volatiles.Bide = false;
@@ -583,6 +598,7 @@ fn beforeMove(
583
598
  volatiles.Charging = false;
584
599
  volatiles.Binding = false;
585
600
  volatiles.Invulnerable = false;
601
+ options.chance.clearDurations(player, null);
586
602
  {
587
603
  // This feels (and is) disgusting but the cartridge literally just overwrites
588
604
  // the opponent's defense with the user's defense and resets it after. As a
@@ -602,7 +618,7 @@ fn beforeMove(
602
618
 
603
619
  const uncapped = battle.last_damage;
604
620
  // Skipping adjustDamage / randomizeDamage / checkHit
605
- _ = try applyDamage(battle, player, target, .Confusion, log);
621
+ _ = try applyDamage(battle, player, target, .Confusion, options);
606
622
  // Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
607
623
  if (showdown) battle.last_damage = uncapped;
608
624
 
@@ -611,14 +627,15 @@ fn beforeMove(
611
627
  }
612
628
  }
613
629
 
614
- if (!showdown and try disabled(side, ident, log)) return .done;
630
+ if (!showdown and try disabled(side, ident, options)) return .done;
615
631
 
616
- if (Status.is(stored.status, .PAR) and Rolls.paralyzed(battle)) {
632
+ if (Status.is(stored.status, .PAR) and try Rolls.paralyzed(battle, player, options)) {
617
633
  if (!volatiles.Rage) volatiles.state = 0;
618
634
  volatiles.Bide = false;
619
635
  volatiles.Thrashing = false;
620
636
  volatiles.Charging = false;
621
637
  volatiles.Binding = false;
638
+ options.chance.clearDurations(player, null);
622
639
  // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
623
640
  try log.cant(ident, .Paralysis);
624
641
  return .done;
@@ -627,11 +644,24 @@ fn beforeMove(
627
644
  if (volatiles.Bide) {
628
645
  assert(!volatiles.Thrashing and !volatiles.Rage);
629
646
 
630
- volatiles.state +%= battle.last_damage;
647
+ if (showdown) {
648
+ // Pokémon Showdown doesn't implement Bide potentially overflowing in the event of
649
+ // OHKO-move damage, but we can fake this incorrect behavior by simply saturating the
650
+ // addition because 65535 is sufficient to faint any Pokémon anyway
651
+ volatiles.state +|= battle.last_damage;
652
+ } else {
653
+ volatiles.state +%= battle.last_damage;
654
+ }
631
655
 
632
656
  assert(volatiles.attacks > 0);
633
657
 
634
- volatiles.attacks -= 1;
658
+ if (options.calc.modify(player, .attacking)) |extend| {
659
+ if (!extend) volatiles.attacks = 0;
660
+ } else {
661
+ volatiles.attacks -= 1;
662
+ }
663
+ try options.chance.attacking(player, volatiles.attacks);
664
+
635
665
  if (volatiles.attacks != 0) {
636
666
  try log.activate(ident, .Bide);
637
667
  return .done;
@@ -649,15 +679,15 @@ fn beforeMove(
649
679
  }
650
680
 
651
681
  const sub = showdown and foe.active.volatiles.Substitute;
652
- _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
653
- if (foe.stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
682
+ _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
683
+ if (foe.stored().hp > 0 and !sub) try buildRage(battle, player.foe(), options);
654
684
 
655
685
  // For reasons passing understanding, Pokémon Showdown still inflicts residual damage to
656
686
  // Bide's user even if the above damage has caused the foe to faint. It's simpler to always
657
687
  // run residual here regardless of whether the foe fainted and opt-out of the default flow
658
688
  if (showdown) {
659
689
  residual.* = false;
660
- try handleResidual(battle, player, log);
690
+ try handleResidual(battle, player, options);
661
691
  }
662
692
 
663
693
  return .done;
@@ -666,8 +696,15 @@ fn beforeMove(
666
696
  if (volatiles.Thrashing) {
667
697
  assert(volatiles.attacks > 0);
668
698
  assert(from != null);
669
- volatiles.attacks -= 1;
670
- if (!showdown and handleThrashing(battle, active)) {
699
+
700
+ if (options.calc.modify(player, .attacking)) |extend| {
701
+ if (!extend) volatiles.attacks = 0;
702
+ } else {
703
+ volatiles.attacks -= 1;
704
+ }
705
+ try options.chance.attacking(player, volatiles.attacks);
706
+
707
+ if (!showdown and handleThrashing(battle, active, player, options)) {
671
708
  try log.start(battle.active(player), .ConfusionSilent);
672
709
  }
673
710
  try log.move(ident, side.last_selected_move, battle.active(player.foe()), from);
@@ -675,7 +712,7 @@ fn beforeMove(
675
712
  // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
676
713
  // used in useMove and doesn't have the notion of skipping canMove semantics
677
714
  side.last_used_move = side.last_selected_move;
678
- if (handleThrashing(battle, active)) {
715
+ if (handleThrashing(battle, active, player, options)) {
679
716
  try log.start(battle.active(player), .ConfusionSilent);
680
717
  }
681
718
  }
@@ -684,7 +721,14 @@ fn beforeMove(
684
721
 
685
722
  if (volatiles.Binding) {
686
723
  assert(volatiles.attacks > 0);
687
- volatiles.attacks -= 1;
724
+
725
+ if (options.calc.modify(player, .binding)) |extend| {
726
+ if (!extend) volatiles.attacks = 0;
727
+ } else {
728
+ volatiles.attacks -= 1;
729
+ }
730
+ try options.chance.binding(player, volatiles.attacks);
731
+
688
732
  try log.move(
689
733
  battle.active(player),
690
734
  side.last_selected_move,
@@ -693,9 +737,9 @@ fn beforeMove(
693
737
  );
694
738
  if (showdown or battle.last_damage != 0) {
695
739
  const sub = showdown and foe.active.volatiles.Substitute;
696
- _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
740
+ _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
697
741
  if (battle.foe(player).stored().hp > 0 and !sub) {
698
- try buildRage(battle, player.foe(), log);
742
+ try buildRage(battle, player.foe(), options);
699
743
  }
700
744
  }
701
745
  return .done;
@@ -712,7 +756,7 @@ fn canMove(
712
756
  skip_pp: bool,
713
757
  from: ?Move,
714
758
  residual: *bool,
715
- log: anytype,
759
+ options: anytype,
716
760
  ) !bool {
717
761
  var side = battle.side(player);
718
762
  const player_ident = battle.active(player);
@@ -727,8 +771,9 @@ fn canMove(
727
771
  side.active.volatiles.Charging = false;
728
772
  side.active.volatiles.Invulnerable = false;
729
773
  } else if (move.effect == .Charge) {
730
- try log.move(player_ident, side.last_selected_move, .{}, from);
731
- try Effects.charge(battle, player, log);
774
+ try options.log.move(player_ident, side.last_selected_move, .{}, from);
775
+ setCounterable(battle, player, side, move);
776
+ try Effects.charge(battle, player, options);
732
777
  return false;
733
778
  }
734
779
 
@@ -743,23 +788,35 @@ fn canMove(
743
788
  // relevant for control flow, but we don't have anything useful to put in the log. This is
744
789
  // regrettable, but this information is more "nice to have" than required
745
790
  const f = if (from != null and from.? == .None) null else from;
746
- try log.move(player_ident, side.last_selected_move, battle.active(target), f);
791
+ try options.log.move(player_ident, side.last_selected_move, battle.active(target), f);
792
+ setCounterable(battle, player, side, move);
747
793
 
748
794
  if (move.effect.onBegin()) {
749
- try onBegin(battle, player, move, mslot, residual, log);
795
+ try onBegin(battle, player, move, mslot, residual, options);
750
796
  return false;
751
797
  }
752
798
 
753
799
  if (move.effect == .Thrashing) {
754
- Effects.thrashing(battle, player);
800
+ Effects.thrashing(battle, player, options);
755
801
  } else if (!showdown and move.effect == .Binding) {
756
802
  // Pokémon Showdown handles this after hit/miss checks and damage calculation
757
- Effects.binding(battle, player, false);
803
+ try Effects.binding(battle, player, false, options);
758
804
  }
759
805
 
760
806
  return true;
761
807
  }
762
808
 
809
+ fn setCounterable(battle: anytype, player: Player, side: *Side, move: Move.Data) void {
810
+ // The Counter desync is caused by the cartridge not calling GetCurrentMove until now, meaning
811
+ // in cases where an early return happens the data for a players last selected move does not get
812
+ // reloaded and HandleCounterMove actually bases its success/failure off of stale information.
813
+ // This boolean state we track here doesn't exist on the cartridge because it instead manifests
814
+ // as the desync results from actually having two separate battle states that subtly disagree
815
+ const counterable = side.last_selected_move != .Counter and move.bp > 0 and
816
+ (move.type == .Normal or move.type == .Fighting);
817
+ battle.lastMove(player).counterable = @intFromBool(counterable);
818
+ }
819
+
763
820
  fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
764
821
  if (side.last_selected_move == .Struggle) return;
765
822
 
@@ -770,11 +827,11 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
770
827
  if (volatiles.Bide) return;
771
828
 
772
829
  assert(active.move(mslot).pp > 0 or auto);
773
- active.move(mslot).pp = @intCast(u6, active.move(mslot).pp) -% 1;
830
+ active.move(mslot).pp = @as(u6, @intCast(active.move(mslot).pp)) -% 1;
774
831
  if (volatiles.Transform) return;
775
832
 
776
833
  assert(side.stored().move(mslot).pp > 0 or auto);
777
- side.stored().move(mslot).pp = @intCast(u6, side.stored().move(mslot).pp) -% 1;
834
+ side.stored().move(mslot).pp = @as(u6, @intCast(side.stored().move(mslot).pp)) -% 1;
778
835
  assert(active.move(mslot).pp == side.stored().move(mslot).pp);
779
836
  }
780
837
 
@@ -782,12 +839,12 @@ fn incrementPP(side: *Side, mslot: u4) void {
782
839
  var active = &side.active;
783
840
  const volatiles = &active.volatiles;
784
841
 
785
- active.move(mslot).pp = @intCast(u6, active.move(mslot).pp) +% 1;
842
+ active.move(mslot).pp = @as(u6, @intCast(active.move(mslot).pp)) +% 1;
786
843
  // GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
787
844
  if (showdown and volatiles.Transform) return;
788
845
 
789
846
  assert(mslot > 0 and mslot <= 4);
790
- side.stored().moves[mslot - 1].pp = @intCast(u6, side.stored().moves[mslot - 1].pp) +% 1;
847
+ side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% 1;
791
848
  }
792
849
 
793
850
  // Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
@@ -798,8 +855,9 @@ fn doMove(
798
855
  rewrap: bool,
799
856
  auto: bool,
800
857
  residual: *bool,
801
- log: anytype,
858
+ options: anytype,
802
859
  ) !?Result {
860
+ var log = options.log;
803
861
  var side = battle.side(player);
804
862
  const foe = battle.foe(player);
805
863
 
@@ -814,23 +872,27 @@ fn doMove(
814
872
  var hits: u4 = 1;
815
873
  var effectiveness = Effectiveness.neutral;
816
874
 
875
+ // Due to control flow shenanigans we need to clear last_damage for Pokémon Showdown
876
+ if (showdown and !counter) battle.last_damage = 0;
877
+
817
878
  // The cartridge handles set damage moves in applyDamage but we short circuit to simplify things
818
879
  if (move.effect == .SuperFang or move.effect == .SpecialDamage) {
819
- return specialDamage(battle, player, move, log);
880
+ return specialDamage(battle, player, move, options);
820
881
  }
821
882
 
822
883
  // Pokémon Showdown runs invulnerability / immunity checks before checking accuracy - simply
823
884
  // calling moveHit early covers most of that but we also need to check type immunity first
824
885
  var miss = showdown and miss: {
825
886
  immune = move.target != .Self and !status and !counter and
826
- (@enumToInt(move.type.effectiveness(foe.active.types.type1)) == 0 or
827
- @enumToInt(move.type.effectiveness(foe.active.types.type2)) == 0);
887
+ (@intFromEnum(move.type.effectiveness(foe.active.types.type1)) == 0 or
888
+ @intFromEnum(move.type.effectiveness(foe.active.types.type2)) == 0);
828
889
  if (immune and move.effect != .Binding) break :miss true;
829
890
  if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) {
830
891
  battle.last_damage = 0;
831
892
  break :miss true;
832
893
  }
833
- break :miss move.target != .Self and !moveHit(battle, player, move, &immune, &mist);
894
+ break :miss (move.target != .Self and
895
+ !moveHit(battle, player, move, &immune, &mist, options));
834
896
  };
835
897
  assert(!immune or miss or (showdown and move.effect == .Binding));
836
898
 
@@ -838,16 +900,16 @@ fn doMove(
838
900
  const skip = status or immune;
839
901
  if ((!showdown or (!skip or counter)) and !miss) blk: {
840
902
  if (showdown and move.effect.isMulti()) {
841
- Effects.multiHit(battle, player, move);
903
+ try Effects.multiHit(battle, player, move, options);
842
904
  hits = side.active.volatiles.attacks;
843
905
  late = false;
844
906
  }
845
907
 
846
908
  // Cartridge rolls for crit even for moves that can't crit (Counter/Metronome/status/OHKO)
847
909
  const check = !showdown or (!counter and move.effect != .OHKO);
848
- if (check) crit = checkCriticalHit(battle, player, move);
910
+ if (check) crit = try checkCriticalHit(battle, player, move, options);
849
911
 
850
- if (counter) return counterDamage(battle, player, move, log);
912
+ if (counter) return counterDamage(battle, player, move, options);
851
913
 
852
914
  battle.last_damage = 0;
853
915
 
@@ -869,17 +931,15 @@ fn doMove(
869
931
  effectiveness = adjustDamage(battle, player);
870
932
  immune = effectiveness == 0;
871
933
  }
872
- randomizeDamage(battle);
934
+ try randomizeDamage(battle, player, options);
873
935
  }
874
936
  }
875
937
 
876
- // Due to control flow shenanigans we need to clear last_damage for Pokémon Showdown
877
- if (showdown and skip) battle.last_damage = 0;
878
-
938
+ var zero = battle.last_damage == 0;
879
939
  miss = if (showdown or skip)
880
940
  miss
881
941
  else
882
- (!moveHit(battle, player, move, &immune, &mist) or battle.last_damage == 0);
942
+ (!moveHit(battle, player, move, &immune, &mist, options) or zero);
883
943
 
884
944
  assert(showdown or miss or battle.last_damage > 0 or skip);
885
945
  assert((!showdown and miss) or !(ohko and immune));
@@ -887,11 +947,11 @@ fn doMove(
887
947
 
888
948
  if (!showdown or !miss) {
889
949
  if (move.effect == .MirrorMove) {
890
- return mirrorMove(battle, player, mslot, rewrap, auto, residual, log);
950
+ return mirrorMove(battle, player, mslot, rewrap, auto, residual, options);
891
951
  } else if (move.effect == .Metronome) {
892
- return metronome(battle, player, mslot, rewrap, auto, residual, log);
952
+ return metronome(battle, player, mslot, rewrap, auto, residual, options);
893
953
  } else if (move.effect.onEnd()) {
894
- try onEnd(battle, player, move, log);
954
+ try onEnd(battle, player, move, options);
895
955
  return null;
896
956
  }
897
957
  }
@@ -910,29 +970,34 @@ fn doMove(
910
970
  if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
911
971
  try log.fail(foe_ident, .None);
912
972
  } else {
973
+ if (showdown or !zero) try options.chance.commit(player, .miss);
913
974
  try log.lastmiss();
914
975
  try log.miss(battle.active(player));
915
976
  }
916
977
  if (move.effect == .JumpKick) {
917
978
  // Recoil is supposed to be damage/8 but damage will always be 0 here
918
- assert(battle.last_damage == 0);
979
+ assert(showdown or battle.last_damage == 0);
919
980
  battle.last_damage = 1;
920
- _ = try applyDamage(battle, player, player.foe(), .None, log);
981
+ _ = try applyDamage(battle, player, player.foe(), .None, options);
921
982
  if (showdown and side.stored().hp == 0) residual.* = false;
922
983
  } else if (move.effect == .Explode) {
923
984
  try Effects.explode(battle, player);
924
- try buildRage(battle, player.foe(), log);
985
+ try buildRage(battle, player.foe(), options);
925
986
  } else if (showdown and move.effect == .Disable) {
926
- try buildRage(battle, player.foe(), log);
987
+ try buildRage(battle, player.foe(), options);
988
+ } else if (!showdown and immune and move.effect == .Binding) {
989
+ try options.chance.commit(player, .binding);
927
990
  }
928
991
  return null;
992
+ } else {
993
+ try options.chance.commit(player, .hit);
929
994
  }
930
995
 
931
996
  // On the cartridge MultiHit doesn't get set up until after damage has been applied for the
932
997
  // first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
933
998
  // it up above before damage calculation).
934
999
  if (!showdown and move.effect.isMulti()) {
935
- Effects.multiHit(battle, player, move);
1000
+ try Effects.multiHit(battle, player, move, options);
936
1001
  hits = side.active.volatiles.attacks;
937
1002
  }
938
1003
 
@@ -950,11 +1015,13 @@ fn doMove(
950
1015
  try log.resisted(battle.active(player.foe()));
951
1016
  }
952
1017
  }
953
- if (!skip) nullified = try applyDamage(battle, player.foe(), player.foe(), .None, log);
1018
+ if (!skip) nullified = try applyDamage(battle, player.foe(), player.foe(), .None, options);
954
1019
  if (hit == 0 and ohko) try log.ohko();
955
1020
  hit += 1;
956
1021
  if (foe.stored().hp == 0) break;
957
- if (!late and (!sub or move.effect == .Explode)) try buildRage(battle, player.foe(), log);
1022
+ if (!late and (!sub or move.effect == .Explode)) {
1023
+ try buildRage(battle, player.foe(), options);
1024
+ }
958
1025
  // If the substitute breaks during a multi-hit attack, the attack ends
959
1026
  if (nullified) break;
960
1027
  }
@@ -964,13 +1031,13 @@ fn doMove(
964
1031
  assert(nullified or foe.stored().hp == 0 or side.active.volatiles.attacks - hit == 0);
965
1032
  side.active.volatiles.attacks = 0;
966
1033
  if (showdown and move.effect == .Twineedle and !nullified and foe.stored().hp > 0) {
967
- try Effects.poison(battle, player, Move.get(.PoisonSting), log);
1034
+ try Effects.poison(battle, player, Move.get(.PoisonSting), options);
968
1035
  }
969
1036
  try log.hitcount(battle.active(player.foe()), hit);
970
1037
  } else if (showdown) {
971
1038
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
972
1039
  if (move.effect == .Binding) {
973
- Effects.binding(battle, player, rewrap);
1040
+ try Effects.binding(battle, player, rewrap, options);
974
1041
  if (immune) {
975
1042
  battle.last_damage = 0;
976
1043
  assert(foe.stored().hp > 0);
@@ -979,7 +1046,7 @@ fn doMove(
979
1046
  try log.activate(battle.active(player.foe()), .Substitute);
980
1047
  } else {
981
1048
  try log.damage(battle.active(player.foe()), foe.stored(), .None);
982
- try buildRage(battle, player.foe(), log);
1049
+ try buildRage(battle, player.foe(), options);
983
1050
  }
984
1051
  return null;
985
1052
  }
@@ -993,12 +1060,12 @@ fn doMove(
993
1060
  // On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
994
1061
  // but this is only done to setup the MultiHit looping in the first place. Moving the MultiHit
995
1062
  // setup before the loop means we can avoid having to waste time doing no-op handler searches.
996
- if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, log);
1063
+ if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
997
1064
 
998
1065
  if (foe.stored().hp == 0) return null;
999
1066
 
1000
1067
  // Pokémon Showdown builds Rage at the wrong time for non-MultiHit move
1001
- if (late and !sub and move.effect != .Disable) try buildRage(battle, player.foe(), log);
1068
+ if (late and !sub and move.effect != .Disable) try buildRage(battle, player.foe(), options);
1002
1069
 
1003
1070
  if (!move.effect.isSpecial()) {
1004
1071
  // On the cartridge Rage is not considered to be "special" and thus gets executed for a
@@ -1009,29 +1076,29 @@ fn doMove(
1009
1076
  // Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
1010
1077
  // puts the |-status| message before |-hitcount| instead of after.
1011
1078
  if (move.effect == .Twineedle) {
1012
- if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), log);
1079
+ if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1013
1080
  } else if (move.effect == .Disable) {
1014
- const result = try Effects.disable(battle, player, move, log);
1015
- if (showdown) try buildRage(battle, player.foe(), log);
1081
+ const result = try Effects.disable(battle, player, move, options);
1082
+ if (showdown) try buildRage(battle, player.foe(), options);
1016
1083
  return result;
1017
1084
  } else {
1018
- try moveEffect(battle, player, move, log);
1085
+ try moveEffect(battle, player, move, options);
1019
1086
  }
1020
1087
  }
1021
1088
 
1022
1089
  return null;
1023
1090
  }
1024
1091
 
1025
- fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
1092
+ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !bool {
1026
1093
  const side = battle.side(player);
1027
1094
 
1028
1095
  // Base speed is used for the critical hit calculation, even when Transform-ed
1029
- var chance = @as(u16, Species.chance(side.stored().species));
1030
- // GLITCH: Focus Energy reduces critical hit chance instead of increasing it
1031
- chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
1032
- chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
1096
+ var rate = @as(u16, Species.chance(side.stored().species));
1097
+ // GLITCH: Focus Energy reduces critical hit rate instead of increasing it
1098
+ rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
1099
+ rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
1033
1100
 
1034
- return Rolls.criticalHit(battle, chance);
1101
+ return Rolls.criticalHit(battle, player, @intCast(rate), options);
1035
1102
  }
1036
1103
 
1037
1104
  fn calcDamage(
@@ -1097,7 +1164,7 @@ fn calcDamage(
1097
1164
  d = @min(997, d);
1098
1165
  d += 2;
1099
1166
 
1100
- battle.last_damage = @intCast(u16, d);
1167
+ battle.last_damage = @intCast(d);
1101
1168
 
1102
1169
  return true;
1103
1170
  }
@@ -1111,9 +1178,9 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
1111
1178
  var d = battle.last_damage;
1112
1179
  if (side.active.types.includes(move.type)) d +%= d / 2;
1113
1180
 
1114
- const neutral = @enumToInt(Effectiveness.Neutral);
1115
- const eff1: u16 = @enumToInt(move.type.effectiveness(types.type1));
1116
- const eff2: u16 = @enumToInt(move.type.effectiveness(types.type2));
1181
+ const neutral = @intFromEnum(Effectiveness.Neutral);
1182
+ const eff1: u16 = @intFromEnum(move.type.effectiveness(types.type1));
1183
+ const eff2: u16 = @intFromEnum(move.type.effectiveness(types.type2));
1117
1184
 
1118
1185
  // Type effectiveness matchup precedence only matters with (NVE, SE)
1119
1186
  if (!showdown and (eff1 + eff2) == Effectiveness.mismatch and
@@ -1133,17 +1200,20 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
1133
1200
  return if (types.type1 == types.type2) eff1 * neutral else eff1 * eff2;
1134
1201
  }
1135
1202
 
1136
- fn randomizeDamage(battle: anytype) void {
1203
+ fn randomizeDamage(battle: anytype, player: Player, options: anytype) !void {
1137
1204
  if (battle.last_damage <= 1) return;
1138
- const random = Rolls.damage(battle);
1139
- battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
1205
+ const random = try Rolls.damage(battle, player, options);
1206
+ options.calc.base(player.foe(), battle.last_damage);
1207
+ battle.last_damage = @intCast(@as(u32, battle.last_damage) *% random / 255);
1208
+ options.calc.final(player.foe(), battle.last_damage);
1140
1209
  }
1141
1210
 
1142
- fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
1211
+ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
1143
1212
  const side = battle.side(player);
1144
1213
  const foe = battle.foe(player);
1145
1214
 
1146
- if (!try checkHit(battle, player, move, log)) return null;
1215
+ if (!try checkHit(battle, player, move, options)) return null;
1216
+ try options.chance.commit(player, .hit);
1147
1217
 
1148
1218
  battle.last_damage = switch (side.last_selected_move) {
1149
1219
  .SuperFang => @max(foe.stored().hp / 2, 1),
@@ -1152,10 +1222,10 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
1152
1222
  .DragonRage => 40,
1153
1223
  // GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
1154
1224
  .Psywave => power: {
1155
- const max = @intCast(u8, @as(u16, side.stored().level) * 3 / 2);
1225
+ const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
1156
1226
  // GLITCH: Psywave infinite glitch loop
1157
1227
  if (!showdown and max <= 1) return Result.Error;
1158
- break :power Rolls.psywave(battle, max);
1228
+ break :power try Rolls.psywave(battle, player, max, options);
1159
1229
  },
1160
1230
  else => unreachable,
1161
1231
  };
@@ -1163,59 +1233,54 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
1163
1233
  if (battle.last_damage == 0) return if (showdown) null else Result.Error;
1164
1234
 
1165
1235
  const sub = showdown and foe.active.volatiles.Substitute;
1166
- _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
1167
- if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
1236
+ _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
1237
+ if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), options);
1168
1238
 
1169
1239
  return null;
1170
1240
  }
1171
1241
 
1172
- fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
1242
+ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
1173
1243
  const foe = battle.foe(player);
1174
1244
 
1175
1245
  if (battle.last_damage == 0) {
1176
- try log.fail(battle.active(player), .None);
1246
+ try options.log.fail(battle.active(player), .None);
1177
1247
  return null;
1178
1248
  }
1179
1249
 
1180
- // Pretend Counter was used as a stand-in when no move has been used to fail below with 0 BP
1181
- const foe_last_used_move =
1182
- Move.get(if (foe.last_used_move == .None) .Counter else foe.last_used_move);
1250
+ // Pretend Splash was used as a stand-in when no move has been used to fail below with 0 BP
1183
1251
  const foe_last_selected_move =
1184
1252
  Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
1185
- .Counter
1253
+ .Splash
1186
1254
  else
1187
1255
  foe.last_selected_move);
1188
1256
 
1189
- const used = foe_last_used_move.bp > 0 and
1190
- foe.last_used_move != .Counter and
1191
- (foe_last_used_move.type == .Normal or
1192
- foe_last_used_move.type == .Fighting);
1193
-
1257
+ const used = battle.lastMove(player.foe()).counterable != 0;
1194
1258
  const selected = foe_last_selected_move.bp > 0 and
1195
1259
  foe.last_selected_move != .Counter and
1196
1260
  (foe_last_selected_move.type == .Normal or
1197
1261
  foe_last_selected_move.type == .Fighting);
1198
1262
 
1199
1263
  if (!used and !selected) {
1200
- try log.fail(battle.active(player), .None);
1264
+ try options.log.fail(battle.active(player), .None);
1201
1265
  return null;
1202
1266
  }
1203
1267
 
1204
1268
  if (!used or !selected) {
1205
1269
  // GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
1206
1270
  if (!showdown) return Result.Error;
1207
- try log.fail(battle.active(player), .None);
1271
+ try options.log.fail(battle.active(player), .None);
1208
1272
  return null;
1209
1273
  }
1210
1274
 
1211
- battle.last_damage = if (battle.last_damage > 0x7FFF) 0xFFFF else battle.last_damage * 2;
1275
+ battle.last_damage *|= 2;
1212
1276
 
1213
1277
  // Pokémon Showdown calls checkHit before Counter
1214
- if (!showdown and !try checkHit(battle, player, move, log)) return null;
1278
+ if (!showdown and !try checkHit(battle, player, move, options)) return null;
1279
+ try options.chance.commit(player, .hit);
1215
1280
 
1216
1281
  const sub = showdown and foe.active.volatiles.Substitute;
1217
- _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
1218
- if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
1282
+ _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
1283
+ if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), options);
1219
1284
  return null;
1220
1285
  }
1221
1286
 
@@ -1224,7 +1289,7 @@ fn applyDamage(
1224
1289
  target_player: Player,
1225
1290
  sub_player: Player,
1226
1291
  reason: Damage,
1227
- log: anytype,
1292
+ options: anytype,
1228
1293
  ) !bool {
1229
1294
  assert(showdown or battle.last_damage != 0);
1230
1295
 
@@ -1239,19 +1304,20 @@ fn applyDamage(
1239
1304
  subbed.active.volatiles.substitute = 0;
1240
1305
  subbed.active.volatiles.Substitute = false;
1241
1306
  // battle.last_damage is not updated with the amount of HP the Substitute had
1242
- try log.end(battle.active(sub_player), .Substitute);
1307
+ try options.log.end(battle.active(sub_player), .Substitute);
1308
+ options.calc.capped(target_player);
1243
1309
  return true;
1244
1310
  } else {
1245
1311
  // Safe to truncate since less than subbed.volatiles.substitute which is a u8
1246
- subbed.active.volatiles.substitute -= @intCast(u8, battle.last_damage);
1247
- try log.activate(battle.active(sub_player), .Substitute);
1312
+ subbed.active.volatiles.substitute -= @intCast(battle.last_damage);
1313
+ try options.log.activate(battle.active(sub_player), .Substitute);
1248
1314
  return false;
1249
1315
  }
1250
1316
  }
1251
1317
 
1252
1318
  if (battle.last_damage > target.stored().hp) battle.last_damage = target.stored().hp;
1253
1319
  target.stored().hp -= battle.last_damage;
1254
- try log.damage(battle.active(target_player), target.stored(), reason);
1320
+ try options.log.damage(battle.active(target_player), target.stored(), reason);
1255
1321
  return false;
1256
1322
  }
1257
1323
 
@@ -1262,22 +1328,24 @@ fn mirrorMove(
1262
1328
  rewrap: bool,
1263
1329
  auto: bool,
1264
1330
  residual: *bool,
1265
- log: anytype,
1266
- ) !?Result {
1331
+ options: anytype,
1332
+ ) anyerror!?Result {
1267
1333
  var side = battle.side(player);
1268
1334
  const foe = battle.foe(player);
1269
1335
 
1270
1336
  side.last_selected_move = foe.last_used_move;
1271
1337
 
1272
1338
  if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
1273
- try log.fail(battle.active(player), .None);
1339
+ try options.log.fail(battle.active(player), .None);
1274
1340
  return null;
1275
1341
  } else if (!showdown or foe.last_used_move != .Struggle) {
1276
1342
  incrementPP(side, mslot);
1277
1343
  }
1278
1344
 
1279
- if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
1280
- return doMove(battle, player, mslot, rewrap, auto, residual, log);
1345
+ if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
1346
+ return null;
1347
+ }
1348
+ return doMove(battle, player, mslot, rewrap, auto, residual, options);
1281
1349
  }
1282
1350
 
1283
1351
  fn metronome(
@@ -1287,35 +1355,48 @@ fn metronome(
1287
1355
  rewrap: bool,
1288
1356
  auto: bool,
1289
1357
  residual: *bool,
1290
- log: anytype,
1291
- ) !?Result {
1358
+ options: anytype,
1359
+ ) anyerror!?Result {
1292
1360
  var side = battle.side(player);
1293
1361
 
1294
- side.last_selected_move = Rolls.metronome(battle);
1362
+ side.last_selected_move = try Rolls.metronome(battle, player, options);
1295
1363
  incrementPP(side, mslot);
1296
1364
 
1297
- if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
1298
- return doMove(battle, player, mslot, rewrap, auto, residual, log);
1365
+ if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
1366
+ return null;
1367
+ }
1368
+ return doMove(battle, player, mslot, rewrap, auto, residual, options);
1299
1369
  }
1300
1370
 
1301
- fn checkHit(battle: anytype, player: Player, move: Move.Data, log: anytype) !bool {
1371
+ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !bool {
1302
1372
  var immune = false;
1303
1373
  var mist = false;
1304
- if (moveHit(battle, player, move, &immune, &mist)) return true;
1374
+
1375
+ if (moveHit(battle, player, move, &immune, &mist, options)) return true;
1376
+
1305
1377
  assert(!immune);
1306
1378
  if (mist) {
1307
1379
  assert(!showdown);
1308
1380
  const foe_ident = battle.active(player.foe());
1309
- try log.activate(foe_ident, .Mist);
1310
- try log.fail(foe_ident, .None);
1381
+ try options.log.activate(foe_ident, .Mist);
1382
+ try options.log.fail(foe_ident, .None);
1311
1383
  } else {
1312
- try log.lastmiss();
1313
- try log.miss(battle.active(player));
1384
+ try options.chance.commit(player, .miss);
1385
+ try options.log.lastmiss();
1386
+ try options.log.miss(battle.active(player));
1314
1387
  }
1388
+
1315
1389
  return false;
1316
1390
  }
1317
1391
 
1318
- fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist: *bool) bool {
1392
+ fn moveHit(
1393
+ battle: anytype,
1394
+ player: Player,
1395
+ move: Move.Data,
1396
+ immune: *bool,
1397
+ mist: *bool,
1398
+ options: anytype,
1399
+ ) bool {
1319
1400
  var side = battle.side(player);
1320
1401
  const foe = battle.foe(player);
1321
1402
 
@@ -1355,9 +1436,9 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
1355
1436
  assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
1356
1437
 
1357
1438
  var accuracy = if (overwritten) state else @as(u16, Gen12.percent(move.accuracy));
1358
- var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
1439
+ var boost = BOOSTS[@as(u4, @intCast(@as(i8, side.active.boosts.accuracy) + 6))];
1359
1440
  accuracy = accuracy * boost[0] / boost[1];
1360
- boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
1441
+ boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
1361
1442
  accuracy = accuracy * boost[0] / boost[1];
1362
1443
  accuracy = @min(255, @max(1, accuracy));
1363
1444
 
@@ -1366,7 +1447,7 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
1366
1447
  if (overwrite) side.active.volatiles.state = accuracy;
1367
1448
 
1368
1449
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1369
- break :miss !Rolls.hit(battle, accuracy);
1450
+ break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
1370
1451
  };
1371
1452
 
1372
1453
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1376,7 +1457,7 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
1376
1457
  }
1377
1458
 
1378
1459
  if (!miss) return true;
1379
- battle.last_damage = 0;
1460
+ if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
1380
1461
  side.active.volatiles.Binding = false;
1381
1462
  return false;
1382
1463
  }
@@ -1384,8 +1465,8 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
1384
1465
  fn checkFaint(
1385
1466
  battle: anytype,
1386
1467
  player: Player,
1387
- log: anytype,
1388
- ) @TypeOf(log).Error!?Result {
1468
+ options: anytype,
1469
+ ) @TypeOf(options.log).Error!?Result {
1389
1470
  const side = battle.side(player);
1390
1471
  if (side.stored().hp > 0) return null;
1391
1472
 
@@ -1397,20 +1478,20 @@ fn checkFaint(
1397
1478
  const tie = player_out and foe_out;
1398
1479
  const more = tie or player_out or foe_out;
1399
1480
 
1400
- if (try faint(battle, player, log, !(more or foe_fainted))) |r| return r;
1401
- if (foe_fainted) if (try faint(battle, player.foe(), log, !more)) |r| return r;
1481
+ if (try faint(battle, player, !(more or foe_fainted), options)) |r| return r;
1482
+ if (foe_fainted) if (try faint(battle, player.foe(), !more, options)) |r| return r;
1402
1483
 
1403
1484
  assert(!side.active.volatiles.MultiHit);
1404
1485
  assert(!foe.active.volatiles.MultiHit);
1405
1486
 
1406
1487
  if (tie) {
1407
- try log.tie();
1488
+ try options.log.tie();
1408
1489
  return Result.Tie;
1409
1490
  } else if (player_out) {
1410
- try log.win(player.foe());
1491
+ try options.log.win(player.foe());
1411
1492
  return if (player == .P1) Result.Lose else Result.Win;
1412
1493
  } else if (foe_out) {
1413
- try log.win(player);
1494
+ try options.log.win(player);
1414
1495
  return if (player == .P1) Result.Win else Result.Lose;
1415
1496
  }
1416
1497
 
@@ -1419,7 +1500,7 @@ fn checkFaint(
1419
1500
  return Result{ .p1 = foe_choice, .p2 = .Switch };
1420
1501
  }
1421
1502
 
1422
- fn faint(battle: anytype, player: Player, log: anytype, done: bool) !?Result {
1503
+ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
1423
1504
  var side = battle.side(player);
1424
1505
  var foe = battle.foe(player);
1425
1506
  assert(side.stored().hp == 0);
@@ -1452,11 +1533,12 @@ fn faint(battle: anytype, player: Player, log: anytype, done: bool) !?Result {
1452
1533
  side.stored().stats.spe;
1453
1534
  }
1454
1535
 
1455
- try log.faint(battle.active(player), done);
1536
+ try options.log.faint(battle.active(player), done);
1537
+ options.calc.capped(player);
1456
1538
  return null;
1457
1539
  }
1458
1540
 
1459
- fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1541
+ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1460
1542
  var side = battle.side(player);
1461
1543
  var stored = side.stored();
1462
1544
  const ident = battle.active(player);
@@ -1477,7 +1559,7 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1477
1559
  stored.hp -= amount;
1478
1560
 
1479
1561
  // Pokémon Showdown uses damageOf here but its not relevant in Generation I
1480
- try log.damage(ident, stored, if (brn) Damage.Burn else Damage.Poison);
1562
+ try options.log.damage(ident, stored, if (brn) Damage.Burn else Damage.Poison);
1481
1563
  }
1482
1564
 
1483
1565
  if (volatiles.LeechSeed) {
@@ -1502,42 +1584,42 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1502
1584
  stored.hp -= amount;
1503
1585
 
1504
1586
  // As above, Pokémon Showdown uses damageOf but its not relevant
1505
- if (amount > 0) try log.damage(ident, stored, .LeechSeed);
1587
+ if (amount > 0) try options.log.damage(ident, stored, .LeechSeed);
1506
1588
 
1507
1589
  const before = foe_stored.hp;
1508
1590
  // Uncapped damage is added back to the foe
1509
1591
  foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
1510
1592
  // Pokémon Showdown uses the less specific heal here instead of drain... because reasons?
1511
- if (foe_stored.hp > before) try log.heal(foe_ident, foe_stored, .Silent);
1593
+ if (foe_stored.hp > before) try options.log.heal(foe_ident, foe_stored, .Silent);
1512
1594
  }
1513
1595
  }
1514
1596
 
1515
- fn endTurn(battle: anytype, log: anytype) @TypeOf(log).Error!Result {
1597
+ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result {
1516
1598
  assert(!battle.side(.P1).active.volatiles.MultiHit);
1517
1599
  assert(!battle.side(.P2).active.volatiles.MultiHit);
1518
1600
 
1519
- if (showdown and options.ebc and checkEBC(battle)) {
1520
- try log.tie();
1601
+ battle.turn += 1;
1602
+
1603
+ if (showdown and pkmn.options.ebc and checkEBC(battle)) {
1604
+ try options.log.tie();
1521
1605
  return Result.Tie;
1522
1606
  }
1523
1607
 
1524
- battle.turn += 1;
1525
-
1526
1608
  if (showdown and battle.turn >= 1000) {
1527
- try log.tie();
1609
+ try options.log.tie();
1528
1610
  return Result.Tie;
1529
1611
  } else if (battle.turn >= 65535) {
1530
1612
  return Result.Error;
1531
1613
  }
1532
1614
 
1533
- try log.turn(battle.turn);
1615
+ try options.log.turn(battle.turn);
1534
1616
 
1535
1617
  return Result.Default;
1536
1618
  }
1537
1619
 
1538
1620
  fn checkEBC(battle: anytype) bool {
1539
1621
  for (battle.sides, 0..) |side, i| {
1540
- const foe = battle.sides[~@intCast(u1, i)];
1622
+ const foe = battle.sides[~@as(u1, @intCast(i))];
1541
1623
 
1542
1624
  var foe_all_ghosts = true;
1543
1625
  var foe_all_transform = true;
@@ -1589,7 +1671,7 @@ fn checkEBC(battle: anytype) bool {
1589
1671
  return false;
1590
1672
  }
1591
1673
 
1592
- fn disabled(side: *Side, ident: ID, log: anytype) !bool {
1674
+ fn disabled(side: *Side, ident: ID, options: anytype) !bool {
1593
1675
  if (side.active.volatiles.disabled_move != 0) {
1594
1676
  // A Pokémon that transforms after being disabled may end up with less move slots
1595
1677
  const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
@@ -1600,28 +1682,28 @@ fn disabled(side: *Side, ident: ID, log: anytype) !bool {
1600
1682
  side.last_selected_move;
1601
1683
  if (m != .None and m == last) {
1602
1684
  side.active.volatiles.Charging = false;
1603
- try log.disabled(ident, last);
1685
+ try options.log.disabled(ident, last);
1604
1686
  return true;
1605
1687
  }
1606
1688
  }
1607
1689
  return false;
1608
1690
  }
1609
1691
 
1610
- inline fn buildRage(battle: anytype, who: Player, log: anytype) !void {
1692
+ inline fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1611
1693
  const side = battle.side(who);
1612
1694
  if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
1613
- try Effects.boost(battle, who, Move.get(.Rage), log);
1695
+ try Effects.boost(battle, who, Move.get(.Rage), options);
1614
1696
  }
1615
1697
  }
1616
1698
 
1617
- fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
1699
+ fn handleThrashing(battle: anytype, active: *ActivePokemon, player: Player, options: anytype) bool {
1618
1700
  var volatiles = &active.volatiles;
1619
1701
  assert(volatiles.Thrashing);
1620
1702
  if (volatiles.attacks > 0) return false;
1621
1703
 
1622
1704
  volatiles.Thrashing = false;
1623
1705
  volatiles.Confusion = true;
1624
- volatiles.confusion = Rolls.confusionDuration(battle);
1706
+ volatiles.confusion = Rolls.confusionDuration(battle, player, true, options);
1625
1707
  return true;
1626
1708
  }
1627
1709
 
@@ -1631,42 +1713,42 @@ inline fn onBegin(
1631
1713
  move: Move.Data,
1632
1714
  mslot: u8,
1633
1715
  residual: *bool,
1634
- log: anytype,
1716
+ options: anytype,
1635
1717
  ) !void {
1636
1718
  assert(move.effect.onBegin());
1637
1719
  return switch (move.effect) {
1638
- .Confusion => Effects.confusion(battle, player, move, log),
1639
- .Conversion => Effects.conversion(battle, player, log),
1640
- .FocusEnergy => Effects.focusEnergy(battle, player, log),
1641
- .Haze => Effects.haze(battle, player, log),
1642
- .Heal => Effects.heal(battle, player, log),
1643
- .LeechSeed => Effects.leechSeed(battle, player, move, log),
1644
- .LightScreen => Effects.lightScreen(battle, player, log),
1645
- .Mimic => Effects.mimic(battle, player, move, mslot, log),
1646
- .Mist => Effects.mist(battle, player, log),
1647
- .Paralyze => Effects.paralyze(battle, player, move, log),
1648
- .Poison => Effects.poison(battle, player, move, log),
1649
- .Reflect => Effects.reflect(battle, player, log),
1650
- .Splash => Effects.splash(battle, player, log),
1651
- .Substitute => Effects.substitute(battle, player, residual, log),
1652
- .SwitchAndTeleport => Effects.switchAndTeleport(battle, player, move, log),
1653
- .Transform => Effects.transform(battle, player, log),
1720
+ .Confusion => Effects.confusion(battle, player, move, options),
1721
+ .Conversion => Effects.conversion(battle, player, options),
1722
+ .FocusEnergy => Effects.focusEnergy(battle, player, options),
1723
+ .Haze => Effects.haze(battle, player, options),
1724
+ .Heal => Effects.heal(battle, player, options),
1725
+ .LeechSeed => Effects.leechSeed(battle, player, move, options),
1726
+ .LightScreen => Effects.lightScreen(battle, player, options),
1727
+ .Mimic => Effects.mimic(battle, player, move, mslot, options),
1728
+ .Mist => Effects.mist(battle, player, options),
1729
+ .Paralyze => Effects.paralyze(battle, player, move, options),
1730
+ .Poison => Effects.poison(battle, player, move, options),
1731
+ .Reflect => Effects.reflect(battle, player, options),
1732
+ .Splash => Effects.splash(battle, player, options),
1733
+ .Substitute => Effects.substitute(battle, player, residual, options),
1734
+ .SwitchAndTeleport => Effects.switchAndTeleport(battle, player, move, options),
1735
+ .Transform => Effects.transform(battle, player, options),
1654
1736
  else => unreachable,
1655
1737
  };
1656
1738
  }
1657
1739
 
1658
- inline fn onEnd(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
1740
+ inline fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1659
1741
  assert(move.effect.onEnd());
1660
1742
  return switch (move.effect) {
1661
- .Bide => Effects.bide(battle, player, log),
1743
+ .Bide => Effects.bide(battle, player, options),
1662
1744
  // zig fmt: off
1663
1745
  .AttackUp1, .AttackUp2, .DefenseUp1, .DefenseUp2,
1664
1746
  .EvasionUp1, .SpecialUp1, .SpecialUp2, .SpeedUp2 =>
1665
- Effects.boost(battle, player, move, log),
1747
+ Effects.boost(battle, player, move, options),
1666
1748
  .AccuracyDown1, .AttackDown1, .DefenseDown1, .DefenseDown2, .SpeedDown1 =>
1667
- Effects.unboost(battle, player, move, log),
1749
+ Effects.unboost(battle, player, move, options),
1668
1750
  // zig fmt: on
1669
- .Sleep => Effects.sleep(battle, player, move, log),
1751
+ .Sleep => Effects.sleep(battle, player, move, options),
1670
1752
  else => unreachable,
1671
1753
  };
1672
1754
  }
@@ -1676,51 +1758,51 @@ inline fn alwaysHappens(
1676
1758
  player: Player,
1677
1759
  move: Move.Data,
1678
1760
  residual: *bool,
1679
- log: anytype,
1761
+ options: anytype,
1680
1762
  ) !void {
1681
1763
  assert(move.effect.alwaysHappens());
1682
1764
  return switch (move.effect) {
1683
- .DrainHP, .DreamEater => Effects.drainHP(battle, player, log),
1765
+ .DrainHP, .DreamEater => Effects.drainHP(battle, player, options),
1684
1766
  .Explode => Effects.explode(battle, player),
1685
- .PayDay => Effects.payDay(battle, player, log),
1767
+ .PayDay => Effects.payDay(battle, player, options),
1686
1768
  .Rage => Effects.rage(battle, player),
1687
- .Recoil => Effects.recoil(battle, player, residual, log),
1769
+ .Recoil => Effects.recoil(battle, player, residual, options),
1688
1770
  .JumpKick, .Binding => {},
1689
1771
  else => unreachable,
1690
1772
  };
1691
1773
  }
1692
1774
 
1693
- inline fn moveEffect(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
1775
+ inline fn moveEffect(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1694
1776
  return switch (move.effect) {
1695
- .BurnChance1, .BurnChance2 => Effects.burnChance(battle, player, move, log),
1696
- .ConfusionChance => Effects.confusion(battle, player, move, log),
1697
- .FlinchChance1, .FlinchChance2 => Effects.flinchChance(battle, player, move),
1698
- .FreezeChance => Effects.freezeChance(battle, player, move, log),
1699
- .HyperBeam => Effects.hyperBeam(battle, player, log),
1777
+ .BurnChance1, .BurnChance2 => Effects.burnChance(battle, player, move, options),
1778
+ .ConfusionChance => Effects.confusion(battle, player, move, options),
1779
+ .FlinchChance1, .FlinchChance2 => Effects.flinchChance(battle, player, move, options),
1780
+ .FreezeChance => Effects.freezeChance(battle, player, move, options),
1781
+ .HyperBeam => Effects.hyperBeam(battle, player, options),
1700
1782
  .MultiHit, .DoubleHit, .Twineedle => unreachable,
1701
- .ParalyzeChance1, .ParalyzeChance2 => Effects.paralyzeChance(battle, player, move, log),
1702
- .PoisonChance1, .PoisonChance2 => Effects.poison(battle, player, move, log),
1783
+ .ParalyzeChance1, .ParalyzeChance2 => Effects.paralyzeChance(battle, player, move, options),
1784
+ .PoisonChance1, .PoisonChance2 => Effects.poison(battle, player, move, options),
1703
1785
  // zig fmt: off
1704
1786
  .AttackDownChance, .DefenseDownChance, .SpecialDownChance, .SpeedDownChance =>
1705
- Effects.unboost(battle, player, move, log),
1787
+ Effects.unboost(battle, player, move, options),
1706
1788
  // zig fmt: on
1707
1789
  else => {},
1708
1790
  };
1709
1791
  }
1710
1792
 
1711
1793
  pub const Effects = struct {
1712
- fn bide(battle: anytype, player: Player, log: anytype) !void {
1794
+ fn bide(battle: anytype, player: Player, options: anytype) !void {
1713
1795
  var side = battle.side(player);
1714
1796
 
1715
1797
  side.active.volatiles.Bide = true;
1716
1798
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1717
1799
  side.active.volatiles.state = 0;
1718
- side.active.volatiles.attacks = Rolls.bideThrashDuration(battle);
1800
+ side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
1719
1801
 
1720
- try log.start(battle.active(player), .Bide);
1802
+ try options.log.start(battle.active(player), .Bide);
1721
1803
  }
1722
1804
 
1723
- fn burnChance(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
1805
+ fn burnChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1724
1806
  var foe = battle.foe(player);
1725
1807
  var foe_stored = foe.stored();
1726
1808
 
@@ -1731,83 +1813,97 @@ pub const Effects = struct {
1731
1813
  // GLITCH: Freeze top move selection desync can occur if thawed Pokémon is slower
1732
1814
  if (Status.is(foe_stored.status, .FRZ)) {
1733
1815
  assert(move.type == .Fire);
1734
- try log.curestatus(battle.active(player.foe()), foe_stored.status, .Message);
1816
+ try options.log.curestatus(
1817
+ battle.active(player.foe()),
1818
+ foe_stored.status,
1819
+ .Message,
1820
+ );
1735
1821
  foe_stored.status = 0;
1736
1822
  }
1737
1823
  return;
1738
1824
  }
1739
1825
 
1740
1826
  if (foe.active.types.includes(move.type)) return;
1741
- if (!Rolls.secondaryChance(battle, move.effect == .BurnChance1)) return;
1827
+ if (!try Rolls.secondaryChance(battle, player, move.effect == .BurnChance1, options)) {
1828
+ return;
1829
+ }
1742
1830
 
1743
1831
  foe_stored.status = Status.init(.BRN);
1744
1832
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
1745
1833
 
1746
- try log.status(battle.active(player.foe()), foe_stored.status, .None);
1834
+ try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
1747
1835
  }
1748
1836
 
1749
- fn charge(battle: anytype, player: Player, log: anytype) !void {
1837
+ fn charge(battle: anytype, player: Player, options: anytype) !void {
1750
1838
  var side = battle.side(player);
1751
1839
  var volatiles = &side.active.volatiles;
1752
1840
 
1753
1841
  volatiles.Charging = true;
1754
1842
  const move = side.last_selected_move;
1755
1843
  if (move == .Fly or move == .Dig) volatiles.Invulnerable = true;
1756
- try log.laststill();
1757
- try log.prepare(battle.active(player), move);
1844
+ try options.log.laststill();
1845
+ try options.log.prepare(battle.active(player), move);
1758
1846
  }
1759
1847
 
1760
- fn confusion(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
1848
+ fn confusion(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1761
1849
  var foe = battle.foe(player);
1762
1850
  const sub = foe.active.volatiles.Substitute;
1763
1851
 
1764
1852
  if (move.effect == .ConfusionChance) {
1765
- if (!Rolls.confusionChance(battle)) return;
1853
+ if (!try Rolls.confusionChance(battle, player, options)) return;
1766
1854
  } else {
1767
1855
  if (showdown) {
1768
- if (!try checkHit(battle, player, move, log)) {
1856
+ if (!try checkHit(battle, player, move, options)) {
1769
1857
  return;
1770
1858
  } else if (sub) {
1771
- return log.fail(battle.active(player.foe()), .None);
1859
+ return options.log.fail(battle.active(player.foe()), .None);
1772
1860
  }
1773
1861
  } else {
1774
1862
  if (sub) {
1775
- return log.fail(battle.active(player.foe()), .None);
1776
- } else if (!try checkHit(battle, player, move, log)) {
1863
+ return options.log.fail(battle.active(player.foe()), .None);
1864
+ } else if (!try checkHit(battle, player, move, options)) {
1777
1865
  return;
1778
1866
  }
1779
1867
  }
1868
+ try options.chance.commit(player, .hit);
1780
1869
  }
1781
1870
 
1782
1871
  if (foe.active.volatiles.Confusion) return;
1783
1872
  foe.active.volatiles.Confusion = true;
1784
- foe.active.volatiles.confusion = Rolls.confusionDuration(battle);
1873
+ foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
1785
1874
 
1786
- try log.start(battle.active(player.foe()), .Confusion);
1875
+ try options.log.start(battle.active(player.foe()), .Confusion);
1787
1876
  }
1788
1877
 
1789
- fn conversion(battle: anytype, player: Player, log: anytype) !void {
1878
+ fn conversion(battle: anytype, player: Player, options: anytype) !void {
1790
1879
  const foe = battle.foe(player);
1791
1880
 
1792
1881
  if (foe.active.volatiles.Invulnerable) {
1793
- try log.lastmiss();
1794
- return log.miss(battle.active(player));
1882
+ try options.log.lastmiss();
1883
+ return options.log.miss(battle.active(player));
1795
1884
  }
1796
1885
 
1797
1886
  battle.side(player).active.types = foe.active.types;
1798
- return log.typechange(battle.active(player), foe.active.types, battle.active(player.foe()));
1887
+ return options.log.typechange(
1888
+ battle.active(player),
1889
+ foe.active.types,
1890
+ battle.active(player.foe()),
1891
+ );
1799
1892
  }
1800
1893
 
1801
- fn disable(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
1894
+ fn disable(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
1802
1895
  var foe = battle.foe(player);
1803
1896
  var volatiles = &foe.active.volatiles;
1804
1897
  const foe_ident = battle.active(player.foe());
1805
1898
 
1806
1899
  // Pokémon Showdown handles hit/miss earlier in doMove
1807
- if (!showdown and !try checkHit(battle, player, move, log)) return null;
1900
+ if (!showdown) {
1901
+ if (!try checkHit(battle, player, move, options)) return null;
1902
+ try options.chance.commit(player, .hit);
1903
+ }
1808
1904
 
1809
1905
  if (volatiles.disabled_move != 0) {
1810
- try log.fail(foe_ident, .None);
1906
+ try options.log.fail(foe_ident, .None);
1811
1907
  return null;
1812
1908
  }
1813
1909
 
@@ -1823,7 +1919,7 @@ pub const Effects = struct {
1823
1919
  // Technically this is still considered simply a "miss" on the cartridge,
1824
1920
  // but diverging from Pokémon Showdown here would mostly just be pedantic
1825
1921
  if (n == 0) {
1826
- try log.fail(foe_ident, .None);
1922
+ try options.log.fail(foe_ident, .None);
1827
1923
  return null;
1828
1924
  } else if (err) {
1829
1925
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
@@ -1831,14 +1927,16 @@ pub const Effects = struct {
1831
1927
  return Result.Error;
1832
1928
  }
1833
1929
 
1834
- volatiles.disabled_move = @intCast(u3, Rolls.moveSlot(battle, &foe.active.moves, n));
1835
- volatiles.disabled_duration = Rolls.disableDuration(battle);
1930
+ volatiles.disabled_move =
1931
+ @intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
1932
+ volatiles.disabled_duration = Rolls.disableDuration(battle, player, options);
1836
1933
 
1837
- try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
1934
+ const id = foe.active.move(volatiles.disabled_move).id;
1935
+ try options.log.startEffect(foe_ident, .Disable, id);
1838
1936
  return null;
1839
1937
  }
1840
1938
 
1841
- fn drainHP(battle: anytype, player: Player, log: anytype) !void {
1939
+ fn drainHP(battle: anytype, player: Player, options: anytype) !void {
1842
1940
  var side = battle.side(player);
1843
1941
  var stored = side.stored();
1844
1942
 
@@ -1853,7 +1951,7 @@ pub const Effects = struct {
1853
1951
  if (stored.hp == stored.stats.hp) return;
1854
1952
  stored.hp = @min(stored.stats.hp, stored.hp + drain);
1855
1953
 
1856
- try log.drain(battle.active(player), stored, battle.active(player.foe()));
1954
+ try options.log.drain(battle.active(player), stored, battle.active(player.foe()));
1857
1955
  }
1858
1956
 
1859
1957
  fn explode(battle: anytype, player: Player) !void {
@@ -1867,26 +1965,28 @@ pub const Effects = struct {
1867
1965
  side.active.volatiles.LeechSeed = false;
1868
1966
  }
1869
1967
 
1870
- fn flinchChance(battle: anytype, player: Player, move: Move.Data) void {
1968
+ fn flinchChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1871
1969
  var volatiles = &battle.foe(player).active.volatiles;
1872
1970
 
1873
1971
  if (volatiles.Substitute) return;
1874
- if (!Rolls.secondaryChance(battle, move.effect == .FlinchChance1)) return;
1972
+ if (!try Rolls.secondaryChance(battle, player, move.effect == .FlinchChance1, options)) {
1973
+ return;
1974
+ }
1875
1975
 
1876
1976
  volatiles.Flinch = true;
1877
1977
  volatiles.Recharging = false;
1878
1978
  }
1879
1979
 
1880
- fn focusEnergy(battle: anytype, player: Player, log: anytype) !void {
1980
+ fn focusEnergy(battle: anytype, player: Player, options: anytype) !void {
1881
1981
  var side = battle.side(player);
1882
1982
 
1883
1983
  if (side.active.volatiles.FocusEnergy) return;
1884
1984
  side.active.volatiles.FocusEnergy = true;
1885
1985
 
1886
- try log.start(battle.active(player), .FocusEnergy);
1986
+ try options.log.start(battle.active(player), .FocusEnergy);
1887
1987
  }
1888
1988
 
1889
- fn freezeChance(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
1989
+ fn freezeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1890
1990
  var foe = battle.foe(player);
1891
1991
  var foe_stored = foe.stored();
1892
1992
  const foe_ident = battle.active(player.foe());
@@ -1898,17 +1998,18 @@ pub const Effects = struct {
1898
1998
  }
1899
1999
 
1900
2000
  if (foe.active.types.includes(move.type)) return;
1901
- if (!Rolls.secondaryChance(battle, true)) return;
2001
+ if (!try Rolls.secondaryChance(battle, player, true, options)) return;
1902
2002
  // Freeze Clause Mod
1903
2003
  if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1904
2004
 
1905
2005
  foe_stored.status = Status.init(.FRZ);
1906
2006
  // GLITCH: Hyper Beam recharging status is not cleared
1907
2007
 
1908
- try log.status(foe_ident, foe_stored.status, .None);
2008
+ try options.log.status(foe_ident, foe_stored.status, .None);
1909
2009
  }
1910
2010
 
1911
- fn haze(battle: anytype, player: Player, log: anytype) !void {
2011
+ fn haze(battle: anytype, player: Player, options: anytype) !void {
2012
+ var log = options.log;
1912
2013
  var side = battle.side(player);
1913
2014
  var foe = battle.foe(player);
1914
2015
 
@@ -1930,7 +2031,7 @@ pub const Effects = struct {
1930
2031
  // Pokémon Showdown clears P1 then P2 instead of status -> side -> foe
1931
2032
  if (showdown) {
1932
2033
  for (&battle.sides, 0..) |*s, i| {
1933
- const p = @intToEnum(Player, i);
2034
+ const p: Player = @enumFromInt(i);
1934
2035
  // Pokémon Showdown incorrectly does not prevent sleep/freeze from moving
1935
2036
  if (p != player and Status.any(s.stored().status)) {
1936
2037
  try log.curestatus(foe_ident, foe_stored.status, .Silent);
@@ -1939,7 +2040,7 @@ pub const Effects = struct {
1939
2040
  s.stored().status = Status.init(.PSN);
1940
2041
  try log.status(battle.active(p), s.stored().status, .None);
1941
2042
  }
1942
- try clearVolatiles(battle, p, log);
2043
+ try clearVolatiles(battle, p, p != player, options);
1943
2044
  }
1944
2045
  } else {
1945
2046
  if (Status.any(foe_stored.status)) {
@@ -1949,71 +2050,76 @@ pub const Effects = struct {
1949
2050
  try log.curestatus(foe_ident, foe_stored.status, .Silent);
1950
2051
  foe_stored.status = 0;
1951
2052
  }
1952
- try clearVolatiles(battle, player, log);
1953
- try clearVolatiles(battle, player.foe(), log);
2053
+ try clearVolatiles(battle, player, false, options);
2054
+ try clearVolatiles(battle, player.foe(), true, options);
1954
2055
  }
1955
2056
  }
1956
2057
 
1957
- fn heal(battle: anytype, player: Player, log: anytype) !void {
2058
+ fn heal(battle: anytype, player: Player, options: anytype) !void {
1958
2059
  var side = battle.side(player);
1959
2060
  var stored = side.stored();
1960
2061
  const ident = battle.active(player);
1961
2062
 
1962
2063
  // GLITCH: HP recovery move failure glitches
1963
2064
  const delta = stored.stats.hp - stored.hp;
1964
- if (delta == 0 or delta & 255 == 255) return try log.fail(ident, .None);
2065
+ if (delta == 0 or (delta & 255 == 255 and stored.hp % 256 != 0)) {
2066
+ return try options.log.fail(ident, .None);
2067
+ }
1965
2068
 
1966
2069
  const rest = side.last_selected_move == .Rest;
1967
2070
  if (rest) {
1968
2071
  // Adding the sleep status runs the sleep condition handler to roll duration
1969
2072
  if (showdown) battle.rng.advance(1);
1970
2073
  stored.status = Status.slf(2);
1971
- try log.statusFrom(ident, stored.status, Move.Rest);
2074
+ try options.log.statusFrom(ident, stored.status, Move.Rest);
1972
2075
  stored.hp = stored.stats.hp;
1973
2076
  } else {
1974
2077
  stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
1975
2078
  }
1976
- try log.heal(ident, stored, if (rest) Heal.Silent else Heal.None);
2079
+ try options.log.heal(ident, stored, if (rest) Heal.Silent else Heal.None);
1977
2080
  }
1978
2081
 
1979
- fn hyperBeam(battle: anytype, player: Player, log: anytype) !void {
2082
+ fn hyperBeam(battle: anytype, player: Player, options: anytype) !void {
1980
2083
  battle.side(player).active.volatiles.Recharging = true;
1981
- try log.mustrecharge(battle.active(player));
2084
+ try options.log.mustrecharge(battle.active(player));
1982
2085
  }
1983
2086
 
1984
- fn leechSeed(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
2087
+ fn leechSeed(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1985
2088
  var foe = battle.foe(player);
1986
2089
 
1987
2090
  if (showdown) {
1988
2091
  // Invulnerability trumps type immunity on Pokémon Showdown
1989
2092
  if (!foe.active.volatiles.Invulnerable and foe.active.types.includes(.Grass)) {
1990
- return log.immune(battle.active(player.foe()), .None);
2093
+ return options.log.immune(battle.active(player.foe()), .None);
1991
2094
  }
1992
- if (!try checkHit(battle, player, move, log)) return;
2095
+ if (!try checkHit(battle, player, move, options)) return;
1993
2096
  if (foe.active.volatiles.LeechSeed) return;
1994
2097
  } else {
1995
- if (!try checkHit(battle, player, move, log)) return;
2098
+ if (!try checkHit(battle, player, move, options)) return;
1996
2099
  if (foe.active.types.includes(.Grass) or foe.active.volatiles.LeechSeed) {
1997
- try log.lastmiss();
1998
- return log.miss(battle.active(player));
2100
+ try options.log.lastmiss();
2101
+ return options.log.miss(battle.active(player));
1999
2102
  }
2000
2103
  }
2001
2104
 
2105
+ try options.chance.commit(player, .hit);
2002
2106
  foe.active.volatiles.LeechSeed = true;
2003
2107
 
2004
- try log.start(battle.active(player.foe()), .LeechSeed);
2108
+ try options.log.start(battle.active(player.foe()), .LeechSeed);
2005
2109
  }
2006
2110
 
2007
- fn lightScreen(battle: anytype, player: Player, log: anytype) !void {
2111
+ fn lightScreen(battle: anytype, player: Player, options: anytype) !void {
2008
2112
  var side = battle.side(player);
2009
2113
 
2010
- if (side.active.volatiles.LightScreen) return log.fail(battle.active(player), .None);
2114
+ if (side.active.volatiles.LightScreen) {
2115
+ return options.log.fail(battle.active(player), .None);
2116
+ }
2011
2117
  side.active.volatiles.LightScreen = true;
2012
2118
 
2013
- try log.start(battle.active(player), .LightScreen);
2119
+ try options.log.start(battle.active(player), .LightScreen);
2014
2120
  }
2015
2121
 
2016
- fn mimic(battle: anytype, player: Player, move: Move.Data, mslot: u8, log: anytype) !void {
2122
+ fn mimic(battle: anytype, player: Player, move: Move.Data, mslot: u8, options: anytype) !void {
2017
2123
  var side = battle.side(player);
2018
2124
  var foe = battle.foe(player);
2019
2125
 
@@ -2029,7 +2135,7 @@ pub const Effects = struct {
2029
2135
  const has_mimic = has_mimic: {
2030
2136
  for (side.active.moves, 0..) |m, i| {
2031
2137
  if (m.id == .Mimic) {
2032
- oslot = @intCast(u8, i + 1);
2138
+ oslot = @intCast(i + 1);
2033
2139
  break :has_mimic true;
2034
2140
  }
2035
2141
  }
@@ -2037,38 +2143,43 @@ pub const Effects = struct {
2037
2143
  };
2038
2144
  if (!has_mimic) {
2039
2145
  // Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
2040
- if (!try checkHit(battle, player, move, log)) return;
2041
- return try log.fail(battle.active(player.foe()), .None);
2146
+ if (!try checkHit(battle, player, move, options)) return;
2147
+ try options.chance.commit(player, .hit);
2148
+ return try options.log.fail(battle.active(player.foe()), .None);
2042
2149
  }
2043
2150
  }
2044
- if (!try checkHit(battle, player, move, log)) return;
2151
+ if (!try checkHit(battle, player, move, options)) return;
2152
+ try options.chance.commit(player, .hit);
2045
2153
 
2046
- const rslot = Rolls.moveSlot(battle, &foe.active.moves, 0);
2154
+ const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
2047
2155
  side.active.move(oslot).id = foe.active.move(rslot).id;
2048
2156
 
2049
- try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
2157
+ try options.log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
2050
2158
  }
2051
2159
 
2052
- fn mist(battle: anytype, player: Player, log: anytype) !void {
2160
+ fn mist(battle: anytype, player: Player, options: anytype) !void {
2053
2161
  var side = battle.side(player);
2054
2162
 
2055
2163
  if (side.active.volatiles.Mist) return;
2056
2164
  side.active.volatiles.Mist = true;
2057
2165
 
2058
- try log.start(battle.active(player), .Mist);
2166
+ try options.log.start(battle.active(player), .Mist);
2059
2167
  }
2060
2168
 
2061
- fn multiHit(battle: anytype, player: Player, move: Move.Data) void {
2169
+ fn multiHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
2062
2170
  var side = battle.side(player);
2063
2171
 
2064
2172
  assert(!side.active.volatiles.MultiHit);
2065
2173
  side.active.volatiles.MultiHit = true;
2066
2174
 
2067
- side.active.volatiles.attacks =
2068
- if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
2175
+ side.active.volatiles.attacks = if (move.effect == .MultiHit)
2176
+ try Rolls.distribution(battle, .MultiHit, player, options)
2177
+ else
2178
+ 2;
2069
2179
  }
2070
2180
 
2071
- fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
2181
+ fn paralyze(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
2182
+ var log = options.log;
2072
2183
  var foe = battle.foe(player);
2073
2184
  var foe_stored = foe.stored();
2074
2185
  const foe_ident = battle.active(player.foe());
@@ -2079,7 +2190,7 @@ pub const Effects = struct {
2079
2190
  if (showdown) {
2080
2191
  // Invulnerability trumps type immunity on Pokémon Showdown
2081
2192
  if (immune and !foe.active.volatiles.Invulnerable) return log.immune(foe_ident, .None);
2082
- if (!try checkHit(battle, player, move, log)) return;
2193
+ if (!try checkHit(battle, player, move, options)) return;
2083
2194
  }
2084
2195
  if (Status.any(foe_stored.status)) {
2085
2196
  return log.fail(
@@ -2089,8 +2200,9 @@ pub const Effects = struct {
2089
2200
  }
2090
2201
  if (!showdown) {
2091
2202
  if (immune) return log.immune(foe_ident, .None);
2092
- if (!try checkHit(battle, player, move, log)) return;
2203
+ if (!try checkHit(battle, player, move, options)) return;
2093
2204
  }
2205
+ try options.chance.commit(player, .hit);
2094
2206
 
2095
2207
  foe_stored.status = Status.init(.PAR);
2096
2208
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
@@ -2098,7 +2210,7 @@ pub const Effects = struct {
2098
2210
  try log.status(foe_ident, foe_stored.status, .None);
2099
2211
  }
2100
2212
 
2101
- fn paralyzeChance(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
2213
+ fn paralyzeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
2102
2214
  var foe = battle.foe(player);
2103
2215
  var foe_stored = foe.stored();
2104
2216
 
@@ -2110,25 +2222,30 @@ pub const Effects = struct {
2110
2222
 
2111
2223
  // Body Slam can't paralyze a Normal type Pokémon
2112
2224
  if (foe.active.types.includes(move.type)) return;
2113
- if (!Rolls.secondaryChance(battle, move.effect == .ParalyzeChance1)) return;
2225
+ if (!try Rolls.secondaryChance(battle, player, move.effect == .ParalyzeChance1, options)) {
2226
+ return;
2227
+ }
2114
2228
 
2115
2229
  foe_stored.status = Status.init(.PAR);
2116
2230
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2117
2231
 
2118
- try log.status(battle.active(player.foe()), foe_stored.status, .None);
2232
+ try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
2119
2233
  }
2120
2234
 
2121
- fn payDay(battle: anytype, player: Player, log: anytype) !void {
2122
- if (!showdown or !battle.foe(player).active.volatiles.Substitute) try log.fieldactivate();
2235
+ fn payDay(battle: anytype, player: Player, options: anytype) !void {
2236
+ if (!showdown or !battle.foe(player).active.volatiles.Substitute) {
2237
+ try options.log.fieldactivate();
2238
+ }
2123
2239
  }
2124
2240
 
2125
- fn poison(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
2241
+ fn poison(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
2242
+ var log = options.log;
2126
2243
  var foe = battle.foe(player);
2127
2244
  var foe_stored = foe.stored();
2128
2245
  const foe_ident = battle.active(player.foe());
2129
2246
  const toxic = battle.side(player).last_selected_move == .Toxic;
2130
2247
 
2131
- if (showdown and move.effect == .Poison and !try checkHit(battle, player, move, log)) {
2248
+ if (showdown and move.effect == .Poison and !try checkHit(battle, player, move, options)) {
2132
2249
  return;
2133
2250
  } else if (foe.active.volatiles.Substitute) {
2134
2251
  if (move.effect != .Poison) return;
@@ -2152,8 +2269,13 @@ pub const Effects = struct {
2152
2269
  }
2153
2270
 
2154
2271
  if (move.effect == .Poison) {
2155
- if (!showdown and !try checkHit(battle, player, move, log)) return;
2156
- } else if (!Rolls.poisonChance(battle, move.effect == .PoisonChance1)) return;
2272
+ if (!showdown and !try checkHit(battle, player, move, options)) return;
2273
+ try options.chance.commit(player, .hit);
2274
+ } else {
2275
+ if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
2276
+ return;
2277
+ }
2278
+ }
2157
2279
 
2158
2280
  foe_stored.status = Status.init(.PSN);
2159
2281
  if (toxic) {
@@ -2171,28 +2293,36 @@ pub const Effects = struct {
2171
2293
  volatiles.Rage = true;
2172
2294
  }
2173
2295
 
2174
- fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
2296
+ fn recoil(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
2175
2297
  var side = battle.side(player);
2176
2298
  var stored = side.stored();
2177
2299
 
2178
- const damage = @intCast(i16, @max(battle.last_damage /
2300
+ assert(showdown or battle.last_damage > 0);
2301
+ if (showdown and battle.last_damage == 0) return;
2302
+
2303
+ const damage: i16 = @intCast(@max(battle.last_damage /
2179
2304
  @as(u8, if (side.last_selected_move == .Struggle) 2 else 4), 1));
2180
- stored.hp = @intCast(u16, @max(@intCast(i16, stored.hp) - damage, 0));
2305
+ stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
2181
2306
 
2182
- try log.damageOf(battle.active(player), stored, .RecoilOf, battle.active(player.foe()));
2307
+ try options.log.damageOf(
2308
+ battle.active(player),
2309
+ stored,
2310
+ .RecoilOf,
2311
+ battle.active(player.foe()),
2312
+ );
2183
2313
  if (showdown and stored.hp == 0) residual.* = false;
2184
2314
  }
2185
2315
 
2186
- fn reflect(battle: anytype, player: Player, log: anytype) !void {
2316
+ fn reflect(battle: anytype, player: Player, options: anytype) !void {
2187
2317
  var side = battle.side(player);
2188
2318
 
2189
- if (side.active.volatiles.Reflect) return log.fail(battle.active(player), .None);
2319
+ if (side.active.volatiles.Reflect) return options.log.fail(battle.active(player), .None);
2190
2320
  side.active.volatiles.Reflect = true;
2191
2321
 
2192
- try log.start(battle.active(player), .Reflect);
2322
+ try options.log.start(battle.active(player), .Reflect);
2193
2323
  }
2194
2324
 
2195
- fn sleep(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
2325
+ fn sleep(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
2196
2326
  var foe = battle.foe(player);
2197
2327
  var foe_stored = foe.stored();
2198
2328
  const foe_ident = battle.active(player.foe());
@@ -2203,13 +2333,14 @@ pub const Effects = struct {
2203
2333
  // the Sleep Clause Mod might activate, causing us to not actually bypass
2204
2334
  } else {
2205
2335
  if (Status.any(foe_stored.status)) {
2206
- return log.fail(
2336
+ return options.log.fail(
2207
2337
  foe_ident,
2208
2338
  if (Status.is(foe_stored.status, .SLP)) .Sleep else .None,
2209
2339
  );
2210
2340
  }
2211
2341
  // If checkHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2212
- if (!showdown and !try checkHit(battle, player, move, log)) return;
2342
+ if (!showdown and !try checkHit(battle, player, move, options)) return;
2343
+ try options.chance.commit(player, .hit);
2213
2344
  }
2214
2345
 
2215
2346
  // Sleep Clause Mod
@@ -2220,31 +2351,32 @@ pub const Effects = struct {
2220
2351
  }
2221
2352
  foe.active.volatiles.Recharging = false;
2222
2353
 
2223
- foe_stored.status = Status.slp(Rolls.sleepDuration(battle));
2224
- try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
2354
+ foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2355
+ const last = battle.side(player).last_selected_move;
2356
+ try options.log.statusFrom(foe_ident, foe_stored.status, last);
2225
2357
  }
2226
2358
 
2227
- fn splash(battle: anytype, player: Player, log: anytype) !void {
2228
- try log.activate(battle.active(player), .Splash);
2359
+ fn splash(battle: anytype, player: Player, options: anytype) !void {
2360
+ try options.log.activate(battle.active(player), .Splash);
2229
2361
  }
2230
2362
 
2231
- fn substitute(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
2363
+ fn substitute(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
2232
2364
  var side = battle.side(player);
2233
2365
  if (side.active.volatiles.Substitute) {
2234
- try log.fail(battle.active(player), .Substitute);
2366
+ try options.log.fail(battle.active(player), .Substitute);
2235
2367
  return;
2236
2368
  }
2237
2369
 
2238
2370
  assert(side.stored().stats.hp <= 1023);
2239
2371
  // Will be 0 if HP is <= 3 meaning that the user gets a 1 HP Substitute for "free"
2240
- const hp = @intCast(u8, side.stored().stats.hp / 4);
2372
+ const hp: u8 = @intCast(side.stored().stats.hp / 4);
2241
2373
  // Pokénon Showdown incorrectly checks for 1/4 HP based on `target.maxhp / 4` which returns
2242
2374
  // a floating point value and thus only correctly implements the Substitute 1/4 glitch when
2243
2375
  // the target's HP is exactly divisible by 4 (here we're using an inlined divCeil routine to
2244
2376
  // avoid having to convert to floating point)
2245
2377
  const required_hp = if (showdown) @divFloor(side.stored().stats.hp - 1, 4) + 1 else hp;
2246
2378
  if (side.stored().hp < required_hp) {
2247
- try log.fail(battle.active(player), .Weak);
2379
+ try options.log.fail(battle.active(player), .Weak);
2248
2380
  return;
2249
2381
  }
2250
2382
 
@@ -2252,31 +2384,32 @@ pub const Effects = struct {
2252
2384
  side.stored().hp -= hp;
2253
2385
  side.active.volatiles.substitute = hp + 1;
2254
2386
  side.active.volatiles.Substitute = true;
2255
- try log.start(battle.active(player), .Substitute);
2387
+ try options.log.start(battle.active(player), .Substitute);
2256
2388
  if (hp > 0) {
2257
- try log.damage(battle.active(player), side.stored(), .None);
2389
+ try options.log.damage(battle.active(player), side.stored(), .None);
2258
2390
  if (showdown and side.stored().hp == 0) residual.* = false;
2259
2391
  }
2260
2392
  }
2261
2393
 
2262
- fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
2394
+ fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
2263
2395
  if (!showdown or battle.side(player).last_selected_move == .Teleport) return;
2264
2396
 
2265
2397
  // Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
2266
- _ = try checkHit(battle, player, move, log);
2398
+ if (try checkHit(battle, player, move, options)) try options.chance.commit(player, .hit);
2399
+
2267
2400
  battle.last_damage = 0;
2268
2401
  }
2269
2402
 
2270
- fn thrashing(battle: anytype, player: Player) void {
2403
+ fn thrashing(battle: anytype, player: Player, options: anytype) void {
2271
2404
  var volatiles = &battle.side(player).active.volatiles;
2272
2405
  assert(!volatiles.Thrashing);
2273
2406
  assert(!volatiles.Bide);
2274
2407
 
2275
2408
  volatiles.Thrashing = true;
2276
- volatiles.attacks = Rolls.bideThrashDuration(battle);
2409
+ volatiles.attacks = Rolls.attackingDuration(battle, player, options);
2277
2410
  }
2278
2411
 
2279
- fn transform(battle: anytype, player: Player, log: anytype) !void {
2412
+ fn transform(battle: anytype, player: Player, options: anytype) !void {
2280
2413
  var side = battle.side(player);
2281
2414
  const foe = battle.foe(player);
2282
2415
  const foe_ident = battle.active(player.foe());
@@ -2302,10 +2435,10 @@ pub const Effects = struct {
2302
2435
  side.active.moves[i].pp = if (m.id != .None) 5 else 0;
2303
2436
  }
2304
2437
 
2305
- try log.transform(battle.active(player), foe_ident);
2438
+ try options.log.transform(battle.active(player), foe_ident);
2306
2439
  }
2307
2440
 
2308
- fn binding(battle: anytype, player: Player, rewrap: bool) void {
2441
+ fn binding(battle: anytype, player: Player, rewrap: bool, options: anytype) !void {
2309
2442
  var side = battle.side(player);
2310
2443
  var foe = battle.foe(player);
2311
2444
 
@@ -2317,10 +2450,12 @@ pub const Effects = struct {
2317
2450
  foe.active.volatiles.Recharging = false;
2318
2451
  } else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
2319
2452
 
2320
- side.active.volatiles.attacks = Rolls.distribution(battle) - 1;
2453
+ side.active.volatiles.attacks =
2454
+ try Rolls.distribution(battle, .Binding, player, options) - 1;
2321
2455
  }
2322
2456
 
2323
- fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
2457
+ fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
2458
+ var log = options.log;
2324
2459
  var side = battle.side(player);
2325
2460
  const ident = battle.active(player);
2326
2461
 
@@ -2332,7 +2467,7 @@ pub const Effects = struct {
2332
2467
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2333
2468
  if (boosts.atk == 6) return try log.fail(ident, .None);
2334
2469
  const n: u2 = if (move.effect == .AttackUp2) 2 else 1;
2335
- boosts.atk = @intCast(i4, @min(6, @as(i8, boosts.atk) + n));
2470
+ boosts.atk = @as(i4, @intCast(@min(6, @as(i8, boosts.atk) + n)));
2336
2471
  const reason = if (move.effect == .Rage) Boost.Rage else Boost.Attack;
2337
2472
  if (stats.atk == MAX_STAT_VALUE) {
2338
2473
  boosts.atk -= 1;
@@ -2343,7 +2478,7 @@ pub const Effects = struct {
2343
2478
  }
2344
2479
  return try log.fail(ident, .None);
2345
2480
  }
2346
- var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2481
+ var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2347
2482
  const stat = unmodifiedStats(battle, side).atk;
2348
2483
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2349
2484
  try log.boost(ident, reason, n);
@@ -2354,7 +2489,7 @@ pub const Effects = struct {
2354
2489
  assert(boosts.def >= -6 and boosts.def <= 6);
2355
2490
  if (boosts.def == 6) return try log.fail(ident, .None);
2356
2491
  const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
2357
- boosts.def = @intCast(i4, @min(6, @as(i8, boosts.def) + n));
2492
+ boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
2358
2493
  if (stats.def == MAX_STAT_VALUE) {
2359
2494
  boosts.def -= 1;
2360
2495
  if (showdown) {
@@ -2363,7 +2498,7 @@ pub const Effects = struct {
2363
2498
  }
2364
2499
  return try log.fail(ident, .None);
2365
2500
  }
2366
- var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2501
+ var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2367
2502
  const stat = unmodifiedStats(battle, side).def;
2368
2503
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2369
2504
  try log.boost(ident, .Defense, n);
@@ -2371,7 +2506,7 @@ pub const Effects = struct {
2371
2506
  .SpeedUp2 => {
2372
2507
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2373
2508
  if (boosts.spe == 6) return try log.fail(ident, .None);
2374
- boosts.spe = @intCast(i4, @min(6, @as(i8, boosts.spe) + 2));
2509
+ boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
2375
2510
  if (stats.spe == MAX_STAT_VALUE) {
2376
2511
  boosts.spe -= 1;
2377
2512
  if (showdown) {
@@ -2380,7 +2515,7 @@ pub const Effects = struct {
2380
2515
  }
2381
2516
  return try log.fail(ident, .None);
2382
2517
  }
2383
- var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2518
+ var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2384
2519
  const stat = unmodifiedStats(battle, side).spe;
2385
2520
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2386
2521
  try log.boost(ident, .Speed, 2);
@@ -2389,7 +2524,7 @@ pub const Effects = struct {
2389
2524
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2390
2525
  if (boosts.spc == 6) return try log.fail(ident, .None);
2391
2526
  const n: u2 = if (move.effect == .SpecialUp2) 2 else 1;
2392
- boosts.spc = @intCast(i4, @min(6, @as(i8, boosts.spc) + n));
2527
+ boosts.spc = @intCast(@min(6, @as(i8, boosts.spc) + n));
2393
2528
  if (stats.spc == MAX_STAT_VALUE) {
2394
2529
  boosts.spc -= 1;
2395
2530
  if (showdown) {
@@ -2400,7 +2535,7 @@ pub const Effects = struct {
2400
2535
  }
2401
2536
  return try log.fail(ident, .None);
2402
2537
  }
2403
- var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2538
+ var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2404
2539
  const stat = unmodifiedStats(battle, side).spc;
2405
2540
  stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2406
2541
  try log.boost(ident, .SpecialAttack, n);
@@ -2409,7 +2544,7 @@ pub const Effects = struct {
2409
2544
  .EvasionUp1 => {
2410
2545
  assert(boosts.evasion >= -6 and boosts.evasion <= 6);
2411
2546
  if (boosts.evasion == 6) return try log.fail(ident, .None);
2412
- boosts.evasion = @intCast(i4, @min(6, @as(i8, boosts.evasion) + 1));
2547
+ boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
2413
2548
  try log.boost(ident, .Evasion, 1);
2414
2549
  },
2415
2550
  else => unreachable,
@@ -2419,28 +2554,32 @@ pub const Effects = struct {
2419
2554
  statusModify(battle.foe(player).stored().status, &battle.foe(player).active.stats);
2420
2555
  }
2421
2556
 
2422
- fn unboost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
2557
+ fn unboost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
2558
+ var log = options.log;
2423
2559
  var foe = battle.foe(player);
2424
2560
  const foe_ident = battle.active(player.foe());
2561
+ const secondary = move.effect.isStatDownChance();
2425
2562
 
2426
- if (foe.active.volatiles.Substitute) {
2427
- return if (!move.effect.isStatDownChance()) log.fail(foe_ident, .None);
2428
- }
2563
+ if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(foe_ident, .None);
2429
2564
 
2430
- if (move.effect.isStatDownChance()) {
2431
- if (!Rolls.unboost(battle) or foe.active.volatiles.Invulnerable) return;
2432
- } else if (!showdown and !try checkHit(battle, player, move, log)) {
2433
- return; // checkHit already checks for Invulnerable
2565
+ if (secondary) {
2566
+ const proc = try Rolls.unboost(battle, player, options);
2567
+ if (!proc or foe.active.volatiles.Invulnerable) return;
2568
+ } else {
2569
+ // checkHit already checks for Invulnerable
2570
+ if (!showdown and !try checkHit(battle, player, move, options)) return;
2571
+ try options.chance.commit(player, .hit);
2434
2572
  }
2435
2573
 
2436
2574
  var stats = &foe.active.stats;
2437
2575
  var boosts = &foe.active.boosts;
2576
+ const fail = showdown or !secondary;
2438
2577
 
2439
2578
  switch (move.effect) {
2440
2579
  .AttackDown1, .AttackDownChance => {
2441
2580
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2442
- if (boosts.atk == -6) return try log.fail(foe_ident, .None);
2443
- boosts.atk = @intCast(i4, @max(-6, @as(i8, boosts.atk) - 1));
2581
+ if (boosts.atk == -6) return if (fail) try log.fail(foe_ident, .None);
2582
+ boosts.atk = @intCast(@max(-6, @as(i8, boosts.atk) - 1));
2444
2583
  if (stats.atk == 1) {
2445
2584
  boosts.atk += 1;
2446
2585
  if (showdown) {
@@ -2449,16 +2588,16 @@ pub const Effects = struct {
2449
2588
  }
2450
2589
  return try log.fail(foe_ident, .None);
2451
2590
  }
2452
- var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2591
+ var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2453
2592
  const stat = unmodifiedStats(battle, foe).atk;
2454
2593
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2455
2594
  try log.boost(foe_ident, .Attack, -1);
2456
2595
  },
2457
2596
  .DefenseDown1, .DefenseDown2, .DefenseDownChance => {
2458
2597
  assert(boosts.def >= -6 and boosts.def <= 6);
2459
- if (boosts.def == -6) return try log.fail(foe_ident, .None);
2598
+ if (boosts.def == -6) return if (fail) try log.fail(foe_ident, .None);
2460
2599
  const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
2461
- boosts.def = @intCast(i4, @max(-6, @as(i8, boosts.def) - n));
2600
+ boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
2462
2601
  if (stats.def == 1) {
2463
2602
  boosts.def += 1;
2464
2603
  if (showdown) {
@@ -2467,15 +2606,15 @@ pub const Effects = struct {
2467
2606
  }
2468
2607
  return try log.fail(foe_ident, .None);
2469
2608
  }
2470
- var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2609
+ var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2471
2610
  const stat = unmodifiedStats(battle, foe).def;
2472
2611
  stats.def = @max(1, stat * mod[0] / mod[1]);
2473
2612
  try log.boost(foe_ident, .Defense, -@as(i8, n));
2474
2613
  },
2475
2614
  .SpeedDown1, .SpeedDownChance => {
2476
2615
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2477
- if (boosts.spe == -6) return try log.fail(foe_ident, .None);
2478
- boosts.spe = @intCast(i4, @max(-6, @as(i8, boosts.spe) - 1));
2616
+ if (boosts.spe == -6) return if (fail) try log.fail(foe_ident, .None);
2617
+ boosts.spe = @intCast(@max(-6, @as(i8, boosts.spe) - 1));
2479
2618
  if (stats.spe == 1) {
2480
2619
  boosts.spe += 1;
2481
2620
  if (showdown) {
@@ -2484,7 +2623,7 @@ pub const Effects = struct {
2484
2623
  }
2485
2624
  return try log.fail(foe_ident, .None);
2486
2625
  }
2487
- var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2626
+ var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2488
2627
  const stat = unmodifiedStats(battle, foe).spe;
2489
2628
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2490
2629
  try log.boost(foe_ident, .Speed, -1);
@@ -2492,8 +2631,8 @@ pub const Effects = struct {
2492
2631
  },
2493
2632
  .SpecialDownChance => {
2494
2633
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2495
- if (boosts.spc == -6) return try log.fail(foe_ident, .None);
2496
- boosts.spc = @intCast(i4, @max(-6, @as(i8, boosts.spc) - 1));
2634
+ if (boosts.spc == -6) return if (fail) try log.fail(foe_ident, .None);
2635
+ boosts.spc = @intCast(@max(-6, @as(i8, boosts.spc) - 1));
2497
2636
  if (stats.spc == 1) {
2498
2637
  boosts.spc += 1;
2499
2638
  if (showdown) {
@@ -2504,7 +2643,7 @@ pub const Effects = struct {
2504
2643
  }
2505
2644
  return try log.fail(foe_ident, .None);
2506
2645
  }
2507
- var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2646
+ var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2508
2647
  const stat = unmodifiedStats(battle, foe).spc;
2509
2648
  stats.spc = @max(1, stat * mod[0] / mod[1]);
2510
2649
  try log.boost(foe_ident, .SpecialAttack, -1);
@@ -2512,8 +2651,8 @@ pub const Effects = struct {
2512
2651
  },
2513
2652
  .AccuracyDown1 => {
2514
2653
  assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
2515
- if (boosts.accuracy == -6) return try log.fail(foe_ident, .None);
2516
- boosts.accuracy = @intCast(i4, @max(-6, @as(i8, boosts.accuracy) - 1));
2654
+ if (boosts.accuracy == -6) return if (fail) try log.fail(foe_ident, .None);
2655
+ boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
2517
2656
  try log.boost(foe_ident, .Accuracy, -1);
2518
2657
  },
2519
2658
  else => unreachable,
@@ -2538,16 +2677,19 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
2538
2677
  }
2539
2678
  }
2540
2679
 
2541
- inline fn isForced(active: ActivePokemon) bool {
2680
+ inline fn isForced(active: anytype) bool {
2542
2681
  return active.volatiles.Recharging or active.volatiles.Rage or
2543
2682
  active.volatiles.Thrashing or active.volatiles.Charging;
2544
2683
  }
2545
2684
 
2546
- fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
2685
+ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !void {
2686
+ var log = options.log;
2547
2687
  var side = battle.side(who);
2548
2688
  var volatiles = &side.active.volatiles;
2549
2689
  const ident = battle.active(who);
2550
2690
 
2691
+ options.chance.clearDurations(who, clear);
2692
+
2551
2693
  if (volatiles.disabled_move != 0) {
2552
2694
  volatiles.disabled_move = 0;
2553
2695
  volatiles.disabled_duration = 0;
@@ -2591,137 +2733,299 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
2591
2733
  }
2592
2734
 
2593
2735
  pub const Rolls = struct {
2594
- inline fn speedTie(battle: anytype) bool {
2595
- if (showdown) return battle.rng.range(u8, 0, 2) == 0;
2596
- return battle.rng.next() < Gen12.percent(50) + 1;
2597
- }
2736
+ inline fn speedTie(battle: anytype, options: anytype) !bool {
2737
+ const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
2738
+ player == .P1
2739
+ else if (showdown)
2740
+ battle.rng.range(u8, 0, 2) == 0
2741
+ else
2742
+ battle.rng.next() < Gen12.percent(50) + 1;
2598
2743
 
2599
- inline fn criticalHit(battle: anytype, chance: u16) bool {
2600
- if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
2601
- return std.math.rotl(u8, battle.rng.next(), 3) < chance;
2744
+ try options.chance.speedTie(p1);
2745
+ return p1;
2602
2746
  }
2603
2747
 
2604
- inline fn damage(battle: anytype) u8 {
2605
- if (showdown) return battle.rng.range(u8, 217, 256);
2606
- while (true) {
2607
- const r = std.math.rotr(u8, battle.rng.next(), 1);
2608
- if (r >= 217) return r;
2609
- }
2610
- }
2748
+ inline fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2749
+ const crit = if (options.calc.overridden(player, .critical_hit)) |val|
2750
+ val == .true
2751
+ else if (showdown)
2752
+ battle.rng.chance(u8, rate, 256)
2753
+ else
2754
+ std.math.rotl(u8, battle.rng.next(), 3) < rate;
2611
2755
 
2612
- inline fn hit(battle: anytype, accuracy: u16) bool {
2613
- if (showdown) return battle.rng.chance(u8, @intCast(u8, accuracy), 256);
2614
- return battle.rng.next() < accuracy;
2756
+ try options.chance.criticalHit(player, crit, rate);
2757
+ return crit;
2615
2758
  }
2616
2759
 
2617
- inline fn confusionDuration(battle: anytype) u3 {
2618
- if (showdown) return @intCast(u3, battle.rng.range(u8, 2, 6));
2619
- return @intCast(u3, (battle.rng.next() & 3) + 2);
2620
- }
2760
+ inline fn damage(battle: anytype, player: anytype, options: anytype) !u8 {
2761
+ const roll = if (options.calc.overridden(player, .damage)) |val|
2762
+ val
2763
+ else roll: {
2764
+ if (showdown) break :roll battle.rng.range(u8, 217, 256);
2765
+ while (true) {
2766
+ const r = std.math.rotr(u8, battle.rng.next(), 1);
2767
+ if (r >= 217) break :roll r;
2768
+ }
2769
+ };
2621
2770
 
2622
- inline fn confused(battle: anytype) bool {
2623
- if (showdown) return !battle.rng.chance(u8, 128, 256);
2624
- return battle.rng.next() >= Gen12.percent(50) + 1;
2771
+ assert(roll >= 217 and roll <= 255);
2772
+ try options.chance.damage(player, roll);
2773
+ return roll;
2625
2774
  }
2626
2775
 
2627
- inline fn paralyzed(battle: anytype) bool {
2628
- if (showdown) return battle.rng.chance(u8, 63, 256);
2629
- return battle.rng.next() < Gen12.percent(25);
2776
+ inline fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) bool {
2777
+ const ok = if (options.calc.overridden(player, .hit)) |val|
2778
+ val == .true
2779
+ else if (showdown)
2780
+ battle.rng.chance(u8, accuracy, 256)
2781
+ else
2782
+ battle.rng.next() < accuracy;
2783
+
2784
+ options.chance.hit(ok, accuracy);
2785
+ return ok;
2630
2786
  }
2631
2787
 
2632
- inline fn confusionChance(battle: anytype) bool {
2633
- if (showdown) return battle.rng.chance(u8, 25, 256);
2634
- return battle.rng.next() < Gen12.percent(10);
2788
+ inline fn confused(battle: anytype, player: Player, options: anytype) !bool {
2789
+ const cfz = if (options.calc.overridden(player, .confused)) |val|
2790
+ val == .true
2791
+ else if (showdown)
2792
+ !battle.rng.chance(u8, 128, 256)
2793
+ else
2794
+ battle.rng.next() >= Gen12.percent(50) + 1;
2795
+
2796
+ try options.chance.confused(player, cfz);
2797
+ return cfz;
2635
2798
  }
2636
2799
 
2637
- inline fn secondaryChance(battle: anytype, low: bool) bool {
2638
- if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 26 else 77), 256);
2639
- return battle.rng.next() < 1 + (if (low) Gen12.percent(10) else Gen12.percent(30));
2800
+ inline fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2801
+ const par = if (options.calc.overridden(player, .paralyzed)) |val|
2802
+ val == .true
2803
+ else if (showdown)
2804
+ battle.rng.chance(u8, 63, 256)
2805
+ else
2806
+ battle.rng.next() < Gen12.percent(25);
2807
+
2808
+ try options.chance.paralyzed(player, par);
2809
+ return par;
2640
2810
  }
2641
2811
 
2642
- inline fn poisonChance(battle: anytype, low: bool) bool {
2643
- if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 52 else 103), 256);
2644
- return battle.rng.next() < 1 + (if (low) Gen12.percent(20) else Gen12.percent(40));
2812
+ inline fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2813
+ const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2814
+ val == .true
2815
+ else if (showdown)
2816
+ battle.rng.chance(u8, 25, 256)
2817
+ else
2818
+ battle.rng.next() < Gen12.percent(10);
2819
+
2820
+ try options.chance.secondaryChance(player, proc, 25);
2821
+ return proc;
2645
2822
  }
2646
2823
 
2647
- inline fn sleepDuration(battle: anytype) u3 {
2648
- if (showdown) return @intCast(u3, battle.rng.range(u8, 1, 8));
2649
- while (true) {
2650
- const r = battle.rng.next() & 7;
2651
- if (r != 0) return @intCast(u3, r);
2652
- }
2824
+ inline fn secondaryChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2825
+ const rate: u8 = if (low) 26 else 77;
2826
+
2827
+ const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2828
+ val == .true
2829
+ else if (showdown)
2830
+ battle.rng.chance(u8, rate, 256)
2831
+ else
2832
+ battle.rng.next() < 1 + (if (low) Gen12.percent(10) else Gen12.percent(30));
2833
+
2834
+ try options.chance.secondaryChance(player, proc, rate);
2835
+ return proc;
2653
2836
  }
2654
2837
 
2655
- inline fn bideThrashDuration(battle: anytype) u3 {
2656
- if (showdown) return @intCast(u3, battle.rng.range(u4, 2, 4));
2657
- return @intCast(u3, (battle.rng.next() & 1) + 2);
2838
+ inline fn poisonChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2839
+ const rate: u8 = if (low) 52 else 103;
2840
+
2841
+ const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2842
+ val == .true
2843
+ else if (showdown)
2844
+ battle.rng.chance(u8, rate, 256)
2845
+ else
2846
+ battle.rng.next() < 1 + (if (low) Gen12.percent(20) else Gen12.percent(40));
2847
+
2848
+ try options.chance.secondaryChance(player, proc, rate);
2849
+ return proc;
2658
2850
  }
2659
2851
 
2660
- fn moveSlot(battle: anytype, moves: []MoveSlot, check_pp: u4) u4 {
2661
- if (showdown) {
2662
- if (check_pp == 0) {
2663
- var i: usize = moves.len;
2664
- while (i > 0) {
2665
- i -= 1;
2666
- if (moves[i].id != .None) {
2667
- return battle.rng.range(u4, 0, @intCast(u4, i + 1)) + 1;
2668
- }
2669
- }
2670
- } else {
2671
- var r = battle.rng.range(u4, 0, @intCast(u4, check_pp)) + 1;
2672
- var i: usize = 0;
2673
- while (i < moves.len and r > 0) : (i += 1) {
2674
- if (moves[i].pp > 0) {
2675
- r -= 1;
2676
- if (r == 0) break;
2677
- }
2678
- }
2679
- return @intCast(u4, i + 1);
2852
+ inline fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2853
+ const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2854
+ val == .true
2855
+ else if (showdown)
2856
+ battle.rng.chance(u8, 85, 256)
2857
+ else
2858
+ battle.rng.next() < Gen12.percent(33) + 1;
2859
+
2860
+ try options.chance.secondaryChance(player, proc, 85);
2861
+ return proc;
2862
+ }
2863
+
2864
+ inline fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2865
+ const move: Move = if (options.calc.overridden(player, .metronome)) |val|
2866
+ val
2867
+ else if (showdown) move: {
2868
+ const r = battle.rng.range(u8, 0, @intFromEnum(Move.Struggle) - 2);
2869
+ const mod = @as(u2, (if (r < @intFromEnum(Move.Metronome) - 1) 1 else 2));
2870
+ break :move @enumFromInt(r + mod);
2871
+ } else move: {
2872
+ while (true) {
2873
+ const r = battle.rng.next();
2874
+ if (r == 0 or r == @intFromEnum(Move.Metronome)) continue;
2875
+ if (r >= @intFromEnum(Move.Struggle)) continue;
2876
+ break :move @enumFromInt(r);
2680
2877
  }
2681
- }
2878
+ };
2682
2879
 
2683
- while (true) {
2684
- const r = @intCast(u4, battle.rng.next() & 3);
2685
- if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
2686
- }
2880
+ try options.chance.metronome(player, move);
2881
+ return move;
2687
2882
  }
2688
2883
 
2689
- inline fn disableDuration(battle: anytype) u4 {
2690
- if (showdown) return @intCast(u4, battle.rng.range(u8, 1, 9));
2691
- return @intCast(u4, (battle.rng.next() & 7) + 1);
2884
+ inline fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2885
+ const power = if (options.calc.overridden(player, .psywave)) |val|
2886
+ val - 1
2887
+ else if (showdown)
2888
+ battle.rng.range(u8, 0, max)
2889
+ else power: {
2890
+ while (true) {
2891
+ const r = battle.rng.next();
2892
+ if (r < max) break :power r;
2893
+ }
2894
+ };
2895
+
2896
+ assert(power < max);
2897
+ try options.chance.psywave(player, power, max);
2898
+ return power;
2692
2899
  }
2693
2900
 
2694
- inline fn metronome(battle: anytype) Move {
2695
- if (showdown) {
2696
- const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
2697
- const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
2698
- return @intToEnum(Move, r + mod);
2699
- }
2700
- while (true) {
2701
- const r = battle.rng.next();
2702
- if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
2703
- if (r >= @enumToInt(Move.Struggle)) continue;
2704
- return @intToEnum(Move, r);
2705
- }
2901
+ inline fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2902
+ const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2903
+ @intCast(val)
2904
+ else if (showdown)
2905
+ @intCast(battle.rng.range(u8, 1, 8))
2906
+ else duration: {
2907
+ while (true) {
2908
+ const r = battle.rng.next() & 7;
2909
+ if (r != 0) break :duration @intCast(r);
2910
+ }
2911
+ };
2912
+
2913
+ assert(duration >= 1 and duration <= 7);
2914
+ options.chance.duration(.sleep, player, player.foe(), duration);
2915
+ return duration;
2706
2916
  }
2707
2917
 
2708
- inline fn psywave(battle: anytype, max: u8) u8 {
2709
- if (showdown) return battle.rng.range(u8, 0, max);
2710
- while (true) {
2711
- const r = battle.rng.next();
2712
- if (r < max) return r;
2713
- }
2918
+ inline fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2919
+ const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
2920
+ val
2921
+ else if (showdown)
2922
+ @intCast(battle.rng.range(u8, 1, 9))
2923
+ else
2924
+ @intCast((battle.rng.next() & 7) + 1);
2925
+
2926
+ assert(duration >= 1 and duration <= 8);
2927
+ options.chance.duration(.disable, player, player.foe(), duration);
2928
+ return duration;
2714
2929
  }
2715
2930
 
2716
- inline fn distribution(battle: anytype) u3 {
2717
- if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
2718
- const r = (battle.rng.next() & 3);
2719
- return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
2931
+ inline fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
2932
+ const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2933
+ @intCast(val)
2934
+ else if (showdown)
2935
+ @intCast(battle.rng.range(u8, 2, 6))
2936
+ else
2937
+ @intCast((battle.rng.next() & 3) + 2);
2938
+
2939
+ assert(duration >= 2 and duration <= 5);
2940
+ options.chance.duration(.confusion, player, if (self) player else player.foe(), duration);
2941
+ return duration;
2720
2942
  }
2721
2943
 
2722
- inline fn unboost(battle: anytype) bool {
2723
- if (showdown) return battle.rng.chance(u8, 85, 256);
2724
- return battle.rng.next() < Gen12.percent(33) + 1;
2944
+ inline fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2945
+ const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2946
+ @intCast(val)
2947
+ else if (showdown)
2948
+ @intCast(battle.rng.range(u4, 2, 4))
2949
+ else
2950
+ @intCast((battle.rng.next() & 1) + 2);
2951
+
2952
+ assert(duration >= 2 and duration <= 3);
2953
+ options.chance.duration(.attacking, player, player, duration);
2954
+ return duration;
2955
+ }
2956
+
2957
+ inline fn distribution(
2958
+ battle: anytype,
2959
+ comptime effect: Move.Effect,
2960
+ player: Player,
2961
+ options: anytype,
2962
+ ) !u3 {
2963
+ const roll = if (effect == .Binding) .duration else .multi_hit;
2964
+ const n: u3 = if (options.calc.overridden(player, roll)) |val|
2965
+ @intCast(val)
2966
+ else if (showdown)
2967
+ DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)]
2968
+ else n: {
2969
+ const r = (battle.rng.next() & 3);
2970
+ break :n @intCast((if (r < 2) r else battle.rng.next() & 3) + 2);
2971
+ };
2972
+
2973
+ assert(n >= 2 and n <= 5);
2974
+ if (effect == .Binding) {
2975
+ options.chance.duration(.binding, player, player, n);
2976
+ } else {
2977
+ try options.chance.multiHit(player, n);
2978
+ }
2979
+ return n;
2980
+ }
2981
+
2982
+ fn moveSlot(
2983
+ battle: anytype,
2984
+ player: Player,
2985
+ moves: []MoveSlot,
2986
+ check_pp: u4,
2987
+ options: anytype,
2988
+ ) !u4 {
2989
+ // TODO: consider throwing error instead of rerolling?
2990
+ const overridden = if (options.calc.overridden(player, .move_slot)) |val|
2991
+ if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
2992
+ val
2993
+ else
2994
+ null
2995
+ else
2996
+ null;
2997
+
2998
+ const slot: u4 = overridden orelse slot: {
2999
+ if (showdown) {
3000
+ if (check_pp == 0) {
3001
+ var i: usize = moves.len;
3002
+ while (i > 0) {
3003
+ i -= 1;
3004
+ if (moves[i].id != .None) {
3005
+ break :slot battle.rng.range(u4, 0, @as(u4, @intCast(i + 1))) + 1;
3006
+ }
3007
+ }
3008
+ } else {
3009
+ var r = battle.rng.range(u4, 0, check_pp) + 1;
3010
+ var i: usize = 0;
3011
+ while (i < moves.len and r > 0) : (i += 1) {
3012
+ if (moves[i].pp > 0) {
3013
+ r -= 1;
3014
+ if (r == 0) break;
3015
+ }
3016
+ }
3017
+ break :slot @intCast(i + 1);
3018
+ }
3019
+ }
3020
+
3021
+ while (true) {
3022
+ const r: u4 = @intCast(battle.rng.next() & 3);
3023
+ if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) break :slot r + 1;
3024
+ }
3025
+ };
3026
+
3027
+ try options.chance.moveSlot(player, slot, moves, check_pp);
3028
+ return slot;
2725
3029
  }
2726
3030
  };
2727
3031
 
@@ -2729,7 +3033,7 @@ test "RNG agreement" {
2729
3033
  if (!showdown) return;
2730
3034
  var expected: [256]u32 = undefined;
2731
3035
  for (0..expected.len) |i| {
2732
- expected[i] = @intCast(u32, i * 0x1000000);
3036
+ expected[i] = @intCast(i * 0x1000000);
2733
3037
  }
2734
3038
 
2735
3039
  var spe = rng.FixedRNG(1, expected.len){ .rolls = expected };
@@ -2772,7 +3076,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
2772
3076
  const side = battle.side(player);
2773
3077
  const foe = battle.foe(player);
2774
3078
 
2775
- var active = side.active;
3079
+ var active = &side.active;
2776
3080
  const stored = side.stored();
2777
3081
 
2778
3082
  // While players are not given any input options on the cartridge in these cases,
@@ -2786,7 +3090,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
2786
3090
  // strictly correct as the player would not have been presented the option to move or
2787
3091
  // switch at all.
2788
3092
  if (isForced(active)) {
2789
- out[n] = .{ .type = .Move, .data = @boolToInt(showdown) };
3093
+ out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
2790
3094
  n += 1;
2791
3095
  return n;
2792
3096
  }