@pkmn/engine 0.1.0-dev.b9fb6eab → 0.1.0-dev.bcb16a6c

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 (57) hide show
  1. package/README.md +98 -59
  2. package/build/pkg/addon.d.ts +7 -0
  3. package/build/pkg/addon.js +112 -0
  4. package/build/pkg/addon.js.map +1 -0
  5. package/build/pkg/data/ids.json +12 -12
  6. package/build/pkg/data/layout.json +25 -3
  7. package/build/pkg/data/protocol.json +69 -12
  8. package/build/pkg/data.d.ts +3 -4
  9. package/build/pkg/data.js +3 -5
  10. package/build/pkg/data.js.map +1 -1
  11. package/build/pkg/gen1.d.ts +5 -4
  12. package/build/pkg/gen1.js +47 -31
  13. package/build/pkg/gen1.js.map +1 -1
  14. package/build/pkg/index.d.ts +15 -12
  15. package/build/pkg/index.js.map +1 -1
  16. package/build/pkg/protocol.d.ts +4 -2
  17. package/build/pkg/protocol.js +60 -39
  18. package/build/pkg/protocol.js.map +1 -1
  19. package/build/test/display.js +400 -0
  20. package/build/test/display.js.map +1 -0
  21. package/build/tools/debug.js +133 -0
  22. package/build/tools/debug.js.map +1 -0
  23. package/build.zig +98 -79
  24. package/package.json +65 -36
  25. package/src/bin/install-pkmn-engine +11 -6
  26. package/src/bin/pkmn-debug +62 -0
  27. package/src/lib/binding/node.zig +47 -142
  28. package/src/lib/binding/wasm.zig +6 -4
  29. package/src/lib/common/battle.zig +17 -0
  30. package/src/lib/common/data.zig +15 -27
  31. package/src/lib/common/debug.zig +21 -11
  32. package/src/lib/common/js.zig +118 -0
  33. package/src/lib/common/optional.zig +69 -0
  34. package/src/lib/common/options.zig +12 -3
  35. package/src/lib/common/protocol.zig +752 -237
  36. package/src/lib/common/rational.zig +353 -0
  37. package/src/lib/common/rng.zig +15 -15
  38. package/src/lib/common/util.zig +12 -0
  39. package/src/lib/gen1/README.md +268 -154
  40. package/src/lib/gen1/calc.zig +426 -0
  41. package/src/lib/gen1/chance.zig +1058 -0
  42. package/src/lib/gen1/data/moves.zig +178 -180
  43. package/src/lib/gen1/data/species.zig +2 -2
  44. package/src/lib/gen1/data/types.zig +5 -5
  45. package/src/lib/gen1/data.zig +78 -64
  46. package/src/lib/gen1/helpers.zig +592 -32
  47. package/src/lib/gen1/mechanics.zig +1020 -658
  48. package/src/lib/gen2/README.md +106 -85
  49. package/src/lib/gen2/data/items.zig +12 -18
  50. package/src/lib/gen2/data/moves.zig +640 -563
  51. package/src/lib/gen2/data/species.zig +1 -1
  52. package/src/lib/gen2/data/types.zig +130 -28
  53. package/src/lib/gen2/data.zig +131 -87
  54. package/src/lib/gen2/helpers.zig +16 -22
  55. package/src/lib/gen2/mechanics.zig +1532 -11
  56. package/src/lib/pkmn.zig +68 -9
  57. package/src/test/display/pkmn.html.tmpl +368 -0
@@ -1,11 +1,14 @@
1
1
  const std = @import("std");
2
2
 
3
+ const pkmn = @import("../pkmn.zig");
4
+
3
5
  const common = @import("../common/data.zig");
4
6
  const DEBUG = @import("../common/debug.zig").print;
5
- const options = @import("../common/options.zig");
6
7
  const protocol = @import("../common/protocol.zig");
7
8
  const rng = @import("../common/rng.zig");
8
9
 
10
+ const Action = @import("chance.zig").Action;
11
+
9
12
  const data = @import("data.zig");
10
13
 
11
14
  const assert = std.debug.assert;
@@ -17,7 +20,7 @@ const ID = common.ID;
17
20
  const Player = common.Player;
18
21
  const Result = common.Result;
19
22
 
20
- const showdown = options.showdown;
23
+ const showdown = pkmn.options.showdown;
21
24
 
22
25
  const Boost = protocol.Boost;
23
26
  const Damage = protocol.Damage;
@@ -56,35 +59,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;
@@ -620,15 +674,16 @@ fn beforeMove(
620
674
  return .done;
621
675
  }
622
676
 
623
- _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
624
- if (foe.stored().hp > 0) try buildRage(battle, player.foe(), log);
677
+ const sub = showdown and foe.active.volatiles.Substitute;
678
+ _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
679
+ if (foe.stored().hp > 0 and !sub) try buildRage(battle, player.foe(), options);
625
680
 
626
681
  // For reasons passing understanding, Pokémon Showdown still inflicts residual damage to
627
682
  // Bide's user even if the above damage has caused the foe to faint. It's simpler to always
628
683
  // run residual here regardless of whether the foe fainted and opt-out of the default flow
629
684
  if (showdown) {
630
685
  residual.* = false;
631
- try handleResidual(battle, player, log);
686
+ try handleResidual(battle, player, options);
632
687
  }
633
688
 
634
689
  return .done;
@@ -637,20 +692,39 @@ fn beforeMove(
637
692
  if (volatiles.Thrashing) {
638
693
  assert(volatiles.attacks > 0);
639
694
  assert(from != null);
640
- volatiles.attacks -= 1;
641
- 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)) {
642
704
  try log.start(battle.active(player), .ConfusionSilent);
643
705
  }
644
706
  try log.move(ident, side.last_selected_move, battle.active(player.foe()), from);
645
- if (showdown and handleThrashing(battle, active)) {
646
- try log.start(battle.active(player), .ConfusionSilent);
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
+ }
647
714
  }
648
715
  return .skip_can;
649
716
  }
650
717
 
651
718
  if (volatiles.Binding) {
652
719
  assert(volatiles.attacks > 0);
653
- 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
+
654
728
  try log.move(
655
729
  battle.active(player),
656
730
  side.last_selected_move,
@@ -658,7 +732,11 @@ fn beforeMove(
658
732
  side.last_selected_move,
659
733
  );
660
734
  if (showdown or battle.last_damage != 0) {
661
- _ = 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
+ }
662
740
  }
663
741
  return .done;
664
742
  }
@@ -674,7 +752,7 @@ fn canMove(
674
752
  skip_pp: bool,
675
753
  from: ?Move,
676
754
  residual: *bool,
677
- log: anytype,
755
+ options: anytype,
678
756
  ) !bool {
679
757
  var side = battle.side(player);
680
758
  const player_ident = battle.active(player);
@@ -689,8 +767,9 @@ fn canMove(
689
767
  side.active.volatiles.Charging = false;
690
768
  side.active.volatiles.Invulnerable = false;
691
769
  } else if (move.effect == .Charge) {
692
- try log.move(player_ident, side.last_selected_move, .{}, from);
693
- 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);
694
773
  return false;
695
774
  }
696
775
 
@@ -705,23 +784,35 @@ fn canMove(
705
784
  // relevant for control flow, but we don't have anything useful to put in the log. This is
706
785
  // regrettable, but this information is more "nice to have" than required
707
786
  const f = if (from != null and from.? == .None) null else from;
708
- try log.move(player_ident, side.last_selected_move, battle.active(target), f);
787
+ try options.log.move(player_ident, side.last_selected_move, battle.active(target), f);
788
+ setCounterable(battle, player, side, move);
709
789
 
710
790
  if (move.effect.onBegin()) {
711
- try onBegin(battle, player, move, mslot, residual, log);
791
+ try onBegin(battle, player, move, mslot, residual, options);
712
792
  return false;
713
793
  }
714
794
 
715
795
  if (move.effect == .Thrashing) {
716
- Effects.thrashing(battle, player);
796
+ Effects.thrashing(battle, player, options);
717
797
  } else if (!showdown and move.effect == .Binding) {
718
798
  // Pokémon Showdown handles this after hit/miss checks and damage calculation
719
- Effects.binding(battle, player);
799
+ try Effects.binding(battle, player, false, options);
720
800
  }
721
801
 
722
802
  return true;
723
803
  }
724
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
+
725
816
  fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
726
817
  if (side.last_selected_move == .Struggle) return;
727
818
 
@@ -732,11 +823,11 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
732
823
  if (volatiles.Bide) return;
733
824
 
734
825
  assert(active.move(mslot).pp > 0 or auto);
735
- active.move(mslot).pp = @intCast(u6, active.move(mslot).pp) -% 1;
826
+ active.move(mslot).pp = @as(u6, @intCast(active.move(mslot).pp)) -% 1;
736
827
  if (volatiles.Transform) return;
737
828
 
738
829
  assert(side.stored().move(mslot).pp > 0 or auto);
739
- 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;
740
831
  assert(active.move(mslot).pp == side.stored().move(mslot).pp);
741
832
  }
742
833
 
@@ -744,12 +835,12 @@ fn incrementPP(side: *Side, mslot: u4) void {
744
835
  var active = &side.active;
745
836
  const volatiles = &active.volatiles;
746
837
 
747
- active.move(mslot).pp = @intCast(u6, active.move(mslot).pp) +% 1;
838
+ active.move(mslot).pp = @as(u6, @intCast(active.move(mslot).pp)) +% 1;
748
839
  // GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
749
840
  if (showdown and volatiles.Transform) return;
750
841
 
751
842
  assert(mslot > 0 and mslot <= 4);
752
- 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;
753
844
  }
754
845
 
755
846
  // Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
@@ -757,10 +848,12 @@ fn doMove(
757
848
  battle: anytype,
758
849
  player: Player,
759
850
  mslot: u4,
851
+ rewrap: bool,
760
852
  auto: bool,
761
853
  residual: *bool,
762
- log: anytype,
854
+ options: anytype,
763
855
  ) !?Result {
856
+ var log = options.log;
764
857
  var side = battle.side(player);
765
858
  const foe = battle.foe(player);
766
859
 
@@ -775,37 +868,44 @@ fn doMove(
775
868
  var hits: u4 = 1;
776
869
  var effectiveness = Effectiveness.neutral;
777
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
+
778
874
  // The cartridge handles set damage moves in applyDamage but we short circuit to simplify things
779
875
  if (move.effect == .SuperFang or move.effect == .SpecialDamage) {
780
- return specialDamage(battle, player, move, log);
876
+ return specialDamage(battle, player, move, options);
781
877
  }
782
878
 
783
879
  // Pokémon Showdown runs invulnerability / immunity checks before checking accuracy - simply
784
880
  // calling moveHit early covers most of that but we also need to check type immunity first
785
881
  var miss = showdown and miss: {
786
882
  immune = move.target != .Self and !status and !counter and
787
- (@enumToInt(move.type.effectiveness(foe.active.types.type1)) == 0 or
788
- @enumToInt(move.type.effectiveness(foe.active.types.type2)) == 0);
883
+ (@intFromEnum(move.type.effectiveness(foe.active.types.type1)) == 0 or
884
+ @intFromEnum(move.type.effectiveness(foe.active.types.type2)) == 0);
789
885
  if (immune and move.effect != .Binding) break :miss true;
790
- if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) break :miss true;
791
- break :miss move.target != .Self and !moveHit(battle, player, move, &immune, &mist);
886
+ if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) {
887
+ battle.last_damage = 0;
888
+ break :miss true;
889
+ }
890
+ break :miss (move.target != .Self and
891
+ !moveHit(battle, player, move, &immune, &mist, options));
792
892
  };
793
893
  assert(!immune or miss or (showdown and move.effect == .Binding));
794
894
 
795
- var late = showdown;
895
+ var late = showdown and move.effect != .Explode;
796
896
  const skip = status or immune;
797
897
  if ((!showdown or (!skip or counter)) and !miss) blk: {
798
898
  if (showdown and move.effect.isMulti()) {
799
- Effects.multiHit(battle, player, move);
899
+ try Effects.multiHit(battle, player, move, options);
800
900
  hits = side.active.volatiles.attacks;
801
901
  late = false;
802
902
  }
803
903
 
804
904
  // Cartridge rolls for crit even for moves that can't crit (Counter/Metronome/status/OHKO)
805
905
  const check = !showdown or (!counter and move.effect != .OHKO);
806
- if (check) crit = checkCriticalHit(battle, player, move);
906
+ if (check) crit = try checkCriticalHit(battle, player, move, options);
807
907
 
808
- if (counter) return counterDamage(battle, player, move, log);
908
+ if (counter) return counterDamage(battle, player, move, options);
809
909
 
810
910
  battle.last_damage = 0;
811
911
 
@@ -827,17 +927,15 @@ fn doMove(
827
927
  effectiveness = adjustDamage(battle, player);
828
928
  immune = effectiveness == 0;
829
929
  }
830
- randomizeDamage(battle);
930
+ try randomizeDamage(battle, player, options);
831
931
  }
832
932
  }
833
933
 
834
- // Due to control flow shenanigans we need to clear last_damage for Pokémon Showdown
835
- if (showdown and skip) battle.last_damage = 0;
836
-
934
+ var zero = battle.last_damage == 0;
837
935
  miss = if (showdown or skip)
838
936
  miss
839
937
  else
840
- (!moveHit(battle, player, move, &immune, &mist) or battle.last_damage == 0);
938
+ (!moveHit(battle, player, move, &immune, &mist, options) or zero);
841
939
 
842
940
  assert(showdown or miss or battle.last_damage > 0 or skip);
843
941
  assert((!showdown and miss) or !(ohko and immune));
@@ -845,11 +943,11 @@ fn doMove(
845
943
 
846
944
  if (!showdown or !miss) {
847
945
  if (move.effect == .MirrorMove) {
848
- return mirrorMove(battle, player, mslot, auto, residual, log);
946
+ return mirrorMove(battle, player, mslot, rewrap, auto, residual, options);
849
947
  } else if (move.effect == .Metronome) {
850
- return metronome(battle, player, mslot, auto, residual, log);
948
+ return metronome(battle, player, mslot, rewrap, auto, residual, options);
851
949
  } else if (move.effect.onEnd()) {
852
- try onEnd(battle, player, move, log);
950
+ try onEnd(battle, player, move, options);
853
951
  return null;
854
952
  }
855
953
  }
@@ -862,38 +960,46 @@ fn doMove(
862
960
  move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
863
961
  if (ohko) {
864
962
  try log.immune(foe_ident, .OHKO);
865
- } else if (immune and !invulnerable) {
963
+ } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
866
964
  try log.immune(foe_ident, .None);
867
965
  } else if (mist) {
868
966
  if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
869
967
  try log.fail(foe_ident, .None);
870
968
  } else {
969
+ if (showdown or !zero) try options.chance.commit(player, .miss);
871
970
  try log.lastmiss();
872
971
  try log.miss(battle.active(player));
873
972
  }
874
973
  if (move.effect == .JumpKick) {
875
974
  // Recoil is supposed to be damage/8 but damage will always be 0 here
876
- assert(battle.last_damage == 0);
975
+ assert(showdown or battle.last_damage == 0);
877
976
  battle.last_damage = 1;
878
- _ = try applyDamage(battle, player, player.foe(), .None, log);
977
+ _ = try applyDamage(battle, player, player.foe(), .None, options);
879
978
  if (showdown and side.stored().hp == 0) residual.* = false;
880
979
  } else if (move.effect == .Explode) {
881
980
  try Effects.explode(battle, player);
882
- try buildRage(battle, player.foe(), log);
981
+ try buildRage(battle, player.foe(), options);
883
982
  } else if (showdown and move.effect == .Disable) {
884
- 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);
885
986
  }
886
987
  return null;
988
+ } else {
989
+ try options.chance.commit(player, .hit);
887
990
  }
888
991
 
889
992
  // On the cartridge MultiHit doesn't get set up until after damage has been applied for the
890
993
  // first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
891
994
  // it up above before damage calculation).
892
995
  if (!showdown and move.effect.isMulti()) {
893
- Effects.multiHit(battle, player, move);
996
+ try Effects.multiHit(battle, player, move, options);
894
997
  hits = side.active.volatiles.attacks;
895
998
  }
896
999
 
1000
+ // Pokémon Showdown only builds Rage for Disable/Explosion (hit/miss) when attacking into a sub
1001
+ const sub = showdown and foe.active.volatiles.Substitute;
1002
+
897
1003
  var nullified = false;
898
1004
  var hit: u4 = 0;
899
1005
  while (hit < hits) {
@@ -905,11 +1011,13 @@ fn doMove(
905
1011
  try log.resisted(battle.active(player.foe()));
906
1012
  }
907
1013
  }
908
- 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);
909
1015
  if (hit == 0 and ohko) try log.ohko();
910
1016
  hit += 1;
911
1017
  if (foe.stored().hp == 0) break;
912
- if (!late) try buildRage(battle, player.foe(), log);
1018
+ if (!late and (!sub or move.effect == .Explode)) {
1019
+ try buildRage(battle, player.foe(), options);
1020
+ }
913
1021
  // If the substitute breaks during a multi-hit attack, the attack ends
914
1022
  if (nullified) break;
915
1023
  }
@@ -919,17 +1027,23 @@ fn doMove(
919
1027
  assert(nullified or foe.stored().hp == 0 or side.active.volatiles.attacks - hit == 0);
920
1028
  side.active.volatiles.attacks = 0;
921
1029
  if (showdown and move.effect == .Twineedle and !nullified and foe.stored().hp > 0) {
922
- try Effects.poison(battle, player, Move.get(.PoisonSting), log);
1030
+ try Effects.poison(battle, player, Move.get(.PoisonSting), options);
923
1031
  }
924
1032
  try log.hitcount(battle.active(player.foe()), hit);
925
1033
  } else if (showdown) {
926
1034
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
927
1035
  if (move.effect == .Binding) {
928
- Effects.binding(battle, player);
1036
+ try Effects.binding(battle, player, rewrap, options);
929
1037
  if (immune) {
930
1038
  battle.last_damage = 0;
1039
+ assert(foe.stored().hp > 0);
931
1040
  // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
932
- 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
+ }
933
1047
  return null;
934
1048
  }
935
1049
  }
@@ -942,12 +1056,12 @@ fn doMove(
942
1056
  // On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
943
1057
  // but this is only done to setup the MultiHit looping in the first place. Moving the MultiHit
944
1058
  // setup before the loop means we can avoid having to waste time doing no-op handler searches.
945
- if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, log);
1059
+ if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
946
1060
 
947
1061
  if (foe.stored().hp == 0) return null;
948
1062
 
949
- // Pokémon Showdown builds Rage at the wrong time for non-MultiHit moves
950
- if (late and move.effect != .Disable) try buildRage(battle, player.foe(), log);
1063
+ // Pokémon Showdown builds Rage at the wrong time for non-MultiHit move
1064
+ if (late and !sub and move.effect != .Disable) try buildRage(battle, player.foe(), options);
951
1065
 
952
1066
  if (!move.effect.isSpecial()) {
953
1067
  // On the cartridge Rage is not considered to be "special" and thus gets executed for a
@@ -958,30 +1072,29 @@ fn doMove(
958
1072
  // Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
959
1073
  // puts the |-status| message before |-hitcount| instead of after.
960
1074
  if (move.effect == .Twineedle) {
961
- if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), log);
1075
+ if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
962
1076
  } else if (move.effect == .Disable) {
963
- const result = try Effects.disable(battle, player, move, log);
964
- 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);
965
1079
  return result;
966
1080
  } else {
967
- try moveEffect(battle, player, move, log);
1081
+ try moveEffect(battle, player, move, options);
968
1082
  }
969
1083
  }
970
1084
 
971
1085
  return null;
972
1086
  }
973
1087
 
974
- fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
1088
+ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !bool {
975
1089
  const side = battle.side(player);
976
1090
 
977
1091
  // Base speed is used for the critical hit calculation, even when Transform-ed
978
- var chance = @as(u16, Species.chance(side.stored().species));
979
- // GLITCH: Focus Energy reduces critical hit chance instead of increasing it
980
- chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
981
- chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
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;
982
1096
 
983
- if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
984
- return std.math.rotl(u8, battle.rng.next(), 3) < chance;
1097
+ return Rolls.criticalHit(battle, player, @intCast(rate), options);
985
1098
  }
986
1099
 
987
1100
  fn calcDamage(
@@ -1047,7 +1160,7 @@ fn calcDamage(
1047
1160
  d = @min(997, d);
1048
1161
  d += 2;
1049
1162
 
1050
- battle.last_damage = @intCast(u16, d);
1163
+ battle.last_damage = @intCast(d);
1051
1164
 
1052
1165
  return true;
1053
1166
  }
@@ -1061,9 +1174,9 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
1061
1174
  var d = battle.last_damage;
1062
1175
  if (side.active.types.includes(move.type)) d +%= d / 2;
1063
1176
 
1064
- const neutral = @enumToInt(Effectiveness.Neutral);
1065
- const eff1: u16 = @enumToInt(move.type.effectiveness(types.type1));
1066
- const eff2: u16 = @enumToInt(move.type.effectiveness(types.type2));
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));
1067
1180
 
1068
1181
  // Type effectiveness matchup precedence only matters with (NVE, SE)
1069
1182
  if (!showdown and (eff1 + eff2) == Effectiveness.mismatch and
@@ -1083,101 +1196,87 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
1083
1196
  return if (types.type1 == types.type2) eff1 * neutral else eff1 * eff2;
1084
1197
  }
1085
1198
 
1086
- fn randomizeDamage(battle: anytype) void {
1199
+ fn randomizeDamage(battle: anytype, player: Player, options: anytype) !void {
1087
1200
  if (battle.last_damage <= 1) return;
1088
-
1089
- const random = if (showdown)
1090
- battle.rng.range(u8, 217, 256)
1091
- else loop: {
1092
- while (true) {
1093
- const r = std.math.rotr(u8, battle.rng.next(), 1);
1094
- if (r >= 217) break :loop r;
1095
- }
1096
- };
1097
-
1098
- battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
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);
1099
1205
  }
1100
1206
 
1101
- 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 {
1102
1208
  const side = battle.side(player);
1209
+ const foe = battle.foe(player);
1103
1210
 
1104
- 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);
1105
1213
 
1106
1214
  battle.last_damage = switch (side.last_selected_move) {
1107
- .SuperFang => @max(battle.foe(player).stored().hp / 2, 1),
1215
+ .SuperFang => @max(foe.stored().hp / 2, 1),
1108
1216
  .SeismicToss, .NightShade => side.stored().level,
1109
1217
  .SonicBoom => 20,
1110
1218
  .DragonRage => 40,
1111
1219
  // GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
1112
1220
  .Psywave => power: {
1113
- const max = @intCast(u8, @as(u16, side.stored().level) * 3 / 2);
1114
- if (showdown) {
1115
- break :power battle.rng.range(u8, 0, max);
1116
- } else {
1117
- // GLITCH: Psywave infinite glitch loop
1118
- if (max <= 1) return Result.Error;
1119
- while (true) {
1120
- const r = battle.rng.next();
1121
- if (r < max) break :power r;
1122
- }
1123
- }
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);
1124
1225
  },
1125
1226
  else => unreachable,
1126
1227
  };
1127
1228
 
1128
1229
  if (battle.last_damage == 0) return if (showdown) null else Result.Error;
1129
1230
 
1130
- _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
1131
- if (battle.foe(player).stored().hp > 0) try buildRage(battle, player.foe(), log);
1231
+ const sub = showdown and foe.active.volatiles.Substitute;
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);
1132
1234
 
1133
1235
  return null;
1134
1236
  }
1135
1237
 
1136
- 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 {
1137
1239
  const foe = battle.foe(player);
1138
1240
 
1139
1241
  if (battle.last_damage == 0) {
1140
- try log.fail(battle.active(player), .None);
1242
+ try options.log.fail(battle.active(player), .None);
1141
1243
  return null;
1142
1244
  }
1143
1245
 
1144
- // Pretend Counter was used as a stand-in when no move has been used to fail below with 0 BP
1145
- const foe_last_used_move =
1146
- Move.get(if (foe.last_used_move == .None) .Counter else foe.last_used_move);
1246
+ // Pretend Splash was used as a stand-in when no move has been used to fail below with 0 BP
1147
1247
  const foe_last_selected_move =
1148
1248
  Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
1149
- .Counter
1249
+ .Splash
1150
1250
  else
1151
1251
  foe.last_selected_move);
1152
1252
 
1153
- const used = foe_last_used_move.bp > 0 and
1154
- foe.last_used_move != .Counter and
1155
- (foe_last_used_move.type == .Normal or
1156
- foe_last_used_move.type == .Fighting);
1157
-
1253
+ const used = battle.lastMove(player.foe()).counterable != 0;
1158
1254
  const selected = foe_last_selected_move.bp > 0 and
1159
1255
  foe.last_selected_move != .Counter and
1160
1256
  (foe_last_selected_move.type == .Normal or
1161
1257
  foe_last_selected_move.type == .Fighting);
1162
1258
 
1163
1259
  if (!used and !selected) {
1164
- try log.fail(battle.active(player), .None);
1260
+ try options.log.fail(battle.active(player), .None);
1165
1261
  return null;
1166
1262
  }
1167
1263
 
1168
1264
  if (!used or !selected) {
1169
1265
  // GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
1170
1266
  if (!showdown) return Result.Error;
1171
- try log.fail(battle.active(player), .None);
1267
+ try options.log.fail(battle.active(player), .None);
1172
1268
  return null;
1173
1269
  }
1174
1270
 
1175
- battle.last_damage = if (battle.last_damage > 0x7FFF) 0xFFFF else battle.last_damage * 2;
1271
+ battle.last_damage *|= 2;
1176
1272
 
1177
1273
  // Pokémon Showdown calls checkHit before Counter
1178
- 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);
1179
1276
 
1180
- _ = 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);
1181
1280
  return null;
1182
1281
  }
1183
1282
 
@@ -1186,7 +1285,7 @@ fn applyDamage(
1186
1285
  target_player: Player,
1187
1286
  sub_player: Player,
1188
1287
  reason: Damage,
1189
- log: anytype,
1288
+ options: anytype,
1190
1289
  ) !bool {
1191
1290
  assert(showdown or battle.last_damage != 0);
1192
1291
 
@@ -1201,19 +1300,20 @@ fn applyDamage(
1201
1300
  subbed.active.volatiles.substitute = 0;
1202
1301
  subbed.active.volatiles.Substitute = false;
1203
1302
  // battle.last_damage is not updated with the amount of HP the Substitute had
1204
- try log.end(battle.active(sub_player), .Substitute);
1303
+ try options.log.end(battle.active(sub_player), .Substitute);
1304
+ options.calc.capped(target_player);
1205
1305
  return true;
1206
1306
  } else {
1207
1307
  // Safe to truncate since less than subbed.volatiles.substitute which is a u8
1208
- subbed.active.volatiles.substitute -= @intCast(u8, battle.last_damage);
1209
- try log.activate(battle.active(sub_player), .Substitute);
1308
+ subbed.active.volatiles.substitute -= @intCast(battle.last_damage);
1309
+ try options.log.activate(battle.active(sub_player), .Substitute);
1210
1310
  return false;
1211
1311
  }
1212
1312
  }
1213
1313
 
1214
1314
  if (battle.last_damage > target.stored().hp) battle.last_damage = target.stored().hp;
1215
1315
  target.stored().hp -= battle.last_damage;
1216
- try log.damage(battle.active(target_player), target.stored(), reason);
1316
+ try options.log.damage(battle.active(target_player), target.stored(), reason);
1217
1317
  return false;
1218
1318
  }
1219
1319
 
@@ -1221,73 +1321,78 @@ fn mirrorMove(
1221
1321
  battle: anytype,
1222
1322
  player: Player,
1223
1323
  mslot: u4,
1324
+ rewrap: bool,
1224
1325
  auto: bool,
1225
1326
  residual: *bool,
1226
- log: anytype,
1227
- ) !?Result {
1327
+ options: anytype,
1328
+ ) anyerror!?Result {
1228
1329
  var side = battle.side(player);
1229
1330
  const foe = battle.foe(player);
1230
1331
 
1231
1332
  side.last_selected_move = foe.last_used_move;
1232
1333
 
1233
1334
  if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
1234
- try log.fail(battle.active(player), .None);
1335
+ try options.log.fail(battle.active(player), .None);
1235
1336
  return null;
1337
+ } else if (!showdown or foe.last_used_move != .Struggle) {
1338
+ incrementPP(side, mslot);
1236
1339
  }
1237
1340
 
1238
- incrementPP(side, mslot);
1239
-
1240
- if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
1241
- return doMove(battle, player, mslot, auto, residual, log);
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);
1242
1345
  }
1243
1346
 
1244
1347
  fn metronome(
1245
1348
  battle: anytype,
1246
1349
  player: Player,
1247
1350
  mslot: u4,
1351
+ rewrap: bool,
1248
1352
  auto: bool,
1249
1353
  residual: *bool,
1250
- log: anytype,
1251
- ) !?Result {
1354
+ options: anytype,
1355
+ ) anyerror!?Result {
1252
1356
  var side = battle.side(player);
1253
1357
 
1254
- side.last_selected_move = if (showdown) blk: {
1255
- const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
1256
- const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
1257
- break :blk @intToEnum(Move, r + mod);
1258
- } else loop: {
1259
- while (true) {
1260
- const r = battle.rng.next();
1261
- if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
1262
- if (r >= @enumToInt(Move.Struggle)) continue;
1263
- break :loop @intToEnum(Move, r);
1264
- }
1265
- };
1266
-
1358
+ side.last_selected_move = try Rolls.metronome(battle, player, options);
1267
1359
  incrementPP(side, mslot);
1268
1360
 
1269
- if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
1270
- 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);
1271
1365
  }
1272
1366
 
1273
- 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 {
1274
1368
  var immune = false;
1275
1369
  var mist = false;
1276
- if (moveHit(battle, player, move, &immune, &mist)) return true;
1370
+
1371
+ if (moveHit(battle, player, move, &immune, &mist, options)) return true;
1372
+
1277
1373
  assert(!immune);
1278
1374
  if (mist) {
1279
1375
  assert(!showdown);
1280
1376
  const foe_ident = battle.active(player.foe());
1281
- try log.activate(foe_ident, .Mist);
1282
- try log.fail(foe_ident, .None);
1377
+ try options.log.activate(foe_ident, .Mist);
1378
+ try options.log.fail(foe_ident, .None);
1283
1379
  } else {
1284
- try log.lastmiss();
1285
- 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));
1286
1383
  }
1384
+
1287
1385
  return false;
1288
1386
  }
1289
1387
 
1290
- 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 {
1291
1396
  var side = battle.side(player);
1292
1397
  const foe = battle.foe(player);
1293
1398
 
@@ -1321,23 +1426,24 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
1321
1426
  }
1322
1427
 
1323
1428
  // GLITCH: Thrash / Petal Dance / Rage get their accuracy overwritten on subsequent hits
1324
- var state = &side.active.volatiles.state;
1325
- const overwritten = (move.effect == .Thrashing or move.effect == .Rage) and state.* > 0;
1326
- assert(!overwritten or (0 < state.* and state.* <= 255 and !side.active.volatiles.Bide));
1327
- var accuracy = if (overwritten) state.* else @as(u16, Gen12.percent(move.accuracy));
1328
- var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
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))];
1329
1436
  accuracy = accuracy * boost[0] / boost[1];
1330
- boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
1437
+ boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
1331
1438
  accuracy = accuracy * boost[0] / boost[1];
1332
1439
  accuracy = @min(255, @max(1, accuracy));
1333
1440
 
1334
- 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;
1335
1444
 
1336
1445
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1337
- break :miss if (showdown)
1338
- !battle.rng.chance(u8, @intCast(u8, accuracy), 256)
1339
- else
1340
- battle.rng.next() >= accuracy;
1446
+ break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
1341
1447
  };
1342
1448
 
1343
1449
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1347,7 +1453,7 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
1347
1453
  }
1348
1454
 
1349
1455
  if (!miss) return true;
1350
- battle.last_damage = 0;
1456
+ if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
1351
1457
  side.active.volatiles.Binding = false;
1352
1458
  return false;
1353
1459
  }
@@ -1355,8 +1461,8 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
1355
1461
  fn checkFaint(
1356
1462
  battle: anytype,
1357
1463
  player: Player,
1358
- log: anytype,
1359
- ) @TypeOf(log).Error!?Result {
1464
+ options: anytype,
1465
+ ) @TypeOf(options.log).Error!?Result {
1360
1466
  const side = battle.side(player);
1361
1467
  if (side.stored().hp > 0) return null;
1362
1468
 
@@ -1368,29 +1474,29 @@ fn checkFaint(
1368
1474
  const tie = player_out and foe_out;
1369
1475
  const more = tie or player_out or foe_out;
1370
1476
 
1371
- if (try faint(battle, player, log, !(more or foe_fainted))) |r| return r;
1372
- if (foe_fainted) if (try faint(battle, player.foe(), log, !more)) |r| return r;
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;
1373
1479
 
1374
1480
  assert(!side.active.volatiles.MultiHit);
1375
1481
  assert(!foe.active.volatiles.MultiHit);
1376
1482
 
1377
1483
  if (tie) {
1378
- try log.tie();
1484
+ try options.log.tie();
1379
1485
  return Result.Tie;
1380
1486
  } else if (player_out) {
1381
- try log.win(player.foe());
1487
+ try options.log.win(player.foe());
1382
1488
  return if (player == .P1) Result.Lose else Result.Win;
1383
1489
  } else if (foe_out) {
1384
- try log.win(player);
1490
+ try options.log.win(player);
1385
1491
  return if (player == .P1) Result.Win else Result.Lose;
1386
1492
  }
1387
1493
 
1388
1494
  const foe_choice: Choice.Type = if (foe_fainted) .Switch else .Pass;
1389
- if (player == .P1) return Result{ .p1 = .Switch, .p2 = foe_choice };
1390
- return Result{ .p1 = foe_choice, .p2 = .Switch };
1495
+ if (player == .P1) return .{ .p1 = .Switch, .p2 = foe_choice };
1496
+ return .{ .p1 = foe_choice, .p2 = .Switch };
1391
1497
  }
1392
1498
 
1393
- fn faint(battle: anytype, player: Player, log: anytype, done: bool) !?Result {
1499
+ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
1394
1500
  var side = battle.side(player);
1395
1501
  var foe = battle.foe(player);
1396
1502
  assert(side.stored().hp == 0);
@@ -1423,32 +1529,31 @@ fn faint(battle: anytype, player: Player, log: anytype, done: bool) !?Result {
1423
1529
  side.stored().stats.spe;
1424
1530
  }
1425
1531
 
1426
- try log.faint(battle.active(player), done);
1532
+ try options.log.faint(battle.active(player), done);
1533
+ options.calc.capped(player);
1427
1534
  return null;
1428
1535
  }
1429
1536
 
1430
- fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1537
+ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1431
1538
  var side = battle.side(player);
1432
1539
  var stored = side.stored();
1433
1540
  const ident = battle.active(player);
1434
1541
  var volatiles = &side.active.volatiles;
1435
1542
 
1436
1543
  const brn = Status.is(stored.status, .BRN);
1437
- if (brn or Status.is(stored.status, .PSN)) blk: {
1544
+ if (brn or Status.is(stored.status, .PSN)) {
1438
1545
  var damage = @max(stored.stats.hp / 16, 1);
1439
-
1440
1546
  if (volatiles.Toxic) {
1441
1547
  volatiles.toxic += 1;
1442
1548
  damage *= volatiles.toxic;
1443
1549
  }
1550
+ damage = @min(damage, stored.hp);
1444
1551
 
1445
- const amount = @min(damage, stored.hp);
1446
- if (showdown and amount == 0) break :blk;
1447
-
1448
- stored.hp -= amount;
1449
-
1450
- // Pokémon Showdown uses damageOf here but its not relevant in Generation I
1451
- try log.damage(ident, stored, if (brn) Damage.Burn else Damage.Poison);
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
+ }
1452
1557
  }
1453
1558
 
1454
1559
  if (volatiles.LeechSeed) {
@@ -1469,49 +1574,46 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1469
1574
  damage *= volatiles.toxic;
1470
1575
  }
1471
1576
 
1472
- // Pokémon Showdown erroneously updates last damage with uncapped Leech Seed damage
1473
- if (showdown) battle.last_damage = damage;
1474
-
1475
1577
  const amount = @min(damage, stored.hp);
1476
1578
  stored.hp -= amount;
1477
1579
 
1478
1580
  // As above, Pokémon Showdown uses damageOf but its not relevant
1479
- if (amount > 0) try log.damage(ident, stored, .LeechSeed);
1581
+ if (amount > 0) try options.log.damage(ident, stored, .LeechSeed);
1480
1582
 
1481
1583
  const before = foe_stored.hp;
1482
1584
  // Uncapped damage is added back to the foe
1483
1585
  foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
1484
1586
  // Pokémon Showdown uses the less specific heal here instead of drain... because reasons?
1485
- if (foe_stored.hp > before) try log.heal(foe_ident, foe_stored, .Silent);
1587
+ if (foe_stored.hp > before) try options.log.heal(foe_ident, foe_stored, .Silent);
1486
1588
  }
1487
1589
  }
1488
1590
 
1489
- fn endTurn(battle: anytype, log: anytype) @TypeOf(log).Error!Result {
1591
+ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result {
1490
1592
  assert(!battle.side(.P1).active.volatiles.MultiHit);
1491
1593
  assert(!battle.side(.P2).active.volatiles.MultiHit);
1492
1594
 
1493
- if (showdown and options.ebc and checkEBC(battle)) {
1494
- try log.tie();
1595
+ battle.turn += 1;
1596
+
1597
+ if (showdown and pkmn.options.ebc and checkEBC(battle)) {
1598
+ try options.log.tie();
1495
1599
  return Result.Tie;
1496
1600
  }
1497
1601
 
1498
- battle.turn += 1;
1499
-
1500
1602
  if (showdown and battle.turn >= 1000) {
1501
- try log.tie();
1603
+ try options.log.tie();
1502
1604
  return Result.Tie;
1503
1605
  } else if (battle.turn >= 65535) {
1504
1606
  return Result.Error;
1505
1607
  }
1506
1608
 
1507
- try log.turn(battle.turn);
1609
+ try options.log.turn(battle.turn);
1508
1610
 
1509
1611
  return Result.Default;
1510
1612
  }
1511
1613
 
1512
1614
  fn checkEBC(battle: anytype) bool {
1513
1615
  for (battle.sides, 0..) |side, i| {
1514
- const foe = battle.sides[~@intCast(u1, i)];
1616
+ const foe = battle.sides[~@as(u1, @intCast(i))];
1515
1617
 
1516
1618
  var foe_all_ghosts = true;
1517
1619
  var foe_all_transform = true;
@@ -1563,48 +1665,84 @@ fn checkEBC(battle: anytype) bool {
1563
1665
  return false;
1564
1666
  }
1565
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
+
1566
1704
  inline fn onBegin(
1567
1705
  battle: anytype,
1568
1706
  player: Player,
1569
1707
  move: Move.Data,
1570
1708
  mslot: u8,
1571
1709
  residual: *bool,
1572
- log: anytype,
1710
+ options: anytype,
1573
1711
  ) !void {
1574
1712
  assert(move.effect.onBegin());
1575
1713
  return switch (move.effect) {
1576
- .Confusion => Effects.confusion(battle, player, move, log),
1577
- .Conversion => Effects.conversion(battle, player, log),
1578
- .FocusEnergy => Effects.focusEnergy(battle, player, log),
1579
- .Haze => Effects.haze(battle, player, log),
1580
- .Heal => Effects.heal(battle, player, log),
1581
- .LeechSeed => Effects.leechSeed(battle, player, move, log),
1582
- .LightScreen => Effects.lightScreen(battle, player, log),
1583
- .Mimic => Effects.mimic(battle, player, move, mslot, log),
1584
- .Mist => Effects.mist(battle, player, log),
1585
- .Paralyze => Effects.paralyze(battle, player, move, log),
1586
- .Poison => Effects.poison(battle, player, move, log),
1587
- .Reflect => Effects.reflect(battle, player, log),
1588
- .Splash => Effects.splash(battle, player, log),
1589
- .Substitute => Effects.substitute(battle, player, residual, log),
1590
- .SwitchAndTeleport => Effects.switchAndTeleport(battle, player, move, log),
1591
- .Transform => Effects.transform(battle, player, log),
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),
1592
1730
  else => unreachable,
1593
1731
  };
1594
1732
  }
1595
1733
 
1596
- 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 {
1597
1735
  assert(move.effect.onEnd());
1598
1736
  return switch (move.effect) {
1599
- .Bide => Effects.bide(battle, player, log),
1737
+ .Bide => Effects.bide(battle, player, options),
1600
1738
  // zig fmt: off
1601
1739
  .AttackUp1, .AttackUp2, .DefenseUp1, .DefenseUp2,
1602
1740
  .EvasionUp1, .SpecialUp1, .SpecialUp2, .SpeedUp2 =>
1603
- Effects.boost(battle, player, move, log),
1741
+ Effects.boost(battle, player, move, options),
1604
1742
  .AccuracyDown1, .AttackDown1, .DefenseDown1, .DefenseDown2, .SpeedDown1 =>
1605
- Effects.unboost(battle, player, move, log),
1743
+ Effects.unboost(battle, player, move, options),
1606
1744
  // zig fmt: on
1607
- .Sleep => Effects.sleep(battle, player, move, log),
1745
+ .Sleep => Effects.sleep(battle, player, move, options),
1608
1746
  else => unreachable,
1609
1747
  };
1610
1748
  }
@@ -1614,54 +1752,51 @@ inline fn alwaysHappens(
1614
1752
  player: Player,
1615
1753
  move: Move.Data,
1616
1754
  residual: *bool,
1617
- log: anytype,
1755
+ options: anytype,
1618
1756
  ) !void {
1619
1757
  assert(move.effect.alwaysHappens());
1620
1758
  return switch (move.effect) {
1621
- .DrainHP, .DreamEater => Effects.drainHP(battle, player, log),
1759
+ .DrainHP, .DreamEater => Effects.drainHP(battle, player, options),
1622
1760
  .Explode => Effects.explode(battle, player),
1623
- .PayDay => Effects.payDay(battle, player, log),
1761
+ .PayDay => Effects.payDay(battle, player, options),
1624
1762
  .Rage => Effects.rage(battle, player),
1625
- .Recoil => Effects.recoil(battle, player, residual, log),
1763
+ .Recoil => Effects.recoil(battle, player, residual, options),
1626
1764
  .JumpKick, .Binding => {},
1627
1765
  else => unreachable,
1628
1766
  };
1629
1767
  }
1630
1768
 
1631
- 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 {
1632
1770
  return switch (move.effect) {
1633
- .BurnChance1, .BurnChance2 => Effects.burnChance(battle, player, move, log),
1634
- .ConfusionChance => Effects.confusion(battle, player, move, log),
1635
- .FlinchChance1, .FlinchChance2 => Effects.flinchChance(battle, player, move),
1636
- .FreezeChance => Effects.freezeChance(battle, player, move, log),
1637
- .HyperBeam => Effects.hyperBeam(battle, player, log),
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),
1638
1776
  .MultiHit, .DoubleHit, .Twineedle => unreachable,
1639
- .ParalyzeChance1, .ParalyzeChance2 => Effects.paralyzeChance(battle, player, move, log),
1640
- .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),
1641
1779
  // zig fmt: off
1642
1780
  .AttackDownChance, .DefenseDownChance, .SpecialDownChance, .SpeedDownChance =>
1643
- Effects.unboost(battle, player, move, log),
1781
+ Effects.unboost(battle, player, move, options),
1644
1782
  // zig fmt: on
1645
1783
  else => {},
1646
1784
  };
1647
1785
  }
1648
1786
 
1649
1787
  pub const Effects = struct {
1650
- fn bide(battle: anytype, player: Player, log: anytype) !void {
1788
+ fn bide(battle: anytype, player: Player, options: anytype) !void {
1651
1789
  var side = battle.side(player);
1652
1790
 
1653
1791
  side.active.volatiles.Bide = true;
1654
1792
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1655
1793
  side.active.volatiles.state = 0;
1656
- side.active.volatiles.attacks = @intCast(u3, if (showdown)
1657
- battle.rng.range(u4, 2, 4)
1658
- else
1659
- (battle.rng.next() & 1) + 2);
1794
+ side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
1660
1795
 
1661
- try log.start(battle.active(player), .Bide);
1796
+ try options.log.start(battle.active(player), .Bide);
1662
1797
  }
1663
1798
 
1664
- 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 {
1665
1800
  var foe = battle.foe(player);
1666
1801
  var foe_stored = foe.stored();
1667
1802
 
@@ -1672,96 +1807,97 @@ pub const Effects = struct {
1672
1807
  // GLITCH: Freeze top move selection desync can occur if thawed Pokémon is slower
1673
1808
  if (Status.is(foe_stored.status, .FRZ)) {
1674
1809
  assert(move.type == .Fire);
1675
- 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
+ );
1676
1815
  foe_stored.status = 0;
1677
1816
  }
1678
1817
  return;
1679
1818
  }
1680
1819
 
1681
- const chance = !foe.active.types.includes(move.type) and if (showdown)
1682
- battle.rng.chance(u8, @as(u8, if (move.effect == .BurnChance1) 26 else 77), 256)
1683
- else
1684
- battle.rng.next() < 1 + (if (move.effect == .BurnChance1)
1685
- Gen12.percent(10)
1686
- else
1687
- Gen12.percent(30));
1688
- if (!chance) return;
1820
+ if (foe.active.types.includes(move.type)) return;
1821
+ if (!try Rolls.secondaryChance(battle, player, move.effect == .BurnChance1, options)) {
1822
+ return;
1823
+ }
1689
1824
 
1690
1825
  foe_stored.status = Status.init(.BRN);
1691
1826
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
1692
1827
 
1693
- try log.status(battle.active(player.foe()), foe_stored.status, .None);
1828
+ try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
1694
1829
  }
1695
1830
 
1696
- fn charge(battle: anytype, player: Player, log: anytype) !void {
1831
+ fn charge(battle: anytype, player: Player, options: anytype) !void {
1697
1832
  var side = battle.side(player);
1698
1833
  var volatiles = &side.active.volatiles;
1699
1834
 
1700
1835
  volatiles.Charging = true;
1701
1836
  const move = side.last_selected_move;
1702
1837
  if (move == .Fly or move == .Dig) volatiles.Invulnerable = true;
1703
- try log.laststill();
1704
- try log.prepare(battle.active(player), move);
1838
+ try options.log.laststill();
1839
+ try options.log.prepare(battle.active(player), move);
1705
1840
  }
1706
1841
 
1707
- 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 {
1708
1843
  var foe = battle.foe(player);
1709
1844
  const sub = foe.active.volatiles.Substitute;
1710
1845
 
1711
1846
  if (move.effect == .ConfusionChance) {
1712
- const chance = if (showdown)
1713
- battle.rng.chance(u8, 25, 256)
1714
- else
1715
- battle.rng.next() < Gen12.percent(10);
1716
- if (!chance) return;
1847
+ if (!try Rolls.confusionChance(battle, player, options)) return;
1717
1848
  } else {
1718
1849
  if (showdown) {
1719
- if (!try checkHit(battle, player, move, log)) {
1850
+ if (!try checkHit(battle, player, move, options)) {
1720
1851
  return;
1721
1852
  } else if (sub) {
1722
- return log.fail(battle.active(player.foe()), .None);
1853
+ return options.log.fail(battle.active(player.foe()), .None);
1723
1854
  }
1724
1855
  } else {
1725
1856
  if (sub) {
1726
- return log.fail(battle.active(player.foe()), .None);
1727
- } 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)) {
1728
1859
  return;
1729
1860
  }
1730
1861
  }
1862
+ try options.chance.commit(player, .hit);
1731
1863
  }
1732
1864
 
1733
1865
  if (foe.active.volatiles.Confusion) return;
1734
1866
  foe.active.volatiles.Confusion = true;
1735
- foe.active.volatiles.confusion = @intCast(u3, if (showdown)
1736
- battle.rng.range(u8, 2, 6)
1737
- else
1738
- (battle.rng.next() & 3) + 2);
1867
+ foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
1739
1868
 
1740
- try log.start(battle.active(player.foe()), .Confusion);
1869
+ try options.log.start(battle.active(player.foe()), .Confusion);
1741
1870
  }
1742
1871
 
1743
- fn conversion(battle: anytype, player: Player, log: anytype) !void {
1872
+ fn conversion(battle: anytype, player: Player, options: anytype) !void {
1744
1873
  const foe = battle.foe(player);
1745
1874
 
1746
1875
  if (foe.active.volatiles.Invulnerable) {
1747
- try log.lastmiss();
1748
- return log.miss(battle.active(player));
1876
+ try options.log.lastmiss();
1877
+ return options.log.miss(battle.active(player));
1749
1878
  }
1750
1879
 
1751
1880
  battle.side(player).active.types = foe.active.types;
1752
- 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
+ );
1753
1886
  }
1754
1887
 
1755
- 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 {
1756
1889
  var foe = battle.foe(player);
1757
1890
  var volatiles = &foe.active.volatiles;
1758
1891
  const foe_ident = battle.active(player.foe());
1759
1892
 
1760
1893
  // Pokémon Showdown handles hit/miss earlier in doMove
1761
- 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
+ }
1762
1898
 
1763
1899
  if (volatiles.disabled_move != 0) {
1764
- try log.fail(foe_ident, .None);
1900
+ try options.log.fail(foe_ident, .None);
1765
1901
  return null;
1766
1902
  }
1767
1903
 
@@ -1777,7 +1913,7 @@ pub const Effects = struct {
1777
1913
  // Technically this is still considered simply a "miss" on the cartridge,
1778
1914
  // but diverging from Pokémon Showdown here would mostly just be pedantic
1779
1915
  if (n == 0) {
1780
- try log.fail(foe_ident, .None);
1916
+ try options.log.fail(foe_ident, .None);
1781
1917
  return null;
1782
1918
  } else if (err) {
1783
1919
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
@@ -1785,23 +1921,22 @@ pub const Effects = struct {
1785
1921
  return Result.Error;
1786
1922
  }
1787
1923
 
1788
- volatiles.disabled_move = @intCast(u3, randomMoveSlot(&battle.rng, &foe.active.moves, n));
1789
- volatiles.disabled_duration = @intCast(u4, if (showdown)
1790
- battle.rng.range(u8, 1, 9)
1791
- else
1792
- (battle.rng.next() & 7) + 1);
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);
1793
1927
 
1794
- 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);
1795
1930
  return null;
1796
1931
  }
1797
1932
 
1798
- fn drainHP(battle: anytype, player: Player, log: anytype) !void {
1933
+ fn drainHP(battle: anytype, player: Player, options: anytype) !void {
1799
1934
  var side = battle.side(player);
1800
1935
  var stored = side.stored();
1801
1936
 
1802
1937
  if (battle.last_damage == 0) {
1803
1938
  assert(showdown);
1804
- return;
1939
+ if (!battle.foe(player).active.volatiles.Substitute) return;
1805
1940
  }
1806
1941
 
1807
1942
  const drain = @max(battle.last_damage / 2, 1);
@@ -1810,7 +1945,7 @@ pub const Effects = struct {
1810
1945
  if (stored.hp == stored.stats.hp) return;
1811
1946
  stored.hp = @min(stored.stats.hp, stored.hp + drain);
1812
1947
 
1813
- try log.drain(battle.active(player), stored, battle.active(player.foe()));
1948
+ try options.log.drain(battle.active(player), stored, battle.active(player.foe()));
1814
1949
  }
1815
1950
 
1816
1951
  fn explode(battle: anytype, player: Player) !void {
@@ -1824,34 +1959,28 @@ pub const Effects = struct {
1824
1959
  side.active.volatiles.LeechSeed = false;
1825
1960
  }
1826
1961
 
1827
- fn flinchChance(battle: anytype, player: Player, move: Move.Data) void {
1962
+ fn flinchChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1828
1963
  var volatiles = &battle.foe(player).active.volatiles;
1829
1964
 
1830
1965
  if (volatiles.Substitute) return;
1831
-
1832
- const chance = if (showdown)
1833
- battle.rng.chance(u8, @as(u8, if (move.effect == .FlinchChance1) 26 else 77), 256)
1834
- else
1835
- battle.rng.next() < 1 + (if (move.effect == .FlinchChance1)
1836
- Gen12.percent(10)
1837
- else
1838
- Gen12.percent(30));
1839
- if (!chance) return;
1966
+ if (!try Rolls.secondaryChance(battle, player, move.effect == .FlinchChance1, options)) {
1967
+ return;
1968
+ }
1840
1969
 
1841
1970
  volatiles.Flinch = true;
1842
1971
  volatiles.Recharging = false;
1843
1972
  }
1844
1973
 
1845
- fn focusEnergy(battle: anytype, player: Player, log: anytype) !void {
1974
+ fn focusEnergy(battle: anytype, player: Player, options: anytype) !void {
1846
1975
  var side = battle.side(player);
1847
1976
 
1848
1977
  if (side.active.volatiles.FocusEnergy) return;
1849
1978
  side.active.volatiles.FocusEnergy = true;
1850
1979
 
1851
- try log.start(battle.active(player), .FocusEnergy);
1980
+ try options.log.start(battle.active(player), .FocusEnergy);
1852
1981
  }
1853
1982
 
1854
- 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 {
1855
1984
  var foe = battle.foe(player);
1856
1985
  var foe_stored = foe.stored();
1857
1986
  const foe_ident = battle.active(player.foe());
@@ -1862,22 +1991,19 @@ pub const Effects = struct {
1862
1991
  return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
1863
1992
  }
1864
1993
 
1865
- const chance = !foe.active.types.includes(move.type) and if (showdown)
1866
- battle.rng.chance(u8, 26, 256)
1867
- else
1868
- battle.rng.next() < 1 + Gen12.percent(10);
1869
- if (!chance) return;
1870
-
1994
+ if (foe.active.types.includes(move.type)) return;
1995
+ if (!try Rolls.secondaryChance(battle, player, true, options)) return;
1871
1996
  // Freeze Clause Mod
1872
1997
  if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1873
1998
 
1874
1999
  foe_stored.status = Status.init(.FRZ);
1875
2000
  // GLITCH: Hyper Beam recharging status is not cleared
1876
2001
 
1877
- try log.status(foe_ident, foe_stored.status, .None);
2002
+ try options.log.status(foe_ident, foe_stored.status, .None);
1878
2003
  }
1879
2004
 
1880
- fn haze(battle: anytype, player: Player, log: anytype) !void {
2005
+ fn haze(battle: anytype, player: Player, options: anytype) !void {
2006
+ var log = options.log;
1881
2007
  var side = battle.side(player);
1882
2008
  var foe = battle.foe(player);
1883
2009
 
@@ -1899,7 +2025,7 @@ pub const Effects = struct {
1899
2025
  // Pokémon Showdown clears P1 then P2 instead of status -> side -> foe
1900
2026
  if (showdown) {
1901
2027
  for (&battle.sides, 0..) |*s, i| {
1902
- const p = @intToEnum(Player, i);
2028
+ const p: Player = @enumFromInt(i);
1903
2029
  // Pokémon Showdown incorrectly does not prevent sleep/freeze from moving
1904
2030
  if (p != player and Status.any(s.stored().status)) {
1905
2031
  try log.curestatus(foe_ident, foe_stored.status, .Silent);
@@ -1908,7 +2034,7 @@ pub const Effects = struct {
1908
2034
  s.stored().status = Status.init(.PSN);
1909
2035
  try log.status(battle.active(p), s.stored().status, .None);
1910
2036
  }
1911
- try clearVolatiles(battle, p, log);
2037
+ try clearVolatiles(battle, p, p != player, options);
1912
2038
  }
1913
2039
  } else {
1914
2040
  if (Status.any(foe_stored.status)) {
@@ -1918,71 +2044,76 @@ pub const Effects = struct {
1918
2044
  try log.curestatus(foe_ident, foe_stored.status, .Silent);
1919
2045
  foe_stored.status = 0;
1920
2046
  }
1921
- try clearVolatiles(battle, player, log);
1922
- try clearVolatiles(battle, player.foe(), log);
2047
+ try clearVolatiles(battle, player, false, options);
2048
+ try clearVolatiles(battle, player.foe(), true, options);
1923
2049
  }
1924
2050
  }
1925
2051
 
1926
- fn heal(battle: anytype, player: Player, log: anytype) !void {
2052
+ fn heal(battle: anytype, player: Player, options: anytype) !void {
1927
2053
  var side = battle.side(player);
1928
2054
  var stored = side.stored();
1929
2055
  const ident = battle.active(player);
1930
2056
 
1931
2057
  // GLITCH: HP recovery move failure glitches
1932
2058
  const delta = stored.stats.hp - stored.hp;
1933
- 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
+ }
1934
2062
 
1935
2063
  const rest = side.last_selected_move == .Rest;
1936
2064
  if (rest) {
1937
2065
  // Adding the sleep status runs the sleep condition handler to roll duration
1938
2066
  if (showdown) battle.rng.advance(1);
1939
2067
  stored.status = Status.slf(2);
1940
- try log.statusFrom(ident, stored.status, Move.Rest);
2068
+ try options.log.statusFrom(ident, stored.status, Move.Rest);
1941
2069
  stored.hp = stored.stats.hp;
1942
2070
  } else {
1943
2071
  stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
1944
2072
  }
1945
- 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);
1946
2074
  }
1947
2075
 
1948
- fn hyperBeam(battle: anytype, player: Player, log: anytype) !void {
2076
+ fn hyperBeam(battle: anytype, player: Player, options: anytype) !void {
1949
2077
  battle.side(player).active.volatiles.Recharging = true;
1950
- try log.mustrecharge(battle.active(player));
2078
+ try options.log.mustrecharge(battle.active(player));
1951
2079
  }
1952
2080
 
1953
- 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 {
1954
2082
  var foe = battle.foe(player);
1955
2083
 
1956
2084
  if (showdown) {
1957
2085
  // Invulnerability trumps type immunity on Pokémon Showdown
1958
2086
  if (!foe.active.volatiles.Invulnerable and foe.active.types.includes(.Grass)) {
1959
- return log.immune(battle.active(player.foe()), .None);
2087
+ return options.log.immune(battle.active(player.foe()), .None);
1960
2088
  }
1961
- if (!try checkHit(battle, player, move, log)) return;
2089
+ if (!try checkHit(battle, player, move, options)) return;
1962
2090
  if (foe.active.volatiles.LeechSeed) return;
1963
2091
  } else {
1964
- if (!try checkHit(battle, player, move, log)) return;
2092
+ if (!try checkHit(battle, player, move, options)) return;
1965
2093
  if (foe.active.types.includes(.Grass) or foe.active.volatiles.LeechSeed) {
1966
- try log.lastmiss();
1967
- return log.miss(battle.active(player));
2094
+ try options.log.lastmiss();
2095
+ return options.log.miss(battle.active(player));
1968
2096
  }
1969
2097
  }
1970
2098
 
2099
+ try options.chance.commit(player, .hit);
1971
2100
  foe.active.volatiles.LeechSeed = true;
1972
2101
 
1973
- try log.start(battle.active(player.foe()), .LeechSeed);
2102
+ try options.log.start(battle.active(player.foe()), .LeechSeed);
1974
2103
  }
1975
2104
 
1976
- fn lightScreen(battle: anytype, player: Player, log: anytype) !void {
2105
+ fn lightScreen(battle: anytype, player: Player, options: anytype) !void {
1977
2106
  var side = battle.side(player);
1978
2107
 
1979
- 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
+ }
1980
2111
  side.active.volatiles.LightScreen = true;
1981
2112
 
1982
- try log.start(battle.active(player), .LightScreen);
2113
+ try options.log.start(battle.active(player), .LightScreen);
1983
2114
  }
1984
2115
 
1985
- 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 {
1986
2117
  var side = battle.side(player);
1987
2118
  var foe = battle.foe(player);
1988
2119
 
@@ -1998,45 +2129,51 @@ pub const Effects = struct {
1998
2129
  const has_mimic = has_mimic: {
1999
2130
  for (side.active.moves, 0..) |m, i| {
2000
2131
  if (m.id == .Mimic) {
2001
- oslot = @intCast(u8, i + 1);
2132
+ oslot = @intCast(i + 1);
2002
2133
  break :has_mimic true;
2003
2134
  }
2004
2135
  }
2005
2136
  break :has_mimic false;
2006
2137
  };
2007
- // If the foe is Invulnerable we still want to fall through to checkHit to be able to
2008
- // trigger |-miss| instead of |-fail|
2009
- if (!has_mimic and !foe.active.volatiles.Invulnerable) {
2010
- return try log.fail(battle.active(player.foe()), .None);
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);
2011
2143
  }
2012
2144
  }
2013
- if (!try checkHit(battle, player, move, log)) return;
2145
+ if (!try checkHit(battle, player, move, options)) return;
2146
+ try options.chance.commit(player, .hit);
2014
2147
 
2015
- const rslot = randomMoveSlot(&battle.rng, &foe.active.moves, 0);
2148
+ const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
2016
2149
  side.active.move(oslot).id = foe.active.move(rslot).id;
2017
2150
 
2018
- 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);
2019
2152
  }
2020
2153
 
2021
- fn mist(battle: anytype, player: Player, log: anytype) !void {
2154
+ fn mist(battle: anytype, player: Player, options: anytype) !void {
2022
2155
  var side = battle.side(player);
2023
2156
 
2024
2157
  if (side.active.volatiles.Mist) return;
2025
2158
  side.active.volatiles.Mist = true;
2026
2159
 
2027
- try log.start(battle.active(player), .Mist);
2160
+ try options.log.start(battle.active(player), .Mist);
2028
2161
  }
2029
2162
 
2030
- fn multiHit(battle: anytype, player: Player, move: Move.Data) void {
2163
+ fn multiHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
2031
2164
  var side = battle.side(player);
2032
2165
 
2033
2166
  assert(!side.active.volatiles.MultiHit);
2034
2167
  side.active.volatiles.MultiHit = true;
2035
2168
 
2036
- 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;
2037
2173
  }
2038
2174
 
2039
- 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;
2040
2177
  var foe = battle.foe(player);
2041
2178
  var foe_stored = foe.stored();
2042
2179
  const foe_ident = battle.active(player.foe());
@@ -2047,7 +2184,7 @@ pub const Effects = struct {
2047
2184
  if (showdown) {
2048
2185
  // Invulnerability trumps type immunity on Pokémon Showdown
2049
2186
  if (immune and !foe.active.volatiles.Invulnerable) return log.immune(foe_ident, .None);
2050
- if (!try checkHit(battle, player, move, log)) return;
2187
+ if (!try checkHit(battle, player, move, options)) return;
2051
2188
  }
2052
2189
  if (Status.any(foe_stored.status)) {
2053
2190
  return log.fail(
@@ -2057,8 +2194,9 @@ pub const Effects = struct {
2057
2194
  }
2058
2195
  if (!showdown) {
2059
2196
  if (immune) return log.immune(foe_ident, .None);
2060
- if (!try checkHit(battle, player, move, log)) return;
2197
+ if (!try checkHit(battle, player, move, options)) return;
2061
2198
  }
2199
+ try options.chance.commit(player, .hit);
2062
2200
 
2063
2201
  foe_stored.status = Status.init(.PAR);
2064
2202
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
@@ -2066,7 +2204,7 @@ pub const Effects = struct {
2066
2204
  try log.status(foe_ident, foe_stored.status, .None);
2067
2205
  }
2068
2206
 
2069
- 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 {
2070
2208
  var foe = battle.foe(player);
2071
2209
  var foe_stored = foe.stored();
2072
2210
 
@@ -2077,32 +2215,31 @@ pub const Effects = struct {
2077
2215
  }
2078
2216
 
2079
2217
  // Body Slam can't paralyze a Normal type Pokémon
2080
- const chance = !foe.active.types.includes(move.type) and if (showdown)
2081
- battle.rng.chance(u8, @as(u8, if (move.effect == .ParalyzeChance1) 26 else 77), 256)
2082
- else
2083
- battle.rng.next() < 1 + (if (move.effect == .ParalyzeChance1)
2084
- Gen12.percent(10)
2085
- else
2086
- Gen12.percent(30));
2087
- if (!chance) return;
2218
+ if (foe.active.types.includes(move.type)) return;
2219
+ if (!try Rolls.secondaryChance(battle, player, move.effect == .ParalyzeChance1, options)) {
2220
+ return;
2221
+ }
2088
2222
 
2089
2223
  foe_stored.status = Status.init(.PAR);
2090
2224
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2091
2225
 
2092
- try log.status(battle.active(player.foe()), foe_stored.status, .None);
2226
+ try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
2093
2227
  }
2094
2228
 
2095
- fn payDay(battle: anytype, player: Player, log: anytype) !void {
2096
- 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
+ }
2097
2233
  }
2098
2234
 
2099
- 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;
2100
2237
  var foe = battle.foe(player);
2101
2238
  var foe_stored = foe.stored();
2102
2239
  const foe_ident = battle.active(player.foe());
2103
2240
  const toxic = battle.side(player).last_selected_move == .Toxic;
2104
2241
 
2105
- 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)) {
2106
2243
  return;
2107
2244
  } else if (foe.active.volatiles.Substitute) {
2108
2245
  if (move.effect != .Poison) return;
@@ -2126,16 +2263,12 @@ pub const Effects = struct {
2126
2263
  }
2127
2264
 
2128
2265
  if (move.effect == .Poison) {
2129
- 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);
2130
2268
  } else {
2131
- const chance = if (showdown)
2132
- battle.rng.chance(u8, @as(u8, if (move.effect == .PoisonChance1) 52 else 103), 256)
2133
- else
2134
- battle.rng.next() < 1 + (if (move.effect == .PoisonChance1)
2135
- Gen12.percent(20)
2136
- else
2137
- Gen12.percent(40));
2138
- if (!chance) return;
2269
+ if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
2270
+ return;
2271
+ }
2139
2272
  }
2140
2273
 
2141
2274
  foe_stored.status = Status.init(.PSN);
@@ -2150,34 +2283,40 @@ pub const Effects = struct {
2150
2283
 
2151
2284
  fn rage(battle: anytype, player: Player) !void {
2152
2285
  var volatiles = &battle.side(player).active.volatiles;
2153
- if (volatiles.Rage) return;
2154
- volatiles.Rage = true;
2155
2286
  assert(!volatiles.Bide);
2156
- volatiles.state = 0;
2287
+ volatiles.Rage = true;
2157
2288
  }
2158
2289
 
2159
- fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
2290
+ fn recoil(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
2160
2291
  var side = battle.side(player);
2161
2292
  var stored = side.stored();
2162
2293
 
2163
- 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 /
2164
2298
  @as(u8, if (side.last_selected_move == .Struggle) 2 else 4), 1));
2165
- stored.hp = @intCast(u16, @max(@intCast(i16, stored.hp) - damage, 0));
2299
+ stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
2166
2300
 
2167
- 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
+ );
2168
2307
  if (showdown and stored.hp == 0) residual.* = false;
2169
2308
  }
2170
2309
 
2171
- fn reflect(battle: anytype, player: Player, log: anytype) !void {
2310
+ fn reflect(battle: anytype, player: Player, options: anytype) !void {
2172
2311
  var side = battle.side(player);
2173
2312
 
2174
- 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);
2175
2314
  side.active.volatiles.Reflect = true;
2176
2315
 
2177
- try log.start(battle.active(player), .Reflect);
2316
+ try options.log.start(battle.active(player), .Reflect);
2178
2317
  }
2179
2318
 
2180
- 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 {
2181
2320
  var foe = battle.foe(player);
2182
2321
  var foe_stored = foe.stored();
2183
2322
  const foe_ident = battle.active(player.foe());
@@ -2188,13 +2327,14 @@ pub const Effects = struct {
2188
2327
  // the Sleep Clause Mod might activate, causing us to not actually bypass
2189
2328
  } else {
2190
2329
  if (Status.any(foe_stored.status)) {
2191
- return log.fail(
2330
+ return options.log.fail(
2192
2331
  foe_ident,
2193
2332
  if (Status.is(foe_stored.status, .SLP)) .Sleep else .None,
2194
2333
  );
2195
2334
  }
2196
2335
  // If checkHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2197
- if (!showdown and !try checkHit(battle, player, move, log)) return;
2336
+ if (!showdown and !try checkHit(battle, player, move, options)) return;
2337
+ try options.chance.commit(player, .hit);
2198
2338
  }
2199
2339
 
2200
2340
  // Sleep Clause Mod
@@ -2205,40 +2345,32 @@ pub const Effects = struct {
2205
2345
  }
2206
2346
  foe.active.volatiles.Recharging = false;
2207
2347
 
2208
- const duration = @intCast(u3, if (showdown)
2209
- battle.rng.range(u8, 1, 8)
2210
- else loop: {
2211
- while (true) {
2212
- const r = battle.rng.next() & 7;
2213
- if (r != 0) break :loop r;
2214
- }
2215
- });
2216
-
2217
- foe_stored.status = Status.slp(duration);
2218
- try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
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);
2219
2351
  }
2220
2352
 
2221
- fn splash(battle: anytype, player: Player, log: anytype) !void {
2222
- 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);
2223
2355
  }
2224
2356
 
2225
- fn substitute(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
2357
+ fn substitute(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
2226
2358
  var side = battle.side(player);
2227
2359
  if (side.active.volatiles.Substitute) {
2228
- try log.fail(battle.active(player), .Substitute);
2360
+ try options.log.fail(battle.active(player), .Substitute);
2229
2361
  return;
2230
2362
  }
2231
2363
 
2232
2364
  assert(side.stored().stats.hp <= 1023);
2233
2365
  // Will be 0 if HP is <= 3 meaning that the user gets a 1 HP Substitute for "free"
2234
- const hp = @intCast(u8, side.stored().stats.hp / 4);
2366
+ const hp: u8 = @intCast(side.stored().stats.hp / 4);
2235
2367
  // Pokénon Showdown incorrectly checks for 1/4 HP based on `target.maxhp / 4` which returns
2236
2368
  // a floating point value and thus only correctly implements the Substitute 1/4 glitch when
2237
2369
  // the target's HP is exactly divisible by 4 (here we're using an inlined divCeil routine to
2238
2370
  // avoid having to convert to floating point)
2239
2371
  const required_hp = if (showdown) @divFloor(side.stored().stats.hp - 1, 4) + 1 else hp;
2240
2372
  if (side.stored().hp < required_hp) {
2241
- try log.fail(battle.active(player), .Weak);
2373
+ try options.log.fail(battle.active(player), .Weak);
2242
2374
  return;
2243
2375
  }
2244
2376
 
@@ -2246,35 +2378,37 @@ pub const Effects = struct {
2246
2378
  side.stored().hp -= hp;
2247
2379
  side.active.volatiles.substitute = hp + 1;
2248
2380
  side.active.volatiles.Substitute = true;
2249
- try log.start(battle.active(player), .Substitute);
2381
+ try options.log.start(battle.active(player), .Substitute);
2250
2382
  if (hp > 0) {
2251
- try log.damage(battle.active(player), side.stored(), .None);
2383
+ try options.log.damage(battle.active(player), side.stored(), .None);
2252
2384
  if (showdown and side.stored().hp == 0) residual.* = false;
2253
2385
  }
2254
2386
  }
2255
2387
 
2256
- fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
2257
- if (!showdown or battle.side(player).last_selected_move == .Teleport) return;
2258
-
2388
+ fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
2259
2389
  // Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
2260
- _ = try checkHit(battle, player, move, log);
2261
- 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
+ }
2262
2400
  }
2263
2401
 
2264
- fn thrashing(battle: anytype, player: Player) void {
2402
+ fn thrashing(battle: anytype, player: Player, options: anytype) void {
2265
2403
  var volatiles = &battle.side(player).active.volatiles;
2266
2404
  assert(!volatiles.Thrashing);
2267
2405
  assert(!volatiles.Bide);
2268
2406
 
2269
2407
  volatiles.Thrashing = true;
2270
- volatiles.state = 0;
2271
- volatiles.attacks = @intCast(u3, if (showdown)
2272
- battle.rng.range(u8, 2, 4)
2273
- else
2274
- (battle.rng.next() & 1) + 2);
2408
+ volatiles.attacks = Rolls.attackingDuration(battle, player, options);
2275
2409
  }
2276
2410
 
2277
- fn transform(battle: anytype, player: Player, log: anytype) !void {
2411
+ fn transform(battle: anytype, player: Player, options: anytype) !void {
2278
2412
  var side = battle.side(player);
2279
2413
  const foe = battle.foe(player);
2280
2414
  const foe_ident = battle.active(player.foe());
@@ -2300,10 +2434,10 @@ pub const Effects = struct {
2300
2434
  side.active.moves[i].pp = if (m.id != .None) 5 else 0;
2301
2435
  }
2302
2436
 
2303
- try log.transform(battle.active(player), foe_ident);
2437
+ try options.log.transform(battle.active(player), foe_ident);
2304
2438
  }
2305
2439
 
2306
- fn binding(battle: anytype, player: Player) void {
2440
+ fn binding(battle: anytype, player: Player, rewrap: bool, options: anytype) !void {
2307
2441
  var side = battle.side(player);
2308
2442
  var foe = battle.foe(player);
2309
2443
 
@@ -2311,12 +2445,16 @@ pub const Effects = struct {
2311
2445
  side.active.volatiles.Binding = true;
2312
2446
  // GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
2313
2447
  // (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
2314
- 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);
2315
2451
 
2316
- side.active.volatiles.attacks = distribution(battle) - 1;
2452
+ side.active.volatiles.attacks =
2453
+ try Rolls.distribution(battle, .Binding, player, options) - 1;
2317
2454
  }
2318
2455
 
2319
- 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;
2320
2458
  var side = battle.side(player);
2321
2459
  const ident = battle.active(player);
2322
2460
 
@@ -2328,7 +2466,7 @@ pub const Effects = struct {
2328
2466
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2329
2467
  if (boosts.atk == 6) return try log.fail(ident, .None);
2330
2468
  const n: u2 = if (move.effect == .AttackUp2) 2 else 1;
2331
- boosts.atk = @intCast(i4, @min(6, @as(i8, boosts.atk) + n));
2469
+ boosts.atk = @as(i4, @intCast(@min(6, @as(i8, boosts.atk) + n)));
2332
2470
  const reason = if (move.effect == .Rage) Boost.Rage else Boost.Attack;
2333
2471
  if (stats.atk == MAX_STAT_VALUE) {
2334
2472
  boosts.atk -= 1;
@@ -2339,8 +2477,8 @@ pub const Effects = struct {
2339
2477
  }
2340
2478
  return try log.fail(ident, .None);
2341
2479
  }
2342
- var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2343
- 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;
2344
2482
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2345
2483
  try log.boost(ident, reason, n);
2346
2484
  // Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
@@ -2350,7 +2488,7 @@ pub const Effects = struct {
2350
2488
  assert(boosts.def >= -6 and boosts.def <= 6);
2351
2489
  if (boosts.def == 6) return try log.fail(ident, .None);
2352
2490
  const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
2353
- boosts.def = @intCast(i4, @min(6, @as(i8, boosts.def) + n));
2491
+ boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
2354
2492
  if (stats.def == MAX_STAT_VALUE) {
2355
2493
  boosts.def -= 1;
2356
2494
  if (showdown) {
@@ -2359,15 +2497,15 @@ pub const Effects = struct {
2359
2497
  }
2360
2498
  return try log.fail(ident, .None);
2361
2499
  }
2362
- var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2363
- 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;
2364
2502
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2365
2503
  try log.boost(ident, .Defense, n);
2366
2504
  },
2367
2505
  .SpeedUp2 => {
2368
2506
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2369
2507
  if (boosts.spe == 6) return try log.fail(ident, .None);
2370
- boosts.spe = @intCast(i4, @min(6, @as(i8, boosts.spe) + 2));
2508
+ boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
2371
2509
  if (stats.spe == MAX_STAT_VALUE) {
2372
2510
  boosts.spe -= 1;
2373
2511
  if (showdown) {
@@ -2376,8 +2514,8 @@ pub const Effects = struct {
2376
2514
  }
2377
2515
  return try log.fail(ident, .None);
2378
2516
  }
2379
- var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2380
- 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;
2381
2519
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2382
2520
  try log.boost(ident, .Speed, 2);
2383
2521
  },
@@ -2385,7 +2523,7 @@ pub const Effects = struct {
2385
2523
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2386
2524
  if (boosts.spc == 6) return try log.fail(ident, .None);
2387
2525
  const n: u2 = if (move.effect == .SpecialUp2) 2 else 1;
2388
- boosts.spc = @intCast(i4, @min(6, @as(i8, boosts.spc) + n));
2526
+ boosts.spc = @intCast(@min(6, @as(i8, boosts.spc) + n));
2389
2527
  if (stats.spc == MAX_STAT_VALUE) {
2390
2528
  boosts.spc -= 1;
2391
2529
  if (showdown) {
@@ -2396,8 +2534,8 @@ pub const Effects = struct {
2396
2534
  }
2397
2535
  return try log.fail(ident, .None);
2398
2536
  }
2399
- var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2400
- 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;
2401
2539
  stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2402
2540
  try log.boost(ident, .SpecialAttack, n);
2403
2541
  try log.boost(ident, .SpecialDefense, n);
@@ -2405,7 +2543,7 @@ pub const Effects = struct {
2405
2543
  .EvasionUp1 => {
2406
2544
  assert(boosts.evasion >= -6 and boosts.evasion <= 6);
2407
2545
  if (boosts.evasion == 6) return try log.fail(ident, .None);
2408
- boosts.evasion = @intCast(i4, @min(6, @as(i8, boosts.evasion) + 1));
2546
+ boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
2409
2547
  try log.boost(ident, .Evasion, 1);
2410
2548
  },
2411
2549
  else => unreachable,
@@ -2415,32 +2553,32 @@ pub const Effects = struct {
2415
2553
  statusModify(battle.foe(player).stored().status, &battle.foe(player).active.stats);
2416
2554
  }
2417
2555
 
2418
- 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;
2419
2558
  var foe = battle.foe(player);
2420
2559
  const foe_ident = battle.active(player.foe());
2560
+ const secondary = move.effect.isStatDownChance();
2421
2561
 
2422
- if (foe.active.volatiles.Substitute) {
2423
- return if (!move.effect.isStatDownChance()) log.fail(foe_ident, .None);
2424
- }
2562
+ if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(foe_ident, .None);
2425
2563
 
2426
- if (move.effect.isStatDownChance()) {
2427
- const chance = if (showdown)
2428
- battle.rng.chance(u8, 85, 256)
2429
- else
2430
- battle.rng.next() < Gen12.percent(33) + 1;
2431
- if (!chance or foe.active.volatiles.Invulnerable) return;
2432
- } else if (!showdown and !try checkHit(battle, player, move, log)) {
2433
- 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);
2434
2571
  }
2435
2572
 
2436
2573
  var stats = &foe.active.stats;
2437
2574
  var boosts = &foe.active.boosts;
2575
+ const fail = showdown or !secondary;
2438
2576
 
2439
2577
  switch (move.effect) {
2440
2578
  .AttackDown1, .AttackDownChance => {
2441
2579
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2442
- if (boosts.atk == -6) return try log.fail(foe_ident, .None);
2443
- 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));
2444
2582
  if (stats.atk == 1) {
2445
2583
  boosts.atk += 1;
2446
2584
  if (showdown) {
@@ -2449,16 +2587,16 @@ pub const Effects = struct {
2449
2587
  }
2450
2588
  return try log.fail(foe_ident, .None);
2451
2589
  }
2452
- var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2453
- 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;
2454
2592
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2455
2593
  try log.boost(foe_ident, .Attack, -1);
2456
2594
  },
2457
2595
  .DefenseDown1, .DefenseDown2, .DefenseDownChance => {
2458
2596
  assert(boosts.def >= -6 and boosts.def <= 6);
2459
- 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);
2460
2598
  const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
2461
- boosts.def = @intCast(i4, @max(-6, @as(i8, boosts.def) - n));
2599
+ boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
2462
2600
  if (stats.def == 1) {
2463
2601
  boosts.def += 1;
2464
2602
  if (showdown) {
@@ -2467,15 +2605,15 @@ pub const Effects = struct {
2467
2605
  }
2468
2606
  return try log.fail(foe_ident, .None);
2469
2607
  }
2470
- var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2471
- 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;
2472
2610
  stats.def = @max(1, stat * mod[0] / mod[1]);
2473
2611
  try log.boost(foe_ident, .Defense, -@as(i8, n));
2474
2612
  },
2475
2613
  .SpeedDown1, .SpeedDownChance => {
2476
2614
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2477
- if (boosts.spe == -6) return try log.fail(foe_ident, .None);
2478
- 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));
2479
2617
  if (stats.spe == 1) {
2480
2618
  boosts.spe += 1;
2481
2619
  if (showdown) {
@@ -2484,16 +2622,16 @@ pub const Effects = struct {
2484
2622
  }
2485
2623
  return try log.fail(foe_ident, .None);
2486
2624
  }
2487
- var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2488
- 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;
2489
2627
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2490
2628
  try log.boost(foe_ident, .Speed, -1);
2491
2629
  assert(boosts.spe >= -6);
2492
2630
  },
2493
2631
  .SpecialDownChance => {
2494
2632
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2495
- if (boosts.spc == -6) return try log.fail(foe_ident, .None);
2496
- 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));
2497
2635
  if (stats.spc == 1) {
2498
2636
  boosts.spc += 1;
2499
2637
  if (showdown) {
@@ -2504,16 +2642,16 @@ pub const Effects = struct {
2504
2642
  }
2505
2643
  return try log.fail(foe_ident, .None);
2506
2644
  }
2507
- var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2508
- 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;
2509
2647
  stats.spc = @max(1, stat * mod[0] / mod[1]);
2510
2648
  try log.boost(foe_ident, .SpecialAttack, -1);
2511
2649
  try log.boost(foe_ident, .SpecialDefense, -1);
2512
2650
  },
2513
2651
  .AccuracyDown1 => {
2514
2652
  assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
2515
- if (boosts.accuracy == -6) return try log.fail(foe_ident, .None);
2516
- 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));
2517
2655
  try log.boost(foe_ident, .Accuracy, -1);
2518
2656
  },
2519
2657
  else => unreachable,
@@ -2524,8 +2662,7 @@ pub const Effects = struct {
2524
2662
  }
2525
2663
  };
2526
2664
 
2527
- fn unmodifiedStats(battle: anytype, who: Player) *Stats(u16) {
2528
- const side = battle.side(who);
2665
+ fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
2529
2666
  if (!side.active.volatiles.Transform) return &side.stored().stats;
2530
2667
  const id = ID.from(side.active.volatiles.transform);
2531
2668
  return &battle.side(id.player).pokemon[id.id - 1].stats;
@@ -2539,16 +2676,19 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
2539
2676
  }
2540
2677
  }
2541
2678
 
2542
- inline fn isForced(active: ActivePokemon) bool {
2679
+ inline fn isForced(active: anytype) bool {
2543
2680
  return active.volatiles.Recharging or active.volatiles.Rage or
2544
2681
  active.volatiles.Thrashing or active.volatiles.Charging;
2545
2682
  }
2546
2683
 
2547
- 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;
2548
2686
  var side = battle.side(who);
2549
2687
  var volatiles = &side.active.volatiles;
2550
2688
  const ident = battle.active(who);
2551
2689
 
2690
+ options.chance.clearDurations(who, clear);
2691
+
2552
2692
  if (volatiles.disabled_move != 0) {
2553
2693
  volatiles.disabled_move = 0;
2554
2694
  volatiles.disabled_duration = 0;
@@ -2561,15 +2701,15 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
2561
2701
  }
2562
2702
  if (volatiles.Mist) {
2563
2703
  volatiles.Mist = false;
2564
- try log.end(ident, .Mist);
2704
+ try log.end(ident, .MistSilent);
2565
2705
  }
2566
2706
  if (volatiles.FocusEnergy) {
2567
2707
  volatiles.FocusEnergy = false;
2568
- try log.end(ident, .FocusEnergy);
2708
+ try log.end(ident, .FocusEnergySilent);
2569
2709
  }
2570
2710
  if (volatiles.LeechSeed) {
2571
2711
  volatiles.LeechSeed = false;
2572
- try log.end(ident, .LeechSeed);
2712
+ try log.end(ident, .LeechSeedSilent);
2573
2713
  }
2574
2714
  if (!showdown and volatiles.Toxic) {
2575
2715
  volatiles.Toxic = false;
@@ -2577,107 +2717,329 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
2577
2717
  }
2578
2718
  if (volatiles.LightScreen) {
2579
2719
  volatiles.LightScreen = false;
2580
- try log.end(ident, .LightScreen);
2720
+ try log.end(ident, .LightScreenSilent);
2581
2721
  }
2582
2722
  if (volatiles.Reflect) {
2583
2723
  volatiles.Reflect = false;
2584
- try log.end(ident, .Reflect);
2724
+ try log.end(ident, .ReflectSilent);
2585
2725
  }
2586
2726
  if (showdown and volatiles.Toxic) {
2587
2727
  volatiles.Toxic = false;
2588
2728
  // Pokémon Showdown erroneously clears the toxic counter
2589
2729
  volatiles.toxic = 0;
2590
- try log.end(ident, .Toxic);
2730
+ try log.end(ident, .ToxicSilent);
2591
2731
  }
2592
2732
  }
2593
2733
 
2594
- fn disabled(side: *Side, ident: ID, log: anytype) !bool {
2595
- if (side.active.volatiles.disabled_move != 0) {
2596
- // A Pokémon that transforms after being disabled may end up with less move slots
2597
- const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
2598
- // side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
2599
- const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
2600
- 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
2601
2740
  else
2602
- side.last_selected_move;
2603
- if (m != .None and m == last) {
2604
- side.active.volatiles.Charging = false;
2605
- try log.disabled(ident, last);
2606
- return true;
2607
- }
2741
+ battle.rng.next() < Gen12.percent(50) + 1;
2742
+
2743
+ try options.chance.speedTie(p1);
2744
+ return p1;
2608
2745
  }
2609
- return false;
2610
- }
2611
2746
 
2612
- inline fn buildRage(battle: anytype, who: Player, log: anytype) !void {
2613
- const side = battle.side(who);
2614
- if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
2615
- 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;
2616
2757
  }
2617
- }
2618
2758
 
2619
- fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
2620
- var volatiles = &active.volatiles;
2621
- assert(volatiles.Thrashing);
2622
- 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
+ };
2623
2769
 
2624
- volatiles.Thrashing = false;
2625
- volatiles.Confusion = true;
2626
- volatiles.confusion = @intCast(u3, if (showdown)
2627
- battle.rng.range(u8, 2, 6)
2628
- else
2629
- (battle.rng.next() & 3) + 2);
2770
+ assert(roll >= 217 and roll <= 255);
2771
+ try options.chance.damage(player, roll);
2772
+ return roll;
2773
+ }
2630
2774
 
2631
- return true;
2632
- }
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;
2633
2782
 
2634
- const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
2783
+ options.chance.hit(ok, accuracy);
2784
+ return ok;
2785
+ }
2635
2786
 
2636
- fn distribution(battle: anytype) u3 {
2637
- if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
2638
- const r = (battle.rng.next() & 3);
2639
- return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
2640
- }
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;
2641
2794
 
2642
- fn randomMoveSlot(rand: anytype, moves: []MoveSlot, check_pp: u4) u4 {
2643
- if (showdown) {
2644
- if (check_pp == 0) {
2645
- var i: usize = moves.len;
2646
- while (i > 0) {
2647
- i -= 1;
2648
- 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);
2649
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);
2650
2975
  } else {
2651
- var r = rand.range(u4, 0, @intCast(u4, check_pp)) + 1;
2652
- var i: usize = 0;
2653
- while (i < moves.len and r > 0) : (i += 1) {
2654
- if (moves[i].pp > 0) {
2655
- r -= 1;
2656
- 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);
2657
3017
  }
2658
3018
  }
2659
- return @intCast(u4, i + 1);
2660
- }
2661
- }
2662
3019
 
2663
- while (true) {
2664
- const r = @intCast(u4, rand.next() & 3);
2665
- 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;
2666
3028
  }
2667
- }
3029
+ };
2668
3030
 
2669
3031
  test "RNG agreement" {
2670
3032
  if (!showdown) return;
2671
3033
  var expected: [256]u32 = undefined;
2672
3034
  for (0..expected.len) |i| {
2673
- expected[i] = @intCast(u32, i * 0x1000000);
3035
+ expected[i] = @intCast(i * 0x1000000);
2674
3036
  }
2675
3037
 
2676
- var spe = rng.FixedRNG(1, expected.len){ .rolls = expected };
2677
- var cfz = rng.FixedRNG(1, expected.len){ .rolls = expected };
2678
- var par = rng.FixedRNG(1, expected.len){ .rolls = expected };
2679
- var brn = rng.FixedRNG(1, expected.len){ .rolls = expected };
2680
- var eff = rng.FixedRNG(1, expected.len){ .rolls = expected };
3038
+ var spe: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3039
+ var cfz: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3040
+ var par: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3041
+ var brn: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3042
+ var eff: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
2681
3043
 
2682
3044
  for (0..expected.len) |i| {
2683
3045
  try expectEqual(spe.range(u8, 0, 2) == 0, i < Gen12.percent(50) + 1);
@@ -2713,7 +3075,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
2713
3075
  const side = battle.side(player);
2714
3076
  const foe = battle.foe(player);
2715
3077
 
2716
- var active = side.active;
3078
+ var active = &side.active;
2717
3079
  const stored = side.stored();
2718
3080
 
2719
3081
  // While players are not given any input options on the cartridge in these cases,
@@ -2727,7 +3089,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
2727
3089
  // strictly correct as the player would not have been presented the option to move or
2728
3090
  // switch at all.
2729
3091
  if (isForced(active)) {
2730
- out[n] = .{ .type = .Move, .data = @boolToInt(showdown) };
3092
+ out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
2731
3093
  n += 1;
2732
3094
  return n;
2733
3095
  }
@@ -2764,7 +3126,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
2764
3126
  while (slot <= 4) : (slot += 1) {
2765
3127
  const m = active.moves[slot - 1];
2766
3128
  if (m.id == .None) break;
2767
- if (m.id == side.last_selected_move) {
3129
+ if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
2768
3130
  // Pokémon Showdown displays Struggle if limited to Bide but unable to pick
2769
3131
  const struggle =
2770
3132
  m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);