@pkmn/engine 0.1.0-dev.9a0c07c3 → 0.1.0-dev.a324c50f

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