@pkmn/engine 0.1.0-dev.fb5bf955 → 0.1.0-dev.fc150752

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