@pkmn/engine 0.1.0-dev.3a273268 → 0.1.0-dev.3de821b9

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