@pkmn/engine 0.1.0-dev.545e0ef3 → 0.1.0-dev.5bbdaae3

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