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

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