@pkmn/engine 0.1.0-dev.d716e940 → 0.1.0-dev.d952b7c9

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