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

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