@pkmn/engine 0.1.0-dev.8b83af6b → 0.1.0-dev.900756a9

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