@pkmn/engine 0.1.0-dev.4d039f83 → 0.1.0-dev.526bb2e1

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