@pkmn/engine 0.1.0-dev.84cc1a02 → 0.1.0-dev.90d6c14c

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