@pkmn/engine 0.1.0-dev.83de29ee → 0.1.0-dev.886adbe3

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