@pkmn/engine 0.1.0-dev.d716e940 → 0.1.0-dev.dd5b9081

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