@pkmn/engine 0.1.0-dev.83de29ee → 0.1.0-dev.86ae1278

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