@pkmn/engine 0.1.0-dev.14e79917 → 0.1.0-dev.1cea12bf

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