@pkmn/engine 0.1.0-dev.e5b5a998 → 0.1.0-dev.f3aaee39

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