@pkmn/engine 0.1.0-dev.c4dec0f7 → 0.1.0-dev.c544e10e

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