@pkmn/engine 0.1.0-dev.02b7f810 → 0.1.0-dev.04a96f24

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