@pkmn/engine 0.1.0-dev.02b7f810 → 0.1.0-dev.04c4d944

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