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

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 (63) hide show
  1. package/LICENSE +1 -3
  2. package/README.md +135 -71
  3. package/build/pkg/addon.d.ts +7 -0
  4. package/build/pkg/addon.js +111 -0
  5. package/build/pkg/addon.js.map +1 -0
  6. package/build/pkg/data/ids.json +12 -12
  7. package/build/pkg/data/layout.json +27 -5
  8. package/build/pkg/data/protocol.json +69 -12
  9. package/build/pkg/data.d.ts +5 -6
  10. package/build/pkg/data.js +3 -5
  11. package/build/pkg/data.js.map +1 -1
  12. package/build/pkg/gen1.d.ts +5 -4
  13. package/build/pkg/gen1.js +49 -33
  14. package/build/pkg/gen1.js.map +1 -1
  15. package/build/pkg/index.d.ts +17 -13
  16. package/build/pkg/index.js.map +1 -1
  17. package/build/pkg/protocol.d.ts +4 -2
  18. package/build/pkg/protocol.js +65 -44
  19. package/build/pkg/protocol.js.map +1 -1
  20. package/build/test/display.js +400 -0
  21. package/build/test/display.js.map +1 -0
  22. package/build/tools/debug.js +134 -0
  23. package/build/tools/debug.js.map +1 -0
  24. package/build.zig +161 -149
  25. package/package.json +86 -46
  26. package/src/bin/install-pkmn-engine +79 -39
  27. package/src/bin/pkmn-debug +62 -0
  28. package/src/lib/common/battle.zig +17 -0
  29. package/src/lib/common/data.zig +19 -31
  30. package/src/lib/common/debug.zig +23 -13
  31. package/src/lib/common/js.zig +118 -0
  32. package/src/lib/common/optional.zig +67 -0
  33. package/src/lib/common/options.zig +14 -3
  34. package/src/lib/common/protocol.zig +942 -403
  35. package/src/lib/common/rational.zig +357 -0
  36. package/src/lib/common/rng.zig +35 -26
  37. package/src/lib/common/util.zig +12 -0
  38. package/src/lib/gen1/calc.zig +491 -0
  39. package/src/lib/gen1/chance.zig +1063 -0
  40. package/src/lib/gen1/data/moves.zig +202 -190
  41. package/src/lib/gen1/data/species.zig +5 -5
  42. package/src/lib/gen1/data/types.zig +10 -10
  43. package/src/lib/gen1/data.zig +106 -92
  44. package/src/lib/gen1/helpers.zig +620 -35
  45. package/src/lib/gen1/mechanics.zig +1214 -826
  46. package/src/lib/gen2/calc.zig +143 -0
  47. package/src/lib/gen2/chance.zig +1315 -0
  48. package/src/lib/gen2/data/items.zig +14 -20
  49. package/src/lib/gen2/data/moves.zig +664 -570
  50. package/src/lib/gen2/data/species.zig +2 -2
  51. package/src/lib/gen2/data/types.zig +137 -34
  52. package/src/lib/gen2/data.zig +134 -96
  53. package/src/lib/gen2/generated.zig +806 -0
  54. package/src/lib/gen2/helpers.zig +50 -26
  55. package/src/lib/gen2/mechanics.zig +3591 -12
  56. package/src/lib/node.zig +131 -0
  57. package/src/lib/pkmn.zig +86 -12
  58. package/src/lib/wasm.zig +13 -0
  59. package/src/test/display/pkmn.html.tmpl +368 -0
  60. package/src/lib/binding/node.zig +0 -229
  61. package/src/lib/binding/wasm.zig +0 -10
  62. package/src/lib/gen1/README.md +0 -778
  63. package/src/lib/gen2/README.md +0 -309
@@ -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,50 +58,50 @@ 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
+ const r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
75
+ const 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
 
91
- var p1_slot = findFirstAlive(p1);
93
+ const p1_slot = findFirstAlive(p1);
92
94
  assert(!showdown or p1_slot == 1);
93
95
  if (p1_slot == 0) return if (findFirstAlive(p2) == 0) Result.Tie else Result.Lose;
94
96
 
95
- var p2_slot = findFirstAlive(p2);
97
+ const p2_slot = findFirstAlive(p2);
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,120 @@ 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
- assert(volatiles.attacks > 0);
638
+ if (options.calc.modify(player, .attacking)) |extend| {
639
+ if (!extend) volatiles.attacks = 0;
640
+ } else {
641
+ volatiles.attacks -= 1;
642
+ }
643
+ try options.chance.attacking(player, volatiles.attacks);
598
644
 
599
- volatiles.attacks -= 1;
600
645
  if (volatiles.attacks != 0) {
601
- try log.activate(ident, .Bide);
646
+ try log.activate(.{ ident, .Bide });
602
647
  return .done;
603
648
  }
604
649
 
605
650
  volatiles.Bide = false;
606
- try log.end(ident, .Bide);
651
+ try log.end(.{ ident, .Bide });
607
652
 
608
653
  battle.last_damage = volatiles.state *% 2;
609
654
  volatiles.state = 0;
610
655
 
611
656
  if (battle.last_damage == 0) {
612
- try log.fail(ident, .None);
657
+ try log.fail(.{ ident, .None });
613
658
  return .done;
614
659
  }
615
660
 
616
- _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
617
- try buildRage(battle, player.foe(), log);
661
+ const sub = showdown and foe.active.volatiles.Substitute;
662
+ _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
663
+ if (foe.stored().hp > 0 and !sub) try buildRage(battle, player.foe(), options);
664
+
665
+ // For reasons passing understanding, Pokémon Showdown still inflicts residual damage to
666
+ // Bide's user even if the above damage has caused the foe to faint. It's simpler to always
667
+ // run residual here regardless of whether the foe fainted and opt-out of the default flow
668
+ if (showdown) {
669
+ residual.* = false;
670
+ try handleResidual(battle, player, options);
671
+ }
618
672
 
619
673
  return .done;
620
674
  }
621
675
 
622
676
  if (volatiles.Thrashing) {
623
- 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);
677
+ if (options.calc.modify(player, .attacking)) |extend| {
678
+ if (!extend) volatiles.attacks = 0;
679
+ } else {
680
+ volatiles.attacks -= 1;
628
681
  }
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);
682
+ try options.chance.attacking(player, volatiles.attacks);
683
+
684
+ if (!showdown and handleThrashing(battle, active, player, options)) {
685
+ try log.start(.{ battle.active(player), .ConfusionSilent });
686
+ }
687
+ try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
688
+ if (showdown) {
689
+ // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
690
+ // used in useMove and doesn't have the notion of skipping canMove semantics
691
+ side.last_used_move = side.last_selected_move;
692
+ if (handleThrashing(battle, active, player, options)) {
693
+ try log.start(.{ battle.active(player), .ConfusionSilent });
694
+ }
632
695
  }
633
696
  return .skip_can;
634
697
  }
635
698
 
636
699
  if (volatiles.Binding) {
637
- 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
- );
700
+ if (options.calc.modify(player, .binding)) |extend| {
701
+ if (!extend) volatiles.attacks = 0;
702
+ } else {
703
+ volatiles.attacks -= 1;
704
+ }
705
+ try options.chance.binding(player, volatiles.attacks);
706
+
707
+ try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
645
708
  if (showdown or battle.last_damage != 0) {
646
- _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
709
+ const sub = showdown and foe.active.volatiles.Substitute;
710
+ _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
711
+ if (battle.foe(player).stored().hp > 0 and !sub) {
712
+ try buildRage(battle, player.foe(), options);
713
+ }
647
714
  }
648
715
  return .done;
649
716
  }
@@ -657,56 +724,57 @@ fn canMove(
657
724
  mslot: u4,
658
725
  auto: bool,
659
726
  skip_pp: bool,
660
- from: ?Move,
727
+ from: Move,
661
728
  residual: *bool,
662
- log: anytype,
729
+ options: anytype,
663
730
  ) !bool {
664
731
  var side = battle.side(player);
665
732
  const player_ident = battle.active(player);
666
733
  const move = Move.get(side.last_selected_move);
667
734
 
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
- var skip = skip_pp;
673
735
  if (side.active.volatiles.Charging) {
674
736
  side.active.volatiles.Charging = false;
675
737
  side.active.volatiles.Invulnerable = false;
676
738
  } else if (move.effect == .Charge) {
677
- try log.move(player_ident, side.last_selected_move, .{}, from);
678
- try Effects.charge(battle, player, log);
739
+ try options.log.move(.{ player_ident, side.last_selected_move, ID{}, from });
740
+ setCounterable(battle, player, side, move);
741
+ try Effects.charge(battle, player, options);
679
742
  return false;
680
743
  }
681
744
 
682
- if (!showdown or !special) side.last_used_move = side.last_selected_move;
683
- if (!skip) decrementPP(side, mslot, auto);
745
+ side.last_used_move = side.last_selected_move;
746
+ if (!skip_pp) decrementPP(side, mslot, auto);
684
747
 
685
748
  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);
749
+ try options.log.move(.{ player_ident, side.last_selected_move, battle.active(target), from });
750
+ setCounterable(battle, player, side, move);
694
751
 
695
752
  if (move.effect.onBegin()) {
696
- try onBegin(battle, player, move, mslot, residual, log);
753
+ try onBegin(battle, player, move, mslot, residual, options);
697
754
  return false;
698
755
  }
699
756
 
700
757
  if (move.effect == .Thrashing) {
701
- Effects.thrashing(battle, player);
758
+ Effects.thrashing(battle, player, options);
702
759
  } else if (!showdown and move.effect == .Binding) {
703
760
  // Pokémon Showdown handles this after hit/miss checks and damage calculation
704
- Effects.binding(battle, player);
761
+ try Effects.binding(battle, player, false, options);
705
762
  }
706
763
 
707
764
  return true;
708
765
  }
709
766
 
767
+ fn setCounterable(battle: anytype, player: Player, side: *Side, move: Move.Data) void {
768
+ // The Counter desync is caused by the cartridge not calling GetCurrentMove until now, meaning
769
+ // in cases where an early return happens the data for a players last selected move does not get
770
+ // reloaded and HandleCounterMove actually bases its success/failure off of stale information.
771
+ // This boolean state we track here doesn't exist on the cartridge because it instead manifests
772
+ // as the desync results from actually having two separate battle states that subtly disagree
773
+ const counterable = side.last_selected_move != .Counter and move.bp > 0 and
774
+ (move.type == .Normal or move.type == .Fighting);
775
+ battle.lastMove(player).counterable = @intFromBool(counterable);
776
+ }
777
+
710
778
  fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
711
779
  if (side.last_selected_move == .Struggle) return;
712
780
 
@@ -716,12 +784,14 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
716
784
  assert(!volatiles.Rage and !volatiles.Thrashing and !volatiles.MultiHit);
717
785
  if (volatiles.Bide) return;
718
786
 
719
- assert(active.move(mslot).pp > 0 or auto);
720
- active.move(mslot).pp = @intCast(u6, active.move(mslot).pp) -% 1;
787
+ var move_slot = active.move(mslot);
788
+ assert(move_slot.pp > 0 or auto);
789
+ move_slot.pp = @as(u6, @intCast(move_slot.pp)) -% 1;
721
790
  if (volatiles.Transform) return;
722
791
 
723
- assert(side.stored().move(mslot).pp > 0 or auto);
724
- side.stored().move(mslot).pp = @intCast(u6, side.stored().move(mslot).pp) -% 1;
792
+ move_slot = side.stored().move(mslot);
793
+ assert(move_slot.pp > 0 or auto);
794
+ move_slot.pp = @as(u6, @intCast(move_slot.pp)) -% 1;
725
795
  assert(active.move(mslot).pp == side.stored().move(mslot).pp);
726
796
  }
727
797
 
@@ -729,12 +799,12 @@ fn incrementPP(side: *Side, mslot: u4) void {
729
799
  var active = &side.active;
730
800
  const volatiles = &active.volatiles;
731
801
 
732
- active.move(mslot).pp = @intCast(u6, active.move(mslot).pp) +% 1;
802
+ active.move(mslot).pp = @as(u6, @intCast(active.move(mslot).pp)) +% 1;
733
803
  // GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
734
804
  if (showdown and volatiles.Transform) return;
735
805
 
736
806
  assert(mslot > 0 and mslot <= 4);
737
- side.stored().moves[mslot - 1].pp = @intCast(u6, side.stored().moves[mslot - 1].pp) +% 1;
807
+ side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% 1;
738
808
  }
739
809
 
740
810
  // Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
@@ -742,10 +812,12 @@ fn doMove(
742
812
  battle: anytype,
743
813
  player: Player,
744
814
  mslot: u4,
815
+ rewrap: bool,
745
816
  auto: bool,
746
817
  residual: *bool,
747
- log: anytype,
818
+ options: anytype,
748
819
  ) !?Result {
820
+ var log = options.log;
749
821
  var side = battle.side(player);
750
822
  const foe = battle.foe(player);
751
823
 
@@ -760,37 +832,44 @@ fn doMove(
760
832
  var hits: u4 = 1;
761
833
  var effectiveness = Effectiveness.neutral;
762
834
 
835
+ // Due to control flow shenanigans we need to clear last_damage for Pokémon Showdown
836
+ if (showdown and !counter) battle.last_damage = 0;
837
+
763
838
  // The cartridge handles set damage moves in applyDamage but we short circuit to simplify things
764
839
  if (move.effect == .SuperFang or move.effect == .SpecialDamage) {
765
- return specialDamage(battle, player, move, log);
840
+ return specialDamage(battle, player, move, options);
766
841
  }
767
842
 
768
843
  // Pokémon Showdown runs invulnerability / immunity checks before checking accuracy - simply
769
844
  // calling moveHit early covers most of that but we also need to check type immunity first
770
845
  var miss = showdown and miss: {
771
846
  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);
847
+ (@intFromEnum(move.type.effectiveness(foe.active.types.type1)) == 0 or
848
+ @intFromEnum(move.type.effectiveness(foe.active.types.type2)) == 0);
774
849
  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);
850
+ if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) {
851
+ battle.last_damage = 0;
852
+ break :miss true;
853
+ }
854
+ break :miss (move.target != .Self and
855
+ !moveHit(battle, player, move, &immune, &mist, options));
777
856
  };
778
857
  assert(!immune or miss or (showdown and move.effect == .Binding));
779
858
 
780
- var late = showdown;
859
+ var late = showdown and move.effect != .Explode;
781
860
  const skip = status or immune;
782
861
  if ((!showdown or (!skip or counter)) and !miss) blk: {
783
862
  if (showdown and move.effect.isMulti()) {
784
- Effects.multiHit(battle, player, move);
863
+ try Effects.multiHit(battle, player, move, options);
785
864
  hits = side.active.volatiles.attacks;
786
865
  late = false;
787
866
  }
788
867
 
789
868
  // Cartridge rolls for crit even for moves that can't crit (Counter/Metronome/status/OHKO)
790
869
  const check = !showdown or (!counter and move.effect != .OHKO);
791
- if (check) crit = checkCriticalHit(battle, player, move);
870
+ if (check) crit = try checkCriticalHit(battle, player, move, options);
792
871
 
793
- if (counter) return counterDamage(battle, player, move, log);
872
+ if (counter) return counterDamage(battle, player, move, options);
794
873
 
795
874
  battle.last_damage = 0;
796
875
 
@@ -812,17 +891,15 @@ fn doMove(
812
891
  effectiveness = adjustDamage(battle, player);
813
892
  immune = effectiveness == 0;
814
893
  }
815
- randomizeDamage(battle);
894
+ try randomizeDamage(battle, player, options);
816
895
  }
817
896
  }
818
897
 
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
-
898
+ const zero = battle.last_damage == 0;
822
899
  miss = if (showdown or skip)
823
900
  miss
824
901
  else
825
- (!moveHit(battle, player, move, &immune, &mist) or battle.last_damage == 0);
902
+ (!moveHit(battle, player, move, &immune, &mist, options) or zero);
826
903
 
827
904
  assert(showdown or miss or battle.last_damage > 0 or skip);
828
905
  assert((!showdown and miss) or !(ohko and immune));
@@ -830,11 +907,11 @@ fn doMove(
830
907
 
831
908
  if (!showdown or !miss) {
832
909
  if (move.effect == .MirrorMove) {
833
- return mirrorMove(battle, player, mslot, auto, residual, log);
910
+ return mirrorMove(battle, player, mslot, rewrap, auto, residual, options);
834
911
  } else if (move.effect == .Metronome) {
835
- return metronome(battle, player, mslot, auto, residual, log);
912
+ return metronome(battle, player, mslot, rewrap, auto, residual, options);
836
913
  } else if (move.effect.onEnd()) {
837
- try onEnd(battle, player, move, log);
914
+ try onEnd(battle, player, move, options);
838
915
  return null;
839
916
  }
840
917
  }
@@ -846,55 +923,65 @@ fn doMove(
846
923
  ohko = (!showdown or (!immune and !invulnerable)) and
847
924
  move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
848
925
  if (ohko) {
849
- try log.immune(foe_ident, .OHKO);
850
- } else if (immune and !invulnerable) {
851
- try log.immune(foe_ident, .None);
926
+ try log.immune(.{ foe_ident, .OHKO });
927
+ } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
928
+ try log.immune(.{ foe_ident, .None });
852
929
  } else if (mist) {
853
- if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
854
- try log.fail(foe_ident, .None);
930
+ if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
931
+ try log.fail(.{ foe_ident, .None });
855
932
  } else {
856
- try log.lastmiss();
857
- try log.miss(battle.active(player));
933
+ if (showdown or !zero) try options.chance.commit(player, .miss);
934
+ try log.lastmiss(.{});
935
+ try log.miss(.{battle.active(player)});
858
936
  }
859
937
  if (move.effect == .JumpKick) {
860
938
  // Recoil is supposed to be damage/8 but damage will always be 0 here
861
- assert(battle.last_damage == 0);
939
+ assert(showdown or battle.last_damage == 0);
862
940
  battle.last_damage = 1;
863
- _ = try applyDamage(battle, player, player.foe(), .None, log);
941
+ _ = try applyDamage(battle, player, player.foe(), .None, options);
864
942
  if (showdown and side.stored().hp == 0) residual.* = false;
865
943
  } else if (move.effect == .Explode) {
866
944
  try Effects.explode(battle, player);
867
- try buildRage(battle, player.foe(), log);
945
+ try buildRage(battle, player.foe(), options);
868
946
  } else if (showdown and move.effect == .Disable) {
869
- try buildRage(battle, player.foe(), log);
947
+ try buildRage(battle, player.foe(), options);
948
+ } else if (!showdown and immune and move.effect == .Binding) {
949
+ try options.chance.commit(player, .binding);
870
950
  }
871
951
  return null;
952
+ } else {
953
+ try options.chance.commit(player, .hit);
872
954
  }
873
955
 
874
956
  // On the cartridge MultiHit doesn't get set up until after damage has been applied for the
875
957
  // first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
876
958
  // it up above before damage calculation).
877
959
  if (!showdown and move.effect.isMulti()) {
878
- Effects.multiHit(battle, player, move);
960
+ try Effects.multiHit(battle, player, move, options);
879
961
  hits = side.active.volatiles.attacks;
880
962
  }
881
963
 
964
+ // Pokémon Showdown only builds Rage for Disable/Explosion (hit/miss) when attacking into a sub
965
+ const sub = showdown and foe.active.volatiles.Substitute;
966
+
882
967
  var nullified = false;
883
968
  var hit: u4 = 0;
884
969
  while (hit < hits) {
885
970
  if (hit == 0) {
886
- if (crit) try log.crit(battle.active(player.foe()));
971
+ if (crit) try log.crit(.{battle.active(player.foe())});
887
972
  if (effectiveness > Effectiveness.neutral) {
888
- try log.supereffective(battle.active(player.foe()));
973
+ try log.supereffective(.{battle.active(player.foe())});
889
974
  } else if (effectiveness < Effectiveness.neutral) {
890
- try log.resisted(battle.active(player.foe()));
975
+ try log.resisted(.{battle.active(player.foe())});
891
976
  }
892
977
  }
893
- if (!skip) nullified = try applyDamage(battle, player.foe(), player.foe(), .None, log);
894
- if (hit == 0 and ohko) try log.ohko();
978
+ if (!skip) nullified = try applyDamage(battle, player.foe(), player.foe(), .None, options);
979
+ if (hit == 0 and ohko) try log.ohko(.{});
895
980
  hit += 1;
896
981
  if (foe.stored().hp == 0) break;
897
- if (!late) try buildRage(battle, player.foe(), log);
982
+ if (!late and (!sub or move.effect == .Explode)) {
983
+ try buildRage(battle, player.foe(), options);
984
+ }
898
985
  // If the substitute breaks during a multi-hit attack, the attack ends
899
986
  if (nullified) break;
900
987
  }
@@ -904,17 +991,23 @@ fn doMove(
904
991
  assert(nullified or foe.stored().hp == 0 or side.active.volatiles.attacks - hit == 0);
905
992
  side.active.volatiles.attacks = 0;
906
993
  if (showdown and move.effect == .Twineedle and !nullified and foe.stored().hp > 0) {
907
- try Effects.poison(battle, player, Move.get(.PoisonSting), log);
994
+ try Effects.poison(battle, player, Move.get(.PoisonSting), options);
908
995
  }
909
- try log.hitcount(battle.active(player.foe()), hit);
996
+ try log.hitcount(.{ battle.active(player.foe()), hit });
910
997
  } else if (showdown) {
911
998
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
912
999
  if (move.effect == .Binding) {
913
- Effects.binding(battle, player);
1000
+ try Effects.binding(battle, player, rewrap, options);
914
1001
  if (immune) {
915
1002
  battle.last_damage = 0;
1003
+ assert(foe.stored().hp > 0);
916
1004
  // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
917
- try log.damage(battle.active(player.foe()), foe.stored(), .None);
1005
+ if (sub) {
1006
+ try log.activate(.{ battle.active(player.foe()), .Substitute });
1007
+ } else {
1008
+ try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
1009
+ try buildRage(battle, player.foe(), options);
1010
+ }
918
1011
  return null;
919
1012
  }
920
1013
  }
@@ -927,12 +1020,12 @@ fn doMove(
927
1020
  // On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
928
1021
  // but this is only done to setup the MultiHit looping in the first place. Moving the MultiHit
929
1022
  // 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);
1023
+ if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
931
1024
 
932
1025
  if (foe.stored().hp == 0) return null;
933
1026
 
934
- // Pokémon Showdown builds Rage at the wrong time for non-MultiHit moves
935
- if (late) try buildRage(battle, player.foe(), log);
1027
+ // Pokémon Showdown builds Rage at the wrong time for non-MultiHit move
1028
+ if (late and !sub and move.effect != .Disable) try buildRage(battle, player.foe(), options);
936
1029
 
937
1030
  if (!move.effect.isSpecial()) {
938
1031
  // On the cartridge Rage is not considered to be "special" and thus gets executed for a
@@ -943,28 +1036,29 @@ fn doMove(
943
1036
  // Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
944
1037
  // puts the |-status| message before |-hitcount| instead of after.
945
1038
  if (move.effect == .Twineedle) {
946
- if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), log);
1039
+ if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
947
1040
  } else if (move.effect == .Disable) {
948
- return Effects.disable(battle, player, move, log);
1041
+ const result = try Effects.disable(battle, player, move, options);
1042
+ if (showdown) try buildRage(battle, player.foe(), options);
1043
+ return result;
949
1044
  } else {
950
- try moveEffect(battle, player, move, log);
1045
+ try moveEffect(battle, player, move, options);
951
1046
  }
952
1047
  }
953
1048
 
954
1049
  return null;
955
1050
  }
956
1051
 
957
- fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
1052
+ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !bool {
958
1053
  const side = battle.side(player);
959
1054
 
960
1055
  // 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;
1056
+ var rate: u16 = Species.chance(side.stored().species);
1057
+ // GLITCH: Focus Energy reduces critical hit rate instead of increasing it
1058
+ rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
1059
+ rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
965
1060
 
966
- if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
967
- return std.math.rotl(u8, battle.rng.next(), 3) < chance;
1061
+ return Rolls.criticalHit(battle, player, @intCast(rate), options);
968
1062
  }
969
1063
 
970
1064
  fn calcDamage(
@@ -1016,9 +1110,9 @@ fn calcDamage(
1016
1110
  def = @max((def / 4) & 255, if (showdown) 1 else 0);
1017
1111
  }
1018
1112
 
1019
- const lvl = @as(u32, side.stored().level * @as(u2, if (crit) 2 else 1));
1113
+ const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
1020
1114
 
1021
- def = @as(u32, if (move.effect == .Explode) @max(def / 2, 1) else def);
1115
+ def = if (move.effect == .Explode) @max(def / 2, 1) else def;
1022
1116
 
1023
1117
  if (def == 0) return false;
1024
1118
 
@@ -1030,7 +1124,7 @@ fn calcDamage(
1030
1124
  d = @min(997, d);
1031
1125
  d += 2;
1032
1126
 
1033
- battle.last_damage = @intCast(u16, d);
1127
+ battle.last_damage = @intCast(d);
1034
1128
 
1035
1129
  return true;
1036
1130
  }
@@ -1044,9 +1138,9 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
1044
1138
  var d = battle.last_damage;
1045
1139
  if (side.active.types.includes(move.type)) d +%= d / 2;
1046
1140
 
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));
1141
+ const neutral = @intFromEnum(Effectiveness.Neutral);
1142
+ const eff1: u16 = @intFromEnum(move.type.effectiveness(types.type1));
1143
+ const eff2: u16 = @intFromEnum(move.type.effectiveness(types.type2));
1050
1144
 
1051
1145
  // Type effectiveness matchup precedence only matters with (NVE, SE)
1052
1146
  if (!showdown and (eff1 + eff2) == Effectiveness.mismatch and
@@ -1066,101 +1160,87 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
1066
1160
  return if (types.type1 == types.type2) eff1 * neutral else eff1 * eff2;
1067
1161
  }
1068
1162
 
1069
- fn randomizeDamage(battle: anytype) void {
1163
+ fn randomizeDamage(battle: anytype, player: Player, options: anytype) !void {
1070
1164
  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);
1165
+ const random = try Rolls.damage(battle, player, options);
1166
+ options.calc.base(player.foe(), battle.last_damage);
1167
+ battle.last_damage = @intCast(@as(u32, battle.last_damage) *% random / 255);
1168
+ options.calc.final(player.foe(), battle.last_damage);
1082
1169
  }
1083
1170
 
1084
- fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
1171
+ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
1085
1172
  const side = battle.side(player);
1173
+ const foe = battle.foe(player);
1086
1174
 
1087
- if (!try checkHit(battle, player, move, log)) return null;
1175
+ if (!try checkHit(battle, player, move, options)) return null;
1176
+ try options.chance.commit(player, .hit);
1088
1177
 
1089
1178
  battle.last_damage = switch (side.last_selected_move) {
1090
- .SuperFang => @max(battle.foe(player).stored().hp / 2, 1),
1179
+ .SuperFang => @max(foe.stored().hp / 2, 1),
1091
1180
  .SeismicToss, .NightShade => side.stored().level,
1092
1181
  .SonicBoom => 20,
1093
1182
  .DragonRage => 40,
1094
1183
  // GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
1095
1184
  .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
- }
1185
+ const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
1186
+ // GLITCH: Psywave infinite glitch loop
1187
+ if (!showdown and max <= 1) return Result.Error;
1188
+ break :power try Rolls.psywave(battle, player, max, options);
1107
1189
  },
1108
1190
  else => unreachable,
1109
1191
  };
1110
1192
 
1111
1193
  if (battle.last_damage == 0) return if (showdown) null else Result.Error;
1112
1194
 
1113
- _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
1114
- try buildRage(battle, player.foe(), log);
1195
+ const sub = showdown and foe.active.volatiles.Substitute;
1196
+ _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
1197
+ if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), options);
1115
1198
 
1116
1199
  return null;
1117
1200
  }
1118
1201
 
1119
- fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
1202
+ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
1120
1203
  const foe = battle.foe(player);
1121
1204
 
1122
1205
  if (battle.last_damage == 0) {
1123
- try log.fail(battle.active(player), .None);
1206
+ try options.log.fail(.{ battle.active(player), .None });
1124
1207
  return null;
1125
1208
  }
1126
1209
 
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);
1210
+ // Pretend Splash was used as a stand-in when no move has been used to fail below with 0 BP
1130
1211
  const foe_last_selected_move =
1131
1212
  Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
1132
- .Counter
1213
+ .Splash
1133
1214
  else
1134
1215
  foe.last_selected_move);
1135
1216
 
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
-
1217
+ const used = battle.lastMove(player.foe()).counterable != 0;
1141
1218
  const selected = foe_last_selected_move.bp > 0 and
1142
1219
  foe.last_selected_move != .Counter and
1143
1220
  (foe_last_selected_move.type == .Normal or
1144
1221
  foe_last_selected_move.type == .Fighting);
1145
1222
 
1146
1223
  if (!used and !selected) {
1147
- try log.fail(battle.active(player), .None);
1224
+ try options.log.fail(.{ battle.active(player), .None });
1148
1225
  return null;
1149
1226
  }
1150
1227
 
1151
1228
  if (!used or !selected) {
1152
1229
  // GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
1153
1230
  if (!showdown) return Result.Error;
1154
- try log.fail(battle.active(player), .None);
1231
+ try options.log.fail(.{ battle.active(player), .None });
1155
1232
  return null;
1156
1233
  }
1157
1234
 
1158
- battle.last_damage = if (battle.last_damage > 0x7FFF) 0xFFFF else battle.last_damage * 2;
1235
+ battle.last_damage *|= 2;
1159
1236
 
1160
1237
  // Pokémon Showdown calls checkHit before Counter
1161
- if (!showdown and !try checkHit(battle, player, move, log)) return null;
1238
+ if (!showdown and !try checkHit(battle, player, move, options)) return null;
1239
+ try options.chance.commit(player, .hit);
1162
1240
 
1163
- _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
1241
+ const sub = showdown and foe.active.volatiles.Substitute;
1242
+ _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
1243
+ if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), options);
1164
1244
  return null;
1165
1245
  }
1166
1246
 
@@ -1169,7 +1249,7 @@ fn applyDamage(
1169
1249
  target_player: Player,
1170
1250
  sub_player: Player,
1171
1251
  reason: Damage,
1172
- log: anytype,
1252
+ options: anytype,
1173
1253
  ) !bool {
1174
1254
  assert(showdown or battle.last_damage != 0);
1175
1255
 
@@ -1184,19 +1264,20 @@ fn applyDamage(
1184
1264
  subbed.active.volatiles.substitute = 0;
1185
1265
  subbed.active.volatiles.Substitute = false;
1186
1266
  // battle.last_damage is not updated with the amount of HP the Substitute had
1187
- try log.end(battle.active(sub_player), .Substitute);
1267
+ try options.log.end(.{ battle.active(sub_player), .Substitute });
1268
+ options.calc.capped(target_player);
1188
1269
  return true;
1189
1270
  } else {
1190
1271
  // 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);
1272
+ subbed.active.volatiles.substitute -= @intCast(battle.last_damage);
1273
+ try options.log.activate(.{ battle.active(sub_player), .Substitute });
1193
1274
  return false;
1194
1275
  }
1195
1276
  }
1196
1277
 
1197
1278
  if (battle.last_damage > target.stored().hp) battle.last_damage = target.stored().hp;
1198
1279
  target.stored().hp -= battle.last_damage;
1199
- try log.damage(battle.active(target_player), target.stored(), reason);
1280
+ try options.log.damage(.{ battle.active(target_player), target.stored(), reason });
1200
1281
  return false;
1201
1282
  }
1202
1283
 
@@ -1204,73 +1285,78 @@ fn mirrorMove(
1204
1285
  battle: anytype,
1205
1286
  player: Player,
1206
1287
  mslot: u4,
1288
+ rewrap: bool,
1207
1289
  auto: bool,
1208
1290
  residual: *bool,
1209
- log: anytype,
1210
- ) !?Result {
1291
+ options: anytype,
1292
+ ) anyerror!?Result {
1211
1293
  var side = battle.side(player);
1212
1294
  const foe = battle.foe(player);
1213
1295
 
1214
1296
  side.last_selected_move = foe.last_used_move;
1215
1297
 
1216
1298
  if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
1217
- try log.fail(battle.active(player), .None);
1299
+ try options.log.fail(.{ battle.active(player), .None });
1218
1300
  return null;
1301
+ } else if (!showdown or foe.last_used_move != .Struggle) {
1302
+ incrementPP(side, mslot);
1219
1303
  }
1220
1304
 
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);
1305
+ if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
1306
+ return null;
1307
+ }
1308
+ return doMove(battle, player, mslot, rewrap, auto, residual, options);
1225
1309
  }
1226
1310
 
1227
1311
  fn metronome(
1228
1312
  battle: anytype,
1229
1313
  player: Player,
1230
1314
  mslot: u4,
1315
+ rewrap: bool,
1231
1316
  auto: bool,
1232
1317
  residual: *bool,
1233
- log: anytype,
1234
- ) !?Result {
1318
+ options: anytype,
1319
+ ) anyerror!?Result {
1235
1320
  var side = battle.side(player);
1236
1321
 
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
-
1322
+ side.last_selected_move = try Rolls.metronome(battle, player, options);
1250
1323
  incrementPP(side, mslot);
1251
1324
 
1252
- if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
1253
- return doMove(battle, player, mslot, auto, residual, log);
1325
+ if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
1326
+ return null;
1327
+ }
1328
+ return doMove(battle, player, mslot, rewrap, auto, residual, options);
1254
1329
  }
1255
1330
 
1256
- fn checkHit(battle: anytype, player: Player, move: Move.Data, log: anytype) !bool {
1331
+ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !bool {
1257
1332
  var immune = false;
1258
1333
  var mist = false;
1259
- if (moveHit(battle, player, move, &immune, &mist)) return true;
1334
+
1335
+ if (moveHit(battle, player, move, &immune, &mist, options)) return true;
1336
+
1260
1337
  assert(!immune);
1261
1338
  if (mist) {
1262
1339
  assert(!showdown);
1263
1340
  const foe_ident = battle.active(player.foe());
1264
- try log.activate(foe_ident, .Mist);
1265
- try log.fail(foe_ident, .None);
1341
+ try options.log.activate(.{ foe_ident, .Mist });
1342
+ try options.log.fail(.{ foe_ident, .None });
1266
1343
  } else {
1267
- try log.lastmiss();
1268
- try log.miss(battle.active(player));
1344
+ try options.chance.commit(player, .miss);
1345
+ try options.log.lastmiss(.{});
1346
+ try options.log.miss(.{battle.active(player)});
1269
1347
  }
1348
+
1270
1349
  return false;
1271
1350
  }
1272
1351
 
1273
- fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist: *bool) bool {
1352
+ fn moveHit(
1353
+ battle: anytype,
1354
+ player: Player,
1355
+ move: Move.Data,
1356
+ immune: *bool,
1357
+ mist: *bool,
1358
+ options: anytype,
1359
+ ) bool {
1274
1360
  var side = battle.side(player);
1275
1361
  const foe = battle.foe(player);
1276
1362
 
@@ -1304,23 +1390,24 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
1304
1390
  }
1305
1391
 
1306
1392
  // 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)];
1393
+ const state = side.active.volatiles.state;
1394
+ var overwrite = move.effect == .Rage or move.effect == .Thrashing;
1395
+ const overwritten = overwrite and state > 0;
1396
+ assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
1397
+
1398
+ var accuracy: u16 = if (overwritten) state else move.accuracy;
1399
+ var boost = BOOSTS[@as(u4, @intCast(@as(i8, side.active.boosts.accuracy) + 6))];
1312
1400
  accuracy = accuracy * boost[0] / boost[1];
1313
- boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
1401
+ boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
1314
1402
  accuracy = accuracy * boost[0] / boost[1];
1315
1403
  accuracy = @min(255, @max(1, accuracy));
1316
1404
 
1317
- if (move.effect == .Thrashing or move.effect == .Rage) state.* = accuracy;
1405
+ // Pokémon Showdown only overwrites if the volatile is present
1406
+ if (showdown) overwrite = side.active.volatiles.Rage or side.active.volatiles.Thrashing;
1407
+ if (overwrite) side.active.volatiles.state = accuracy;
1318
1408
 
1319
1409
  // 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;
1410
+ break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
1324
1411
  };
1325
1412
 
1326
1413
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1330,7 +1417,7 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
1330
1417
  }
1331
1418
 
1332
1419
  if (!miss) return true;
1333
- battle.last_damage = 0;
1420
+ if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
1334
1421
  side.active.volatiles.Binding = false;
1335
1422
  return false;
1336
1423
  }
@@ -1338,8 +1425,8 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
1338
1425
  fn checkFaint(
1339
1426
  battle: anytype,
1340
1427
  player: Player,
1341
- log: anytype,
1342
- ) @TypeOf(log).Error!?Result {
1428
+ options: anytype,
1429
+ ) @TypeOf(options.log).Error!?Result {
1343
1430
  const side = battle.side(player);
1344
1431
  if (side.stored().hp > 0) return null;
1345
1432
 
@@ -1347,33 +1434,32 @@ fn checkFaint(
1347
1434
  const foe_fainted = foe.stored().hp == 0;
1348
1435
 
1349
1436
  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;
1437
+ const foe_out = foe_fainted and findFirstAlive(foe) == 0;
1438
+ const more = player_out or foe_out;
1353
1439
 
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;
1440
+ if (try faint(battle, player, !(more or foe_fainted), options)) |r| return r;
1441
+ if (foe_fainted) if (try faint(battle, player.foe(), !more, options)) |r| return r;
1356
1442
 
1357
1443
  assert(!side.active.volatiles.MultiHit);
1358
1444
  assert(!foe.active.volatiles.MultiHit);
1359
1445
 
1360
- if (tie) {
1361
- try log.tie();
1446
+ if (player_out and foe_out) {
1447
+ try options.log.tie(.{});
1362
1448
  return Result.Tie;
1363
1449
  } else if (player_out) {
1364
- try log.win(player.foe());
1450
+ try options.log.win(.{player.foe()});
1365
1451
  return if (player == .P1) Result.Lose else Result.Win;
1366
1452
  } else if (foe_out) {
1367
- try log.win(player);
1453
+ try options.log.win(.{player});
1368
1454
  return if (player == .P1) Result.Win else Result.Lose;
1369
1455
  }
1370
1456
 
1371
1457
  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 };
1458
+ if (player == .P1) return .{ .p1 = .Switch, .p2 = foe_choice };
1459
+ return .{ .p1 = foe_choice, .p2 = .Switch };
1374
1460
  }
1375
1461
 
1376
- fn faint(battle: anytype, player: Player, log: anytype, done: bool) !?Result {
1462
+ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
1377
1463
  var side = battle.side(player);
1378
1464
  var foe = battle.foe(player);
1379
1465
  assert(side.stored().hp == 0);
@@ -1406,35 +1492,43 @@ fn faint(battle: anytype, player: Player, log: anytype, done: bool) !?Result {
1406
1492
  side.stored().stats.spe;
1407
1493
  }
1408
1494
 
1409
- try log.faint(battle.active(player), done);
1495
+ try options.log.faint(.{ battle.active(player), done });
1496
+ options.calc.capped(player);
1410
1497
  return null;
1411
1498
  }
1412
1499
 
1413
- fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1500
+ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1414
1501
  var side = battle.side(player);
1415
1502
  var stored = side.stored();
1416
1503
  const ident = battle.active(player);
1417
1504
  var volatiles = &side.active.volatiles;
1418
1505
 
1419
1506
  const brn = Status.is(stored.status, .BRN);
1420
- if (brn or Status.is(stored.status, .PSN)) blk: {
1507
+ if (brn or Status.is(stored.status, .PSN)) {
1421
1508
  var damage = @max(stored.stats.hp / 16, 1);
1422
-
1423
1509
  if (volatiles.Toxic) {
1424
1510
  volatiles.toxic += 1;
1425
1511
  damage *= volatiles.toxic;
1426
1512
  }
1513
+ damage = @min(damage, stored.hp);
1427
1514
 
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);
1515
+ if (!showdown or damage > 0) {
1516
+ stored.hp -= damage;
1517
+ // Pokémon Showdown uses damage [of] here but its not relevant in Generation I
1518
+ try options.log.damage(.{ ident, stored, if (brn) Damage.Burn else Damage.Poison });
1519
+ }
1435
1520
  }
1436
1521
 
1437
1522
  if (volatiles.LeechSeed) {
1523
+ var foe = battle.foe(player);
1524
+ var foe_stored = foe.stored();
1525
+ const foe_ident = battle.active(player.foe());
1526
+
1527
+ if (foe_stored.hp == 0) {
1528
+ assert(showdown);
1529
+ return;
1530
+ }
1531
+
1438
1532
  var damage = @max(stored.stats.hp / 16, 1);
1439
1533
 
1440
1534
  // GLITCH: Leech Seed + Toxic glitch
@@ -1446,47 +1540,43 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1446
1540
  const amount = @min(damage, stored.hp);
1447
1541
  stored.hp -= amount;
1448
1542
 
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);
1543
+ // As above, Pokémon Showdown uses damage [of] but its not relevant
1544
+ if (amount > 0) try options.log.damage(.{ ident, stored, .LeechSeed });
1455
1545
 
1456
1546
  const before = foe_stored.hp;
1457
1547
  // Uncapped damage is added back to the foe
1458
1548
  foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
1459
1549
  // 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);
1550
+ if (foe_stored.hp > before) try options.log.heal(.{ foe_ident, foe_stored, .Silent });
1461
1551
  }
1462
1552
  }
1463
1553
 
1464
- fn endTurn(battle: anytype, log: anytype) @TypeOf(log).Error!Result {
1554
+ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result {
1465
1555
  assert(!battle.side(.P1).active.volatiles.MultiHit);
1466
1556
  assert(!battle.side(.P2).active.volatiles.MultiHit);
1467
1557
 
1468
- if (showdown and options.ebc and checkEBC(battle)) {
1469
- try log.tie();
1558
+ battle.turn += 1;
1559
+
1560
+ if (showdown and pkmn.options.ebc and checkEBC(battle)) {
1561
+ try options.log.tie(.{});
1470
1562
  return Result.Tie;
1471
1563
  }
1472
1564
 
1473
- battle.turn += 1;
1474
-
1475
1565
  if (showdown and battle.turn >= 1000) {
1476
- try log.tie();
1566
+ try options.log.tie(.{});
1477
1567
  return Result.Tie;
1478
1568
  } else if (battle.turn >= 65535) {
1479
1569
  return Result.Error;
1480
1570
  }
1481
1571
 
1482
- try log.turn(battle.turn);
1572
+ try options.log.turn(.{battle.turn});
1483
1573
 
1484
1574
  return Result.Default;
1485
1575
  }
1486
1576
 
1487
1577
  fn checkEBC(battle: anytype) bool {
1488
1578
  for (battle.sides, 0..) |side, i| {
1489
- const foe = battle.sides[~@intCast(u1, i)];
1579
+ const foe = battle.sides[~@as(u1, @intCast(i))];
1490
1580
 
1491
1581
  var foe_all_ghosts = true;
1492
1582
  var foe_all_transform = true;
@@ -1538,105 +1628,135 @@ fn checkEBC(battle: anytype) bool {
1538
1628
  return false;
1539
1629
  }
1540
1630
 
1541
- inline fn onBegin(
1631
+ fn disabled(side: *Side, ident: ID, options: anytype) !bool {
1632
+ if (side.active.volatiles.disable_move != 0) {
1633
+ // A Pokémon that transforms after being disabled may end up with less move slots
1634
+ const m = side.active.moves[side.active.volatiles.disable_move - 1].id;
1635
+ // side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
1636
+ const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
1637
+ m
1638
+ else
1639
+ side.last_selected_move;
1640
+ if (m != .None and m == last) {
1641
+ side.active.volatiles.Charging = false;
1642
+ try options.log.cant(.{ ident, .Disable, last });
1643
+ return true;
1644
+ }
1645
+ }
1646
+ return false;
1647
+ }
1648
+
1649
+ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1650
+ const side = battle.side(who);
1651
+ if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
1652
+ try Effects.boost(battle, who, Move.get(.Rage), options);
1653
+ }
1654
+ }
1655
+
1656
+ fn handleThrashing(battle: anytype, active: *ActivePokemon, player: Player, options: anytype) bool {
1657
+ var volatiles = &active.volatiles;
1658
+ assert(volatiles.Thrashing);
1659
+ if (volatiles.attacks > 0) return false;
1660
+
1661
+ volatiles.Thrashing = false;
1662
+ volatiles.Confusion = true;
1663
+ volatiles.confusion = Rolls.confusionDuration(battle, player, true, options);
1664
+ return true;
1665
+ }
1666
+
1667
+ fn onBegin(
1542
1668
  battle: anytype,
1543
1669
  player: Player,
1544
1670
  move: Move.Data,
1545
1671
  mslot: u8,
1546
1672
  residual: *bool,
1547
- log: anytype,
1673
+ options: anytype,
1548
1674
  ) !void {
1549
- assert(move.effect.onBegin());
1550
1675
  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),
1676
+ .Confusion => Effects.confusion(battle, player, move, options),
1677
+ .Conversion => Effects.conversion(battle, player, options),
1678
+ .FocusEnergy => Effects.focusEnergy(battle, player, options),
1679
+ .Haze => Effects.haze(battle, player, options),
1680
+ .Heal => Effects.heal(battle, player, options),
1681
+ .LeechSeed => Effects.leechSeed(battle, player, move, options),
1682
+ .LightScreen => Effects.lightScreen(battle, player, options),
1683
+ .Mimic => Effects.mimic(battle, player, move, mslot, options),
1684
+ .Mist => Effects.mist(battle, player, options),
1685
+ .Paralyze => Effects.paralyze(battle, player, move, options),
1686
+ .Poison => Effects.poison(battle, player, move, options),
1687
+ .Reflect => Effects.reflect(battle, player, options),
1688
+ .Splash => Effects.splash(battle, player, options),
1689
+ .Substitute => Effects.substitute(battle, player, residual, options),
1690
+ .SwitchAndTeleport => Effects.switchAndTeleport(battle, player, move, options),
1691
+ .Transform => Effects.transform(battle, player, options),
1567
1692
  else => unreachable,
1568
1693
  };
1569
1694
  }
1570
1695
 
1571
- inline fn onEnd(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
1572
- assert(move.effect.onEnd());
1696
+ fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1573
1697
  return switch (move.effect) {
1574
- .Bide => Effects.bide(battle, player, log),
1698
+ .Bide => Effects.bide(battle, player, options),
1575
1699
  // zig fmt: off
1576
1700
  .AttackUp1, .AttackUp2, .DefenseUp1, .DefenseUp2,
1577
1701
  .EvasionUp1, .SpecialUp1, .SpecialUp2, .SpeedUp2 =>
1578
- Effects.boost(battle, player, move, log),
1702
+ Effects.boost(battle, player, move, options),
1579
1703
  .AccuracyDown1, .AttackDown1, .DefenseDown1, .DefenseDown2, .SpeedDown1 =>
1580
- Effects.unboost(battle, player, move, log),
1704
+ Effects.unboost(battle, player, move, options),
1581
1705
  // zig fmt: on
1582
- .Sleep => Effects.sleep(battle, player, move, log),
1706
+ .Sleep => Effects.sleep(battle, player, move, options),
1583
1707
  else => unreachable,
1584
1708
  };
1585
1709
  }
1586
1710
 
1587
- inline fn alwaysHappens(
1711
+ fn alwaysHappens(
1588
1712
  battle: anytype,
1589
1713
  player: Player,
1590
1714
  move: Move.Data,
1591
1715
  residual: *bool,
1592
- log: anytype,
1716
+ options: anytype,
1593
1717
  ) !void {
1594
- assert(move.effect.alwaysHappens());
1595
1718
  return switch (move.effect) {
1596
- .DrainHP, .DreamEater => Effects.drainHP(battle, player, log),
1719
+ .DrainHP, .DreamEater => Effects.drainHP(battle, player, options),
1597
1720
  .Explode => Effects.explode(battle, player),
1598
- .PayDay => Effects.payDay(battle, player, log),
1721
+ .PayDay => Effects.payDay(battle, player, options),
1599
1722
  .Rage => Effects.rage(battle, player),
1600
- .Recoil => Effects.recoil(battle, player, residual, log),
1723
+ .Recoil => Effects.recoil(battle, player, residual, options),
1601
1724
  .JumpKick, .Binding => {},
1602
1725
  else => unreachable,
1603
1726
  };
1604
1727
  }
1605
1728
 
1606
- inline fn moveEffect(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
1729
+ fn moveEffect(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1607
1730
  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),
1731
+ .BurnChance1, .BurnChance2 => Effects.burnChance(battle, player, move, options),
1732
+ .ConfusionChance => Effects.confusion(battle, player, move, options),
1733
+ .FlinchChance1, .FlinchChance2 => Effects.flinchChance(battle, player, move, options),
1734
+ .FreezeChance => Effects.freezeChance(battle, player, move, options),
1735
+ .HyperBeam => Effects.hyperBeam(battle, player, options),
1613
1736
  .MultiHit, .DoubleHit, .Twineedle => unreachable,
1614
- .ParalyzeChance1, .ParalyzeChance2 => Effects.paralyzeChance(battle, player, move, log),
1615
- .PoisonChance1, .PoisonChance2 => Effects.poison(battle, player, move, log),
1737
+ .ParalyzeChance1, .ParalyzeChance2 => Effects.paralyzeChance(battle, player, move, options),
1738
+ .PoisonChance1, .PoisonChance2 => Effects.poison(battle, player, move, options),
1616
1739
  // zig fmt: off
1617
1740
  .AttackDownChance, .DefenseDownChance, .SpecialDownChance, .SpeedDownChance =>
1618
- Effects.unboost(battle, player, move, log),
1741
+ Effects.unboost(battle, player, move, options),
1619
1742
  // zig fmt: on
1620
1743
  else => {},
1621
1744
  };
1622
1745
  }
1623
1746
 
1624
1747
  pub const Effects = struct {
1625
- fn bide(battle: anytype, player: Player, log: anytype) !void {
1748
+ fn bide(battle: anytype, player: Player, options: anytype) !void {
1626
1749
  var side = battle.side(player);
1627
1750
 
1628
1751
  side.active.volatiles.Bide = true;
1629
1752
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1630
1753
  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);
1754
+ side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
1635
1755
 
1636
- try log.start(battle.active(player), .Bide);
1756
+ try options.log.start(.{ battle.active(player), .Bide });
1637
1757
  }
1638
1758
 
1639
- fn burnChance(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
1759
+ fn burnChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1640
1760
  var foe = battle.foe(player);
1641
1761
  var foe_stored = foe.stored();
1642
1762
 
@@ -1647,96 +1767,92 @@ pub const Effects = struct {
1647
1767
  // GLITCH: Freeze top move selection desync can occur if thawed Pokémon is slower
1648
1768
  if (Status.is(foe_stored.status, .FRZ)) {
1649
1769
  assert(move.type == .Fire);
1650
- try log.curestatus(battle.active(player.foe()), foe_stored.status, .Message);
1770
+ try options.log.curestatus(.{
1771
+ battle.active(player.foe()),
1772
+ foe_stored.status,
1773
+ .Message,
1774
+ });
1651
1775
  foe_stored.status = 0;
1652
1776
  }
1653
1777
  return;
1654
1778
  }
1655
1779
 
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;
1780
+ if (foe.active.types.includes(move.type)) return;
1781
+ if (!try Rolls.secondaryChance(battle, player, move.effect == .BurnChance1, options)) {
1782
+ return;
1783
+ }
1664
1784
 
1665
1785
  foe_stored.status = Status.init(.BRN);
1666
1786
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
1667
1787
 
1668
- try log.status(battle.active(player.foe()), foe_stored.status, .None);
1788
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
1669
1789
  }
1670
1790
 
1671
- fn charge(battle: anytype, player: Player, log: anytype) !void {
1791
+ fn charge(battle: anytype, player: Player, options: anytype) !void {
1672
1792
  var side = battle.side(player);
1673
1793
  var volatiles = &side.active.volatiles;
1674
1794
 
1675
1795
  volatiles.Charging = true;
1676
1796
  const move = side.last_selected_move;
1677
1797
  if (move == .Fly or move == .Dig) volatiles.Invulnerable = true;
1678
- try log.laststill();
1679
- try log.prepare(battle.active(player), move);
1798
+ try options.log.laststill(.{});
1799
+ try options.log.prepare(.{ battle.active(player), move });
1680
1800
  }
1681
1801
 
1682
- fn confusion(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
1802
+ fn confusion(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1683
1803
  var foe = battle.foe(player);
1684
1804
  const sub = foe.active.volatiles.Substitute;
1685
1805
 
1686
1806
  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;
1807
+ if (!try Rolls.confusionChance(battle, player, options)) return;
1692
1808
  } else {
1693
1809
  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
- }
1810
+ if (!try checkHit(battle, player, move, options)) return;
1811
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1699
1812
  } 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
- }
1813
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1814
+ if (!try checkHit(battle, player, move, options)) return;
1705
1815
  }
1816
+ try options.chance.commit(player, .hit);
1706
1817
  }
1707
1818
 
1708
1819
  if (foe.active.volatiles.Confusion) return;
1709
1820
  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);
1821
+ foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
1714
1822
 
1715
- try log.start(battle.active(player.foe()), .Confusion);
1823
+ try options.log.start(.{ battle.active(player.foe()), .Confusion });
1716
1824
  }
1717
1825
 
1718
- fn conversion(battle: anytype, player: Player, log: anytype) !void {
1826
+ fn conversion(battle: anytype, player: Player, options: anytype) !void {
1719
1827
  const foe = battle.foe(player);
1720
1828
 
1721
1829
  if (foe.active.volatiles.Invulnerable) {
1722
- try log.lastmiss();
1723
- return log.miss(battle.active(player));
1830
+ try options.log.lastmiss(.{});
1831
+ return options.log.miss(.{battle.active(player)});
1724
1832
  }
1725
1833
 
1726
1834
  battle.side(player).active.types = foe.active.types;
1727
- return log.typechange(battle.active(player), foe.active.types, battle.active(player.foe()));
1835
+ return options.log.start(.{
1836
+ battle.active(player),
1837
+ .TypeChange,
1838
+ foe.active.types,
1839
+ battle.active(player.foe()),
1840
+ });
1728
1841
  }
1729
1842
 
1730
- fn disable(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
1843
+ fn disable(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
1731
1844
  var foe = battle.foe(player);
1732
1845
  var volatiles = &foe.active.volatiles;
1733
1846
  const foe_ident = battle.active(player.foe());
1734
1847
 
1735
1848
  // Pokémon Showdown handles hit/miss earlier in doMove
1736
- if (!showdown and !try checkHit(battle, player, move, log)) return null;
1849
+ if (!showdown) {
1850
+ if (!try checkHit(battle, player, move, options)) return null;
1851
+ try options.chance.commit(player, .hit);
1852
+ }
1737
1853
 
1738
- if (volatiles.disabled_move != 0) {
1739
- try log.fail(foe_ident, .None);
1854
+ if (volatiles.disable_move != 0) {
1855
+ try options.log.fail(.{ foe_ident, .None });
1740
1856
  return null;
1741
1857
  }
1742
1858
 
@@ -1752,7 +1868,7 @@ pub const Effects = struct {
1752
1868
  // Technically this is still considered simply a "miss" on the cartridge,
1753
1869
  // but diverging from Pokémon Showdown here would mostly just be pedantic
1754
1870
  if (n == 0) {
1755
- try log.fail(foe_ident, .None);
1871
+ try options.log.fail(.{ foe_ident, .None });
1756
1872
  return null;
1757
1873
  } else if (err) {
1758
1874
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
@@ -1760,23 +1876,22 @@ pub const Effects = struct {
1760
1876
  return Result.Error;
1761
1877
  }
1762
1878
 
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);
1879
+ volatiles.disable_move =
1880
+ @intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
1881
+ volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
1768
1882
 
1769
- try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
1883
+ const id = foe.active.move(volatiles.disable_move).id;
1884
+ try options.log.start(.{ foe_ident, .Disable, id });
1770
1885
  return null;
1771
1886
  }
1772
1887
 
1773
- fn drainHP(battle: anytype, player: Player, log: anytype) !void {
1774
- var side = battle.side(player);
1775
- var stored = side.stored();
1888
+ fn drainHP(battle: anytype, player: Player, options: anytype) !void {
1889
+ var stored = battle.side(player).stored();
1890
+ const ident = battle.active(player);
1776
1891
 
1777
1892
  if (battle.last_damage == 0) {
1778
1893
  assert(showdown);
1779
- return;
1894
+ if (!battle.foe(player).active.volatiles.Substitute) return;
1780
1895
  }
1781
1896
 
1782
1897
  const drain = @max(battle.last_damage / 2, 1);
@@ -1785,7 +1900,7 @@ pub const Effects = struct {
1785
1900
  if (stored.hp == stored.stats.hp) return;
1786
1901
  stored.hp = @min(stored.stats.hp, stored.hp + drain);
1787
1902
 
1788
- try log.drain(battle.active(player), stored, battle.active(player.foe()));
1903
+ try options.log.heal(.{ ident, stored, .Drain, battle.active(player.foe()) });
1789
1904
  }
1790
1905
 
1791
1906
  fn explode(battle: anytype, player: Player) !void {
@@ -1799,34 +1914,28 @@ pub const Effects = struct {
1799
1914
  side.active.volatiles.LeechSeed = false;
1800
1915
  }
1801
1916
 
1802
- fn flinchChance(battle: anytype, player: Player, move: Move.Data) void {
1917
+ fn flinchChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1803
1918
  var volatiles = &battle.foe(player).active.volatiles;
1804
1919
 
1805
1920
  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;
1921
+ if (!try Rolls.secondaryChance(battle, player, move.effect == .FlinchChance1, options)) {
1922
+ return;
1923
+ }
1815
1924
 
1816
1925
  volatiles.Flinch = true;
1817
1926
  volatiles.Recharging = false;
1818
1927
  }
1819
1928
 
1820
- fn focusEnergy(battle: anytype, player: Player, log: anytype) !void {
1929
+ fn focusEnergy(battle: anytype, player: Player, options: anytype) !void {
1821
1930
  var side = battle.side(player);
1822
1931
 
1823
1932
  if (side.active.volatiles.FocusEnergy) return;
1824
1933
  side.active.volatiles.FocusEnergy = true;
1825
1934
 
1826
- try log.start(battle.active(player), .FocusEnergy);
1935
+ try options.log.start(.{ battle.active(player), .FocusEnergy });
1827
1936
  }
1828
1937
 
1829
- fn freezeChance(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
1938
+ fn freezeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1830
1939
  var foe = battle.foe(player);
1831
1940
  var foe_stored = foe.stored();
1832
1941
  const foe_ident = battle.active(player.foe());
@@ -1837,26 +1946,23 @@ pub const Effects = struct {
1837
1946
  return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
1838
1947
  }
1839
1948
 
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
-
1949
+ if (foe.active.types.includes(move.type)) return;
1950
+ if (!try Rolls.secondaryChance(battle, player, true, options)) return;
1846
1951
  // Freeze Clause Mod
1847
1952
  if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1848
1953
 
1849
1954
  foe_stored.status = Status.init(.FRZ);
1850
1955
  // GLITCH: Hyper Beam recharging status is not cleared
1851
1956
 
1852
- try log.status(foe_ident, foe_stored.status, .None);
1957
+ try options.log.status(.{ foe_ident, foe_stored.status, .None });
1853
1958
  }
1854
1959
 
1855
- fn haze(battle: anytype, player: Player, log: anytype) !void {
1960
+ fn haze(battle: anytype, player: Player, options: anytype) !void {
1961
+ var log = options.log;
1856
1962
  var side = battle.side(player);
1857
1963
  var foe = battle.foe(player);
1858
1964
 
1859
- var side_stored = side.stored();
1965
+ const side_stored = side.stored();
1860
1966
  var foe_stored = foe.stored();
1861
1967
 
1862
1968
  const player_ident = battle.active(player);
@@ -1868,96 +1974,100 @@ pub const Effects = struct {
1868
1974
  side.active.stats = side_stored.stats;
1869
1975
  foe.active.stats = foe_stored.stats;
1870
1976
 
1871
- try log.activate(player_ident, .Haze);
1872
- try log.clearallboost();
1977
+ try log.activate(.{ player_ident, .Haze });
1978
+ try log.clearallboost(.{});
1873
1979
 
1874
1980
  // Pokémon Showdown clears P1 then P2 instead of status -> side -> foe
1875
1981
  if (showdown) {
1876
1982
  for (&battle.sides, 0..) |*s, i| {
1877
- const p = @intToEnum(Player, i);
1983
+ const p: Player = @enumFromInt(i);
1878
1984
  // Pokémon Showdown incorrectly does not prevent sleep/freeze from moving
1879
1985
  if (p != player and Status.any(s.stored().status)) {
1880
- try log.curestatus(foe_ident, foe_stored.status, .Silent);
1986
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
1881
1987
  s.stored().status = 0;
1882
1988
  } else if (showdown and s.stored().status == Status.TOX) {
1883
1989
  s.stored().status = Status.init(.PSN);
1884
- try log.status(battle.active(p), s.stored().status, .None);
1990
+ try log.status(.{ battle.active(p), s.stored().status, .None });
1885
1991
  }
1886
- try clearVolatiles(battle, p, log);
1992
+ try clearVolatiles(battle, p, p != player, options);
1887
1993
  }
1888
1994
  } else {
1889
1995
  if (Status.any(foe_stored.status)) {
1890
1996
  if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
1891
1997
  foe.last_selected_move = .SKIP_TURN;
1892
1998
  }
1893
- try log.curestatus(foe_ident, foe_stored.status, .Silent);
1999
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
1894
2000
  foe_stored.status = 0;
1895
2001
  }
1896
- try clearVolatiles(battle, player, log);
1897
- try clearVolatiles(battle, player.foe(), log);
2002
+ try clearVolatiles(battle, player, false, options);
2003
+ try clearVolatiles(battle, player.foe(), true, options);
1898
2004
  }
1899
2005
  }
1900
2006
 
1901
- fn heal(battle: anytype, player: Player, log: anytype) !void {
2007
+ fn heal(battle: anytype, player: Player, options: anytype) !void {
1902
2008
  var side = battle.side(player);
1903
2009
  var stored = side.stored();
1904
2010
  const ident = battle.active(player);
1905
2011
 
1906
2012
  // GLITCH: HP recovery move failure glitches
1907
2013
  const delta = stored.stats.hp - stored.hp;
1908
- if (delta == 0 or delta & 255 == 255) return try log.fail(ident, .None);
2014
+ if (delta == 0 or (delta & 255 == 255 and stored.hp % 256 != 0)) {
2015
+ return options.log.fail(.{ ident, .None });
2016
+ }
1909
2017
 
1910
2018
  const rest = side.last_selected_move == .Rest;
1911
2019
  if (rest) {
1912
2020
  // Adding the sleep status runs the sleep condition handler to roll duration
1913
2021
  if (showdown) battle.rng.advance(1);
1914
2022
  stored.status = Status.slf(2);
1915
- try log.statusFrom(ident, stored.status, Move.Rest);
2023
+ try options.log.status(.{ ident, stored.status, .From, Move.Rest });
1916
2024
  stored.hp = stored.stats.hp;
1917
2025
  } else {
1918
2026
  stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
1919
2027
  }
1920
- try log.heal(ident, stored, if (rest) Heal.Silent else Heal.None);
2028
+ try options.log.heal(.{ ident, stored, if (rest) Heal.Silent else Heal.None });
1921
2029
  }
1922
2030
 
1923
- fn hyperBeam(battle: anytype, player: Player, log: anytype) !void {
2031
+ fn hyperBeam(battle: anytype, player: Player, options: anytype) !void {
1924
2032
  battle.side(player).active.volatiles.Recharging = true;
1925
- try log.mustrecharge(battle.active(player));
2033
+ try options.log.mustrecharge(.{battle.active(player)});
1926
2034
  }
1927
2035
 
1928
- fn leechSeed(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
2036
+ fn leechSeed(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1929
2037
  var foe = battle.foe(player);
1930
2038
 
1931
2039
  if (showdown) {
1932
2040
  // Invulnerability trumps type immunity on Pokémon Showdown
1933
2041
  if (!foe.active.volatiles.Invulnerable and foe.active.types.includes(.Grass)) {
1934
- return log.immune(battle.active(player.foe()), .None);
2042
+ return options.log.immune(.{ battle.active(player.foe()), .None });
1935
2043
  }
1936
- if (!try checkHit(battle, player, move, log)) return;
2044
+ if (!try checkHit(battle, player, move, options)) return;
1937
2045
  if (foe.active.volatiles.LeechSeed) return;
1938
2046
  } else {
1939
- if (!try checkHit(battle, player, move, log)) return;
2047
+ if (!try checkHit(battle, player, move, options)) return;
1940
2048
  if (foe.active.types.includes(.Grass) or foe.active.volatiles.LeechSeed) {
1941
- try log.lastmiss();
1942
- return log.miss(battle.active(player));
2049
+ try options.log.lastmiss(.{});
2050
+ return options.log.miss(.{battle.active(player)});
1943
2051
  }
1944
2052
  }
1945
2053
 
2054
+ try options.chance.commit(player, .hit);
1946
2055
  foe.active.volatiles.LeechSeed = true;
1947
2056
 
1948
- try log.start(battle.active(player.foe()), .LeechSeed);
2057
+ try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
1949
2058
  }
1950
2059
 
1951
- fn lightScreen(battle: anytype, player: Player, log: anytype) !void {
2060
+ fn lightScreen(battle: anytype, player: Player, options: anytype) !void {
1952
2061
  var side = battle.side(player);
2062
+ const ident = battle.active(player);
1953
2063
 
1954
- if (side.active.volatiles.LightScreen) return log.fail(battle.active(player), .None);
2064
+ if (side.active.volatiles.LightScreen) return options.log.fail(.{ ident, .None });
1955
2065
  side.active.volatiles.LightScreen = true;
1956
2066
 
1957
- try log.start(battle.active(player), .LightScreen);
2067
+ try options.log.start(.{ ident, .LightScreen });
1958
2068
  }
1959
2069
 
1960
- fn mimic(battle: anytype, player: Player, move: Move.Data, mslot: u8, log: anytype) !void {
2070
+ fn mimic(battle: anytype, player: Player, move: Move.Data, mslot: u8, options: anytype) !void {
1961
2071
  var side = battle.side(player);
1962
2072
  var foe = battle.foe(player);
1963
2073
 
@@ -1973,45 +2083,51 @@ pub const Effects = struct {
1973
2083
  const has_mimic = has_mimic: {
1974
2084
  for (side.active.moves, 0..) |m, i| {
1975
2085
  if (m.id == .Mimic) {
1976
- oslot = @intCast(u8, i + 1);
2086
+ oslot = @intCast(i + 1);
1977
2087
  break :has_mimic true;
1978
2088
  }
1979
2089
  }
1980
2090
  break :has_mimic false;
1981
2091
  };
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);
2092
+ if (!has_mimic) {
2093
+ // Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
2094
+ if (!try checkHit(battle, player, move, options)) return;
2095
+ try options.chance.commit(player, .hit);
2096
+ return options.log.fail(.{ battle.active(player.foe()), .None });
1986
2097
  }
1987
2098
  }
1988
- if (!try checkHit(battle, player, move, log)) return;
2099
+ if (!try checkHit(battle, player, move, options)) return;
2100
+ try options.chance.commit(player, .hit);
1989
2101
 
1990
- const rslot = randomMoveSlot(&battle.rng, &foe.active.moves, 0);
2102
+ const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
1991
2103
  side.active.move(oslot).id = foe.active.move(rslot).id;
1992
2104
 
1993
- try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
2105
+ try options.log.start(.{ battle.active(player), .Mimic, side.active.move(oslot).id });
1994
2106
  }
1995
2107
 
1996
- fn mist(battle: anytype, player: Player, log: anytype) !void {
2108
+ fn mist(battle: anytype, player: Player, options: anytype) !void {
1997
2109
  var side = battle.side(player);
1998
2110
 
1999
2111
  if (side.active.volatiles.Mist) return;
2000
2112
  side.active.volatiles.Mist = true;
2001
2113
 
2002
- try log.start(battle.active(player), .Mist);
2114
+ try options.log.start(.{ battle.active(player), .Mist });
2003
2115
  }
2004
2116
 
2005
- fn multiHit(battle: anytype, player: Player, move: Move.Data) void {
2117
+ fn multiHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
2006
2118
  var side = battle.side(player);
2007
2119
 
2008
2120
  assert(!side.active.volatiles.MultiHit);
2009
2121
  side.active.volatiles.MultiHit = true;
2010
2122
 
2011
- side.active.volatiles.attacks = if (move.effect == .MultiHit) distribution(battle) else 2;
2123
+ side.active.volatiles.attacks = if (move.effect == .MultiHit)
2124
+ try Rolls.distribution(battle, .MultiHit, player, options)
2125
+ else
2126
+ 2;
2012
2127
  }
2013
2128
 
2014
- fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
2129
+ fn paralyze(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
2130
+ var log = options.log;
2015
2131
  var foe = battle.foe(player);
2016
2132
  var foe_stored = foe.stored();
2017
2133
  const foe_ident = battle.active(player.foe());
@@ -2021,27 +2137,29 @@ pub const Effects = struct {
2021
2137
 
2022
2138
  if (showdown) {
2023
2139
  // 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;
2140
+ if (immune and !foe.active.volatiles.Invulnerable) {
2141
+ return log.immune(.{ foe_ident, .None });
2142
+ } else if (!try checkHit(battle, player, move, options)) return;
2026
2143
  }
2027
2144
  if (Status.any(foe_stored.status)) {
2028
- return log.fail(
2145
+ return log.fail(.{
2029
2146
  foe_ident,
2030
- if (Status.is(foe_stored.status, .PAR)) .Paralysis else .None,
2031
- );
2147
+ if (Status.is(foe_stored.status, .PAR)) Fail.Paralysis else Fail.None,
2148
+ });
2032
2149
  }
2033
2150
  if (!showdown) {
2034
- if (immune) return log.immune(foe_ident, .None);
2035
- if (!try checkHit(battle, player, move, log)) return;
2151
+ if (immune) return log.immune(.{ foe_ident, .None });
2152
+ if (!try checkHit(battle, player, move, options)) return;
2036
2153
  }
2154
+ try options.chance.commit(player, .hit);
2037
2155
 
2038
2156
  foe_stored.status = Status.init(.PAR);
2039
2157
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2040
2158
 
2041
- try log.status(foe_ident, foe_stored.status, .None);
2159
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2042
2160
  }
2043
2161
 
2044
- fn paralyzeChance(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
2162
+ fn paralyzeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
2045
2163
  var foe = battle.foe(player);
2046
2164
  var foe_stored = foe.stored();
2047
2165
 
@@ -2052,65 +2170,60 @@ pub const Effects = struct {
2052
2170
  }
2053
2171
 
2054
2172
  // 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;
2173
+ if (foe.active.types.includes(move.type)) return;
2174
+ if (!try Rolls.secondaryChance(battle, player, move.effect == .ParalyzeChance1, options)) {
2175
+ return;
2176
+ }
2063
2177
 
2064
2178
  foe_stored.status = Status.init(.PAR);
2065
2179
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2066
2180
 
2067
- try log.status(battle.active(player.foe()), foe_stored.status, .None);
2181
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
2068
2182
  }
2069
2183
 
2070
- fn payDay(battle: anytype, player: Player, log: anytype) !void {
2071
- if (!showdown or !battle.foe(player).active.volatiles.Substitute) try log.fieldactivate();
2184
+ fn payDay(battle: anytype, player: Player, options: anytype) !void {
2185
+ if (!showdown or !battle.foe(player).active.volatiles.Substitute) {
2186
+ try options.log.fieldactivate(.{});
2187
+ }
2072
2188
  }
2073
2189
 
2074
- fn poison(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
2190
+ fn poison(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
2191
+ var log = options.log;
2075
2192
  var foe = battle.foe(player);
2076
2193
  var foe_stored = foe.stored();
2077
2194
  const foe_ident = battle.active(player.foe());
2078
2195
  const toxic = battle.side(player).last_selected_move == .Toxic;
2079
2196
 
2080
- if (showdown and move.effect == .Poison and !try checkHit(battle, player, move, log)) {
2197
+ if (showdown and move.effect == .Poison and !try checkHit(battle, player, move, options)) {
2081
2198
  return;
2082
2199
  } else if (foe.active.volatiles.Substitute) {
2083
2200
  if (move.effect != .Poison) return;
2084
- return log.fail(foe_ident, .None);
2201
+ return log.fail(.{ foe_ident, .None });
2085
2202
  } else if (Status.any(foe_stored.status)) {
2086
2203
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2087
2204
  // Pokémon Showdown considers Toxic to be a status even in Generation I and so
2088
2205
  // 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))
2206
+ return log.fail(.{ foe_ident, if (Status.is(foe_stored.status, .PSN))
2090
2207
  if (!showdown)
2091
- .Poison
2208
+ Fail.Poison
2092
2209
  else if (toxic == (foe_stored.status == Status.TOX))
2093
- if (toxic) .Toxic else .Poison
2210
+ if (toxic) Fail.Toxic else Fail.Poison
2094
2211
  else
2095
- .None
2212
+ Fail.None
2096
2213
  else
2097
- .None);
2214
+ Fail.None });
2098
2215
  } else if (foe.active.types.includes(.Poison)) {
2099
2216
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2100
- return log.immune(foe_ident, .None);
2217
+ return log.immune(.{ foe_ident, .None });
2101
2218
  }
2102
2219
 
2103
2220
  if (move.effect == .Poison) {
2104
- if (!showdown and !try checkHit(battle, player, move, log)) return;
2221
+ if (!showdown and !try checkHit(battle, player, move, options)) return;
2222
+ try options.chance.commit(player, .hit);
2105
2223
  } 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;
2224
+ if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
2225
+ return;
2226
+ }
2114
2227
  }
2115
2228
 
2116
2229
  foe_stored.status = Status.init(.PSN);
@@ -2120,39 +2233,46 @@ pub const Effects = struct {
2120
2233
  foe.active.volatiles.toxic = 0;
2121
2234
  }
2122
2235
 
2123
- try log.status(foe_ident, foe_stored.status, .None);
2236
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2124
2237
  }
2125
2238
 
2126
2239
  fn rage(battle: anytype, player: Player) !void {
2127
2240
  var volatiles = &battle.side(player).active.volatiles;
2128
- if (volatiles.Rage) return;
2129
- volatiles.Rage = true;
2130
2241
  assert(!volatiles.Bide);
2131
- volatiles.state = 0;
2242
+ volatiles.Rage = true;
2132
2243
  }
2133
2244
 
2134
- fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
2245
+ fn recoil(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
2135
2246
  var side = battle.side(player);
2136
2247
  var stored = side.stored();
2137
2248
 
2138
- const damage = @intCast(i16, @max(battle.last_damage /
2249
+ assert(showdown or battle.last_damage > 0);
2250
+ if (showdown and battle.last_damage == 0) return;
2251
+
2252
+ const damage: i16 = @intCast(@max(battle.last_damage /
2139
2253
  @as(u8, if (side.last_selected_move == .Struggle) 2 else 4), 1));
2140
- stored.hp = @intCast(u16, @max(@intCast(i16, stored.hp) - damage, 0));
2254
+ stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
2141
2255
 
2142
- try log.damageOf(battle.active(player), stored, .RecoilOf, battle.active(player.foe()));
2256
+ try options.log.damage(.{
2257
+ battle.active(player),
2258
+ stored,
2259
+ .RecoilOf,
2260
+ battle.active(player.foe()),
2261
+ });
2143
2262
  if (showdown and stored.hp == 0) residual.* = false;
2144
2263
  }
2145
2264
 
2146
- fn reflect(battle: anytype, player: Player, log: anytype) !void {
2265
+ fn reflect(battle: anytype, player: Player, options: anytype) !void {
2147
2266
  var side = battle.side(player);
2267
+ const ident = battle.active(player);
2148
2268
 
2149
- if (side.active.volatiles.Reflect) return log.fail(battle.active(player), .None);
2269
+ if (side.active.volatiles.Reflect) return options.log.fail(.{ ident, .None });
2150
2270
  side.active.volatiles.Reflect = true;
2151
2271
 
2152
- try log.start(battle.active(player), .Reflect);
2272
+ try options.log.start(.{ ident, .Reflect });
2153
2273
  }
2154
2274
 
2155
- fn sleep(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
2275
+ fn sleep(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
2156
2276
  var foe = battle.foe(player);
2157
2277
  var foe_stored = foe.stored();
2158
2278
  const foe_ident = battle.active(player.foe());
@@ -2163,13 +2283,14 @@ pub const Effects = struct {
2163
2283
  // the Sleep Clause Mod might activate, causing us to not actually bypass
2164
2284
  } else {
2165
2285
  if (Status.any(foe_stored.status)) {
2166
- return log.fail(
2286
+ return options.log.fail(.{
2167
2287
  foe_ident,
2168
- if (Status.is(foe_stored.status, .SLP)) .Sleep else .None,
2169
- );
2288
+ if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
2289
+ });
2170
2290
  }
2171
2291
  // 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;
2292
+ if (!showdown and !try checkHit(battle, player, move, options)) return;
2293
+ try options.chance.commit(player, .hit);
2173
2294
  }
2174
2295
 
2175
2296
  // Sleep Clause Mod
@@ -2180,75 +2301,68 @@ pub const Effects = struct {
2180
2301
  }
2181
2302
  foe.active.volatiles.Recharging = false;
2182
2303
 
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);
2304
+ foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2305
+ const last = battle.side(player).last_selected_move;
2306
+ try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
2194
2307
  }
2195
2308
 
2196
- fn splash(battle: anytype, player: Player, log: anytype) !void {
2197
- try log.activate(battle.active(player), .Splash);
2309
+ fn splash(battle: anytype, player: Player, options: anytype) !void {
2310
+ try options.log.activate(.{ battle.active(player), .Splash });
2198
2311
  }
2199
2312
 
2200
- fn substitute(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
2313
+ fn substitute(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
2201
2314
  var side = battle.side(player);
2202
2315
  if (side.active.volatiles.Substitute) {
2203
- try log.fail(battle.active(player), .Substitute);
2204
- return;
2316
+ return options.log.fail(.{ battle.active(player), .Substitute });
2205
2317
  }
2206
2318
 
2207
2319
  assert(side.stored().stats.hp <= 1023);
2208
2320
  // 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);
2321
+ const hp: u8 = @intCast(side.stored().stats.hp / 4);
2210
2322
  // Pokénon Showdown incorrectly checks for 1/4 HP based on `target.maxhp / 4` which returns
2211
2323
  // a floating point value and thus only correctly implements the Substitute 1/4 glitch when
2212
2324
  // the target's HP is exactly divisible by 4 (here we're using an inlined divCeil routine to
2213
2325
  // avoid having to convert to floating point)
2214
2326
  const required_hp = if (showdown) @divFloor(side.stored().stats.hp - 1, 4) + 1 else hp;
2215
2327
  if (side.stored().hp < required_hp) {
2216
- try log.fail(battle.active(player), .Weak);
2217
- return;
2328
+ return options.log.fail(.{ battle.active(player), .Weak });
2218
2329
  }
2219
2330
 
2220
2331
  // GLITCH: can leave the user with 0 HP (faints later) because didn't check '<=' above
2221
2332
  side.stored().hp -= hp;
2222
2333
  side.active.volatiles.substitute = hp + 1;
2223
2334
  side.active.volatiles.Substitute = true;
2224
- try log.start(battle.active(player), .Substitute);
2335
+ try options.log.start(.{ battle.active(player), .Substitute });
2225
2336
  if (hp > 0) {
2226
- try log.damage(battle.active(player), side.stored(), .None);
2337
+ try options.log.damage(.{ battle.active(player), side.stored(), .None });
2227
2338
  if (showdown and side.stored().hp == 0) residual.* = false;
2228
2339
  }
2229
2340
  }
2230
2341
 
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);
2342
+ fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
2343
+ // Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
2344
+ if (showdown) {
2345
+ if (battle.side(player).last_selected_move == .Teleport) return;
2346
+ if (try checkHit(battle, player, move, options)) {
2347
+ try options.chance.commit(player, .hit);
2348
+ }
2349
+ battle.last_damage = 0;
2350
+ } else {
2351
+ try options.log.fail(.{ battle.active(player), .None });
2352
+ try options.log.laststill(.{});
2353
+ }
2236
2354
  }
2237
2355
 
2238
- fn thrashing(battle: anytype, player: Player) void {
2356
+ fn thrashing(battle: anytype, player: Player, options: anytype) void {
2239
2357
  var volatiles = &battle.side(player).active.volatiles;
2240
2358
  assert(!volatiles.Thrashing);
2241
2359
  assert(!volatiles.Bide);
2242
2360
 
2243
2361
  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);
2362
+ volatiles.attacks = Rolls.attackingDuration(battle, player, options);
2249
2363
  }
2250
2364
 
2251
- fn transform(battle: anytype, player: Player, log: anytype) !void {
2365
+ fn transform(battle: anytype, player: Player, options: anytype) !void {
2252
2366
  var side = battle.side(player);
2253
2367
  const foe = battle.foe(player);
2254
2368
  const foe_ident = battle.active(player.foe());
@@ -2274,10 +2388,10 @@ pub const Effects = struct {
2274
2388
  side.active.moves[i].pp = if (m.id != .None) 5 else 0;
2275
2389
  }
2276
2390
 
2277
- try log.transform(battle.active(player), foe_ident);
2391
+ try options.log.transform(.{ battle.active(player), foe_ident });
2278
2392
  }
2279
2393
 
2280
- fn binding(battle: anytype, player: Player) void {
2394
+ fn binding(battle: anytype, player: Player, rewrap: bool, options: anytype) !void {
2281
2395
  var side = battle.side(player);
2282
2396
  var foe = battle.foe(player);
2283
2397
 
@@ -2285,12 +2399,16 @@ pub const Effects = struct {
2285
2399
  side.active.volatiles.Binding = true;
2286
2400
  // GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
2287
2401
  // (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
2288
- if (!showdown) foe.active.volatiles.Recharging = false;
2402
+ if (!showdown) {
2403
+ foe.active.volatiles.Recharging = false;
2404
+ } else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
2289
2405
 
2290
- side.active.volatiles.attacks = distribution(battle) - 1;
2406
+ side.active.volatiles.attacks =
2407
+ try Rolls.distribution(battle, .Binding, player, options) - 1;
2291
2408
  }
2292
2409
 
2293
- fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
2410
+ fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
2411
+ var log = options.log;
2294
2412
  var side = battle.side(player);
2295
2413
  const ident = battle.active(player);
2296
2414
 
@@ -2300,85 +2418,87 @@ pub const Effects = struct {
2300
2418
  switch (move.effect) {
2301
2419
  .AttackUp1, .AttackUp2, .Rage => {
2302
2420
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2303
- if (boosts.atk == 6) return try log.fail(ident, .None);
2421
+ if (boosts.atk == 6) return log.fail(.{ ident, .None });
2304
2422
  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;
2423
+ boosts.atk = @as(i4, @intCast(@min(6, @as(i8, boosts.atk) + n)));
2424
+ const reason: Boost = if (move.effect == .Rage) .Rage else .Attack;
2307
2425
  if (stats.atk == MAX_STAT_VALUE) {
2308
2426
  boosts.atk -= 1;
2309
2427
  if (showdown) {
2310
- try log.boost(ident, reason, n);
2311
- try log.boost(ident, Boost.Attack, -1);
2428
+ try log.boost(.{ ident, reason, n });
2429
+ try log.boost(.{ ident, .Attack, -1 });
2312
2430
  if (move.effect == .Rage) return;
2313
2431
  }
2314
- return try log.fail(ident, .None);
2432
+ return log.fail(.{ ident, .None });
2315
2433
  }
2316
- var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2317
- const stat = unmodifiedStats(battle, player).atk;
2434
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2435
+ const stat = unmodifiedStats(battle, side).atk;
2318
2436
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2319
- try log.boost(ident, reason, n);
2437
+ try log.boost(.{ ident, reason, n });
2438
+ // Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
2439
+ if (showdown and move.effect == .Rage) return;
2320
2440
  },
2321
2441
  .DefenseUp1, .DefenseUp2 => {
2322
2442
  assert(boosts.def >= -6 and boosts.def <= 6);
2323
- if (boosts.def == 6) return try log.fail(ident, .None);
2443
+ if (boosts.def == 6) return log.fail(.{ ident, .None });
2324
2444
  const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
2325
- boosts.def = @intCast(i4, @min(6, @as(i8, boosts.def) + n));
2445
+ boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
2326
2446
  if (stats.def == MAX_STAT_VALUE) {
2327
2447
  boosts.def -= 1;
2328
2448
  if (showdown) {
2329
- try log.boost(ident, .Defense, n);
2330
- try log.boost(ident, .Defense, -1);
2449
+ try log.boost(.{ ident, .Defense, n });
2450
+ try log.boost(.{ ident, .Defense, -1 });
2331
2451
  }
2332
- return try log.fail(ident, .None);
2452
+ return log.fail(.{ ident, .None });
2333
2453
  }
2334
- var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2335
- const stat = unmodifiedStats(battle, player).def;
2454
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2455
+ const stat = unmodifiedStats(battle, side).def;
2336
2456
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2337
- try log.boost(ident, .Defense, n);
2457
+ try log.boost(.{ ident, .Defense, n });
2338
2458
  },
2339
2459
  .SpeedUp2 => {
2340
2460
  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));
2461
+ if (boosts.spe == 6) return log.fail(.{ ident, .None });
2462
+ boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
2343
2463
  if (stats.spe == MAX_STAT_VALUE) {
2344
2464
  boosts.spe -= 1;
2345
2465
  if (showdown) {
2346
- try log.boost(ident, .Speed, 2);
2347
- try log.boost(ident, .Speed, -1);
2466
+ try log.boost(.{ ident, .Speed, 2 });
2467
+ try log.boost(.{ ident, .Speed, -1 });
2348
2468
  }
2349
- return try log.fail(ident, .None);
2469
+ return log.fail(.{ ident, .None });
2350
2470
  }
2351
- var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2352
- const stat = unmodifiedStats(battle, player).spe;
2471
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2472
+ const stat = unmodifiedStats(battle, side).spe;
2353
2473
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2354
- try log.boost(ident, .Speed, 2);
2474
+ try log.boost(.{ ident, .Speed, 2 });
2355
2475
  },
2356
2476
  .SpecialUp1, .SpecialUp2 => {
2357
2477
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2358
- if (boosts.spc == 6) return try log.fail(ident, .None);
2478
+ if (boosts.spc == 6) return log.fail(.{ ident, .None });
2359
2479
  const n: u2 = if (move.effect == .SpecialUp2) 2 else 1;
2360
- boosts.spc = @intCast(i4, @min(6, @as(i8, boosts.spc) + n));
2480
+ boosts.spc = @intCast(@min(6, @as(i8, boosts.spc) + n));
2361
2481
  if (stats.spc == MAX_STAT_VALUE) {
2362
2482
  boosts.spc -= 1;
2363
2483
  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);
2484
+ try log.boost(.{ ident, .SpecialAttack, n });
2485
+ try log.boost(.{ ident, .SpecialAttack, -1 });
2486
+ try log.boost(.{ ident, .SpecialDefense, n });
2487
+ try log.boost(.{ ident, .SpecialDefense, -1 });
2368
2488
  }
2369
- return try log.fail(ident, .None);
2489
+ return log.fail(.{ ident, .None });
2370
2490
  }
2371
- var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2372
- const stat = unmodifiedStats(battle, player).spc;
2491
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2492
+ const stat = unmodifiedStats(battle, side).spc;
2373
2493
  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);
2494
+ try log.boost(.{ ident, .SpecialAttack, n });
2495
+ try log.boost(.{ ident, .SpecialDefense, n });
2376
2496
  },
2377
2497
  .EvasionUp1 => {
2378
2498
  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);
2499
+ if (boosts.evasion == 6) return log.fail(.{ ident, .None });
2500
+ boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
2501
+ try log.boost(.{ ident, .Evasion, 1 });
2382
2502
  },
2383
2503
  else => unreachable,
2384
2504
  }
@@ -2387,106 +2507,106 @@ pub const Effects = struct {
2387
2507
  statusModify(battle.foe(player).stored().status, &battle.foe(player).active.stats);
2388
2508
  }
2389
2509
 
2390
- fn unboost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
2510
+ fn unboost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
2511
+ var log = options.log;
2391
2512
  var foe = battle.foe(player);
2392
2513
  const foe_ident = battle.active(player.foe());
2514
+ const secondary = move.effect.isStatDownChance();
2393
2515
 
2394
- if (foe.active.volatiles.Substitute) {
2395
- return if (!move.effect.isStatDownChance()) log.fail(foe_ident, .None);
2396
- }
2516
+ if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(.{ foe_ident, .None });
2397
2517
 
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
2518
+ if (secondary) {
2519
+ const proc = try Rolls.unboost(battle, player, options);
2520
+ if (!proc or foe.active.volatiles.Invulnerable) return;
2521
+ } else {
2522
+ // checkHit already checks for Invulnerable
2523
+ if (!showdown and !try checkHit(battle, player, move, options)) return;
2524
+ try options.chance.commit(player, .hit);
2406
2525
  }
2407
2526
 
2408
2527
  var stats = &foe.active.stats;
2409
2528
  var boosts = &foe.active.boosts;
2529
+ const fail = showdown or !secondary;
2410
2530
 
2411
2531
  switch (move.effect) {
2412
2532
  .AttackDown1, .AttackDownChance => {
2413
2533
  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));
2534
+ if (boosts.atk == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2535
+ boosts.atk = @intCast(@max(-6, @as(i8, boosts.atk) - 1));
2416
2536
  if (stats.atk == 1) {
2417
2537
  boosts.atk += 1;
2418
2538
  if (showdown) {
2419
- try log.boost(foe_ident, .Attack, -1);
2420
- try log.boost(foe_ident, .Attack, 1);
2539
+ try log.boost(.{ foe_ident, .Attack, -1 });
2540
+ try log.boost(.{ foe_ident, .Attack, 1 });
2421
2541
  }
2422
- return try log.fail(foe_ident, .None);
2542
+ return log.fail(.{ foe_ident, .None });
2423
2543
  }
2424
- var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2425
- const stat = unmodifiedStats(battle, player.foe()).atk;
2544
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2545
+ const stat = unmodifiedStats(battle, foe).atk;
2426
2546
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2427
- try log.boost(foe_ident, .Attack, -1);
2547
+ try log.boost(.{ foe_ident, .Attack, -1 });
2428
2548
  },
2429
2549
  .DefenseDown1, .DefenseDown2, .DefenseDownChance => {
2430
2550
  assert(boosts.def >= -6 and boosts.def <= 6);
2431
- if (boosts.def == -6) return try log.fail(foe_ident, .None);
2551
+ if (boosts.def == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2432
2552
  const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
2433
- boosts.def = @intCast(i4, @max(-6, @as(i8, boosts.def) - n));
2553
+ boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
2434
2554
  if (stats.def == 1) {
2435
2555
  boosts.def += 1;
2436
2556
  if (showdown) {
2437
- try log.boost(foe_ident, .Defense, -@as(i8, n));
2438
- try log.boost(foe_ident, .Defense, 1);
2557
+ try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
2558
+ try log.boost(.{ foe_ident, .Defense, 1 });
2439
2559
  }
2440
- return try log.fail(foe_ident, .None);
2560
+ return log.fail(.{ foe_ident, .None });
2441
2561
  }
2442
- var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2443
- const stat = unmodifiedStats(battle, player.foe()).def;
2562
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2563
+ const stat = unmodifiedStats(battle, foe).def;
2444
2564
  stats.def = @max(1, stat * mod[0] / mod[1]);
2445
- try log.boost(foe_ident, .Defense, -@as(i8, n));
2565
+ try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
2446
2566
  },
2447
2567
  .SpeedDown1, .SpeedDownChance => {
2448
2568
  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));
2569
+ if (boosts.spe == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2570
+ boosts.spe = @intCast(@max(-6, @as(i8, boosts.spe) - 1));
2451
2571
  if (stats.spe == 1) {
2452
2572
  boosts.spe += 1;
2453
2573
  if (showdown) {
2454
- try log.boost(foe_ident, .Speed, -1);
2455
- try log.boost(foe_ident, .Speed, 1);
2574
+ try log.boost(.{ foe_ident, .Speed, -1 });
2575
+ try log.boost(.{ foe_ident, .Speed, 1 });
2456
2576
  }
2457
- return try log.fail(foe_ident, .None);
2577
+ return log.fail(.{ foe_ident, .None });
2458
2578
  }
2459
- var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2460
- const stat = unmodifiedStats(battle, player.foe()).spe;
2579
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2580
+ const stat = unmodifiedStats(battle, foe).spe;
2461
2581
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2462
- try log.boost(foe_ident, .Speed, -1);
2582
+ try log.boost(.{ foe_ident, .Speed, -1 });
2463
2583
  assert(boosts.spe >= -6);
2464
2584
  },
2465
2585
  .SpecialDownChance => {
2466
2586
  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));
2587
+ if (boosts.spc == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2588
+ boosts.spc = @intCast(@max(-6, @as(i8, boosts.spc) - 1));
2469
2589
  if (stats.spc == 1) {
2470
2590
  boosts.spc += 1;
2471
2591
  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);
2592
+ try log.boost(.{ foe_ident, .SpecialAttack, -1 });
2593
+ try log.boost(.{ foe_ident, .SpecialAttack, 1 });
2594
+ try log.boost(.{ foe_ident, .SpecialDefense, -1 });
2595
+ try log.boost(.{ foe_ident, .SpecialDefense, 1 });
2476
2596
  }
2477
- return try log.fail(foe_ident, .None);
2597
+ return log.fail(.{ foe_ident, .None });
2478
2598
  }
2479
- var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2480
- const stat = unmodifiedStats(battle, player.foe()).spc;
2599
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2600
+ const stat = unmodifiedStats(battle, foe).spc;
2481
2601
  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);
2602
+ try log.boost(.{ foe_ident, .SpecialAttack, -1 });
2603
+ try log.boost(.{ foe_ident, .SpecialDefense, -1 });
2484
2604
  },
2485
2605
  .AccuracyDown1 => {
2486
2606
  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);
2607
+ if (boosts.accuracy == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2608
+ boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
2609
+ try log.boost(.{ foe_ident, .Accuracy, -1 });
2490
2610
  },
2491
2611
  else => unreachable,
2492
2612
  }
@@ -2496,8 +2616,7 @@ pub const Effects = struct {
2496
2616
  }
2497
2617
  };
2498
2618
 
2499
- fn unmodifiedStats(battle: anytype, who: Player) *Stats(u16) {
2500
- const side = battle.side(who);
2619
+ fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
2501
2620
  if (!side.active.volatiles.Transform) return &side.stored().stats;
2502
2621
  const id = ID.from(side.active.volatiles.transform);
2503
2622
  return &battle.side(id.player).pokemon[id.id - 1].stats;
@@ -2511,37 +2630,40 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
2511
2630
  }
2512
2631
  }
2513
2632
 
2514
- inline fn isForced(active: ActivePokemon) bool {
2633
+ fn isForced(active: anytype) bool {
2515
2634
  return active.volatiles.Recharging or active.volatiles.Rage or
2516
2635
  active.volatiles.Thrashing or active.volatiles.Charging;
2517
2636
  }
2518
2637
 
2519
- fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
2638
+ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !void {
2639
+ var log = options.log;
2520
2640
  var side = battle.side(who);
2521
2641
  var volatiles = &side.active.volatiles;
2522
2642
  const ident = battle.active(who);
2523
2643
 
2524
- if (volatiles.disabled_move != 0) {
2525
- volatiles.disabled_move = 0;
2526
- volatiles.disabled_duration = 0;
2527
- try log.end(ident, .DisableSilent);
2644
+ options.chance.clearDurations(who, clear);
2645
+
2646
+ if (volatiles.disable_move != 0) {
2647
+ volatiles.disable_move = 0;
2648
+ volatiles.disable_duration = 0;
2649
+ try log.end(.{ ident, .DisableSilent });
2528
2650
  }
2529
2651
  if (volatiles.Confusion) {
2530
2652
  // volatiles.confusion is left unchanged
2531
2653
  volatiles.Confusion = false;
2532
- try log.end(ident, .ConfusionSilent);
2654
+ try log.end(.{ ident, .ConfusionSilent });
2533
2655
  }
2534
2656
  if (volatiles.Mist) {
2535
2657
  volatiles.Mist = false;
2536
- try log.end(ident, .Mist);
2658
+ try log.end(.{ ident, .MistSilent });
2537
2659
  }
2538
2660
  if (volatiles.FocusEnergy) {
2539
2661
  volatiles.FocusEnergy = false;
2540
- try log.end(ident, .FocusEnergy);
2662
+ try log.end(.{ ident, .FocusEnergySilent });
2541
2663
  }
2542
2664
  if (volatiles.LeechSeed) {
2543
2665
  volatiles.LeechSeed = false;
2544
- try log.end(ident, .LeechSeed);
2666
+ try log.end(.{ ident, .LeechSeedSilent });
2545
2667
  }
2546
2668
  if (!showdown and volatiles.Toxic) {
2547
2669
  volatiles.Toxic = false;
@@ -2549,102 +2671,366 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
2549
2671
  }
2550
2672
  if (volatiles.LightScreen) {
2551
2673
  volatiles.LightScreen = false;
2552
- try log.end(ident, .LightScreen);
2674
+ try log.end(.{ ident, .LightScreenSilent });
2553
2675
  }
2554
2676
  if (volatiles.Reflect) {
2555
2677
  volatiles.Reflect = false;
2556
- try log.end(ident, .Reflect);
2678
+ try log.end(.{ ident, .ReflectSilent });
2557
2679
  }
2558
2680
  if (showdown and volatiles.Toxic) {
2559
2681
  volatiles.Toxic = false;
2560
2682
  // Pokémon Showdown erroneously clears the toxic counter
2561
2683
  volatiles.toxic = 0;
2562
- try log.end(ident, .Toxic);
2684
+ try log.end(.{ ident, .ToxicSilent });
2563
2685
  }
2564
2686
  }
2565
2687
 
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
- }
2688
+ pub const Rolls = struct {
2689
+ fn speedTie(battle: anytype, options: anytype) !bool {
2690
+ const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
2691
+ player == .P1
2692
+ else if (@hasDecl(@TypeOf(battle.rng), "speedTie"))
2693
+ battle.rng.speedTie()
2694
+ else if (showdown)
2695
+ battle.rng.range(u8, 0, 2) == 0
2696
+ else
2697
+ battle.rng.next() < Gen12.percent(50) + 1;
2698
+
2699
+ try options.chance.speedTie(p1);
2700
+ return p1;
2575
2701
  }
2576
- return false;
2577
- }
2578
2702
 
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);
2703
+ fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2704
+ const crit = if (options.calc.overridden(player, .critical_hit)) |val|
2705
+ val == .true
2706
+ else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
2707
+ battle.rng.criticalHit(player, rate)
2708
+ else if (showdown)
2709
+ battle.rng.chance(u8, rate, 256)
2710
+ else
2711
+ std.math.rotl(u8, battle.rng.next(), 3) < rate;
2712
+
2713
+ try options.chance.criticalHit(player, crit, rate);
2714
+ return crit;
2583
2715
  }
2584
- }
2585
2716
 
2586
- fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
2587
- var volatiles = &active.volatiles;
2588
- assert(volatiles.Thrashing);
2589
- if (volatiles.attacks > 0) return false;
2717
+ fn damage(battle: anytype, player: Player, options: anytype) !u8 {
2718
+ const roll = if (options.calc.overridden(player, .damage)) |val|
2719
+ val
2720
+ else if (@hasDecl(@TypeOf(battle.rng), "damage"))
2721
+ battle.rng.damage(player)
2722
+ else roll: {
2723
+ if (showdown) break :roll battle.rng.range(u8, 217, 256);
2724
+ while (true) {
2725
+ const r = std.math.rotr(u8, battle.rng.next(), 1);
2726
+ if (r >= 217) break :roll r;
2727
+ }
2728
+ };
2590
2729
 
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);
2730
+ assert(roll >= 217 and roll <= 255);
2731
+ try options.chance.damage(player, roll);
2732
+ return roll;
2733
+ }
2597
2734
 
2598
- return true;
2599
- }
2735
+ const uN = if (pkmn.options.miss) u8 else u9;
2736
+ fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) bool {
2737
+ const acc: uN = if (pkmn.options.miss) accuracy else @as(uN, accuracy) + 1;
2738
+ const ok = if (options.calc.overridden(player, .hit)) |val|
2739
+ val == .true
2740
+ else if (@hasDecl(@TypeOf(battle.rng), "hit"))
2741
+ battle.rng.hit(player, acc)
2742
+ else if (showdown)
2743
+ battle.rng.chance(uN, acc, 256)
2744
+ else
2745
+ battle.rng.next() < acc;
2600
2746
 
2601
- const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
2747
+ options.chance.hit(ok, acc);
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 (@hasDecl(@TypeOf(battle.rng), "confused"))
2755
+ battle.rng.confused(player)
2756
+ else if (showdown)
2757
+ !battle.rng.chance(u8, 128, 256)
2758
+ else
2759
+ battle.rng.next() >= Gen12.percent(50) + 1;
2608
2760
 
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;
2761
+ try options.chance.confused(player, cfz);
2762
+ return cfz;
2763
+ }
2764
+
2765
+ fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2766
+ const par = if (options.calc.overridden(player, .paralyzed)) |val|
2767
+ val == .true
2768
+ else if (@hasDecl(@TypeOf(battle.rng), "paralyzed"))
2769
+ battle.rng.paralyzed(player)
2770
+ else if (showdown)
2771
+ battle.rng.chance(u8, 63, 256)
2772
+ else
2773
+ battle.rng.next() < Gen12.percent(25);
2774
+
2775
+ try options.chance.paralyzed(player, par);
2776
+ return par;
2777
+ }
2778
+
2779
+ fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2780
+ const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2781
+ val == .true
2782
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2783
+ battle.rng.secondaryChance(player, 25)
2784
+ else if (showdown)
2785
+ battle.rng.chance(u8, 25, 256)
2786
+ else
2787
+ battle.rng.next() < Gen12.percent(10);
2788
+
2789
+ try options.chance.secondaryChance(player, proc, 25);
2790
+ return proc;
2791
+ }
2792
+
2793
+ fn secondaryChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2794
+ const rate: u8 = if (low) 26 else 77;
2795
+
2796
+ const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2797
+ val == .true
2798
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2799
+ battle.rng.secondaryChance(player, rate)
2800
+ else if (showdown)
2801
+ battle.rng.chance(u8, rate, 256)
2802
+ else
2803
+ battle.rng.next() < 1 + (if (low) Gen12.percent(10) else Gen12.percent(30));
2804
+
2805
+ try options.chance.secondaryChance(player, proc, rate);
2806
+ return proc;
2807
+ }
2808
+
2809
+ fn poisonChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2810
+ const rate: u8 = if (low) 52 else 103;
2811
+
2812
+ const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2813
+ val == .true
2814
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2815
+ battle.rng.secondaryChance(player, rate)
2816
+ else if (showdown)
2817
+ battle.rng.chance(u8, rate, 256)
2818
+ else
2819
+ battle.rng.next() < 1 + (if (low) Gen12.percent(20) else Gen12.percent(40));
2820
+
2821
+ try options.chance.secondaryChance(player, proc, rate);
2822
+ return proc;
2823
+ }
2824
+
2825
+ fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2826
+ const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2827
+ val == .true
2828
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2829
+ battle.rng.secondaryChance(player, 85)
2830
+ else if (showdown)
2831
+ battle.rng.chance(u8, 85, 256)
2832
+ else
2833
+ battle.rng.next() < Gen12.percent(33) + 1;
2834
+
2835
+ try options.chance.secondaryChance(player, proc, 85);
2836
+ return proc;
2837
+ }
2838
+
2839
+ fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2840
+ const move: Move = if (options.calc.overridden(player, .metronome)) |val|
2841
+ val
2842
+ else if (@hasDecl(@TypeOf(battle.rng), "metronome"))
2843
+ battle.rng.metronome(player)
2844
+ else if (showdown)
2845
+ Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
2846
+ else move: {
2847
+ while (true) {
2848
+ const r = battle.rng.next();
2849
+ if (r == 0 or r == @intFromEnum(Move.Metronome)) continue;
2850
+ if (r >= @intFromEnum(Move.Struggle)) continue;
2851
+ break :move @enumFromInt(r);
2616
2852
  }
2853
+ };
2854
+
2855
+ try options.chance.metronome(player, move);
2856
+ return move;
2857
+ }
2858
+
2859
+ fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2860
+ const power = if (options.calc.overridden(player, .psywave)) |val|
2861
+ val - 1
2862
+ else if (@hasDecl(@TypeOf(battle.rng), "psywave"))
2863
+ battle.rng.psywave(player)
2864
+ else if (showdown)
2865
+ battle.rng.range(u8, 0, max)
2866
+ else power: {
2867
+ while (true) {
2868
+ const r = battle.rng.next();
2869
+ if (r < max) break :power r;
2870
+ }
2871
+ };
2872
+
2873
+ assert(power < max);
2874
+ try options.chance.psywave(player, power, max);
2875
+ return power;
2876
+ }
2877
+
2878
+ fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2879
+ const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2880
+ @intCast(val)
2881
+ else if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
2882
+ battle.rng.sleepDuration(player)
2883
+ else if (showdown)
2884
+ battle.rng.range(u3, 1, 8)
2885
+ else duration: {
2886
+ while (true) {
2887
+ const r = battle.rng.next() & 7;
2888
+ if (r != 0) break :duration @intCast(r);
2889
+ }
2890
+ };
2891
+
2892
+ assert(duration >= 1 and duration <= 7);
2893
+ options.chance.duration(.sleep, player, player.foe(), duration);
2894
+ return duration;
2895
+ }
2896
+
2897
+ fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2898
+ const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
2899
+ val
2900
+ else if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
2901
+ battle.rng.disableDuration(player)
2902
+ else if (showdown)
2903
+ battle.rng.range(u4, 1, 9)
2904
+ else
2905
+ @intCast((battle.rng.next() & 7) + 1);
2906
+
2907
+ assert(duration >= 1 and duration <= 8);
2908
+ options.chance.duration(.disable, player, player.foe(), duration);
2909
+ return duration;
2910
+ }
2911
+
2912
+ fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
2913
+ const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2914
+ @intCast(val)
2915
+ else if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
2916
+ battle.rng.confusionDuration(player, self)
2917
+ else if (showdown)
2918
+ battle.rng.range(u3, 2, 6)
2919
+ else
2920
+ @intCast((battle.rng.next() & 3) + 2);
2921
+
2922
+ assert(duration >= 2 and duration <= 5);
2923
+ options.chance.duration(.confusion, player, if (self) player else player.foe(), duration);
2924
+ return duration;
2925
+ }
2926
+
2927
+ fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2928
+ const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2929
+ @intCast(val)
2930
+ else if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
2931
+ battle.rng.attackingDuration(player)
2932
+ else if (showdown)
2933
+ battle.rng.range(u3, 2, 4)
2934
+ else
2935
+ @intCast((battle.rng.next() & 1) + 2);
2936
+
2937
+ assert(duration >= 2 and duration <= 3);
2938
+ options.chance.duration(.attacking, player, player, duration);
2939
+ return duration;
2940
+ }
2941
+
2942
+ const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
2943
+
2944
+ fn distribution(
2945
+ battle: anytype,
2946
+ comptime effect: Move.Effect,
2947
+ player: Player,
2948
+ options: anytype,
2949
+ ) !u3 {
2950
+ const roll = if (effect == .Binding) .duration else .multi_hit;
2951
+ const n: u3 = if (options.calc.overridden(player, roll)) |val|
2952
+ @intCast(val)
2953
+ else if (@hasDecl(@TypeOf(battle.rng), "distribution"))
2954
+ battle.rng.distribution(player)
2955
+ else if (showdown)
2956
+ DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
2957
+ else n: {
2958
+ const r = (battle.rng.next() & 3);
2959
+ break :n @intCast((if (r < 2) r else battle.rng.next() & 3) + 2);
2960
+ };
2961
+
2962
+ assert(n >= 2 and n <= 5);
2963
+ if (effect == .Binding) {
2964
+ options.chance.duration(.binding, player, player, n);
2617
2965
  } 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;
2966
+ try options.chance.multiHit(player, n);
2967
+ }
2968
+ return n;
2969
+ }
2970
+
2971
+ fn moveSlot(
2972
+ battle: anytype,
2973
+ player: Player,
2974
+ moves: []MoveSlot,
2975
+ check_pp: u4,
2976
+ options: anytype,
2977
+ ) !u4 {
2978
+ // TODO: consider throwing error instead of rerolling?
2979
+ const overridden = if (options.calc.overridden(player, .move_slot)) |val|
2980
+ if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
2981
+ val
2982
+ else
2983
+ null
2984
+ else
2985
+ null;
2986
+
2987
+ const slot: u4 = overridden orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
2988
+ battle.rng.moveSlot(player, moves, check_pp)
2989
+ else slot: {
2990
+ if (showdown) {
2991
+ if (check_pp == 0) {
2992
+ var i: usize = moves.len;
2993
+ while (i > 0) {
2994
+ i -= 1;
2995
+ if (moves[i].id != .None) {
2996
+ break :slot battle.rng.range(u4, 0, @as(u4, @intCast(i + 1))) + 1;
2997
+ }
2998
+ }
2999
+ } else {
3000
+ var r = battle.rng.range(u4, 0, check_pp) + 1;
3001
+ var i: usize = 0;
3002
+ while (i < moves.len and r > 0) : (i += 1) {
3003
+ if (moves[i].pp > 0) {
3004
+ r -= 1;
3005
+ if (r == 0) break :slot @intCast(i + 1);
3006
+ }
3007
+ }
2624
3008
  }
2625
3009
  }
2626
- return @intCast(u4, i + 1);
2627
- }
2628
- }
2629
3010
 
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;
3011
+ while (true) {
3012
+ const r: u4 = @intCast(battle.rng.next() & 3);
3013
+ if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) break :slot r + 1;
3014
+ }
3015
+ };
3016
+
3017
+ try options.chance.moveSlot(player, slot, moves, check_pp);
3018
+ return slot;
2633
3019
  }
2634
- }
3020
+ };
2635
3021
 
2636
3022
  test "RNG agreement" {
2637
3023
  if (!showdown) return;
2638
3024
  var expected: [256]u32 = undefined;
2639
3025
  for (0..expected.len) |i| {
2640
- expected[i] = @intCast(u32, i * 0x1000000);
3026
+ expected[i] = @intCast(i * 0x1000000);
2641
3027
  }
2642
3028
 
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 };
3029
+ var spe: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3030
+ var cfz: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3031
+ var par: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3032
+ var brn: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3033
+ var eff: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
2648
3034
 
2649
3035
  for (0..expected.len) |i| {
2650
3036
  try expectEqual(spe.range(u8, 0, 2) == 0, i < Gen12.percent(50) + 1);
@@ -2680,7 +3066,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
2680
3066
  const side = battle.side(player);
2681
3067
  const foe = battle.foe(player);
2682
3068
 
2683
- var active = side.active;
3069
+ const active = &side.active;
2684
3070
  const stored = side.stored();
2685
3071
 
2686
3072
  // While players are not given any input options on the cartridge in these cases,
@@ -2694,7 +3080,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
2694
3080
  // strictly correct as the player would not have been presented the option to move or
2695
3081
  // switch at all.
2696
3082
  if (isForced(active)) {
2697
- out[n] = .{ .type = .Move, .data = @boolToInt(showdown) };
3083
+ out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
2698
3084
  n += 1;
2699
3085
  return n;
2700
3086
  }
@@ -2731,9 +3117,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
2731
3117
  while (slot <= 4) : (slot += 1) {
2732
3118
  const m = active.moves[slot - 1];
2733
3119
  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;
3120
+ if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
3121
+ // Pokémon Showdown displays Struggle if limited to Bide but unable to pick
3122
+ const struggle =
3123
+ m.id == .Bide and (m.pp == 0 or active.volatiles.disable_move == slot);
3124
+ const s = if (struggle) 0 else slot;
2737
3125
  out[n] = .{ .type = .Move, .data = s };
2738
3126
  n += 1;
2739
3127
  return n;
@@ -2747,7 +3135,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
2747
3135
  const m = active.moves[slot - 1];
2748
3136
  if (m.id == .None) break;
2749
3137
  if (m.pp == 0) continue;
2750
- if (active.volatiles.disabled_move == slot) continue;
3138
+ if (active.volatiles.disable_move == slot) continue;
2751
3139
  out[n] = .{ .type = .Move, .data = slot };
2752
3140
  n += 1;
2753
3141
  }