@pkmn/engine 0.1.0-dev.979a393a → 0.1.0-dev.a02611a4

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