@pkmn/engine 0.1.0-dev.83de29ee → 0.1.0-dev.89cc1c98

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 (51) hide show
  1. package/README.md +55 -44
  2. package/build/pkg/addon.js +3 -2
  3. package/build/pkg/addon.js.map +1 -1
  4. package/build/pkg/data/ids.json +12 -12
  5. package/build/pkg/data/layout.json +26 -4
  6. package/build/pkg/data/protocol.json +69 -12
  7. package/build/pkg/data.d.ts +2 -2
  8. package/build/pkg/gen1.js +23 -18
  9. package/build/pkg/gen1.js.map +1 -1
  10. package/build/pkg/index.js.map +1 -1
  11. package/build/pkg/protocol.d.ts +4 -2
  12. package/build/pkg/protocol.js +60 -39
  13. package/build/pkg/protocol.js.map +1 -1
  14. package/build/test/display.js +0 -1
  15. package/build/test/display.js.map +1 -1
  16. package/build.zig +89 -65
  17. package/package.json +41 -20
  18. package/src/bin/install-pkmn-engine +10 -5
  19. package/src/lib/binding/node.zig +46 -141
  20. package/src/lib/binding/wasm.zig +4 -2
  21. package/src/lib/common/battle.zig +17 -0
  22. package/src/lib/common/data.zig +19 -31
  23. package/src/lib/common/debug.zig +20 -11
  24. package/src/lib/common/js.zig +118 -0
  25. package/src/lib/common/optional.zig +67 -0
  26. package/src/lib/common/options.zig +9 -0
  27. package/src/lib/common/protocol.zig +750 -208
  28. package/src/lib/common/rational.zig +353 -0
  29. package/src/lib/common/rng.zig +15 -15
  30. package/src/lib/common/util.zig +12 -0
  31. package/src/lib/gen1/README.md +297 -211
  32. package/src/lib/gen1/calc.zig +426 -0
  33. package/src/lib/gen1/chance.zig +1060 -0
  34. package/src/lib/gen1/data/moves.zig +202 -190
  35. package/src/lib/gen1/data/species.zig +4 -4
  36. package/src/lib/gen1/data/types.zig +10 -10
  37. package/src/lib/gen1/data.zig +85 -73
  38. package/src/lib/gen1/helpers.zig +576 -29
  39. package/src/lib/gen1/mechanics.zig +859 -600
  40. package/src/lib/gen2/README.md +106 -85
  41. package/src/lib/gen2/calc.zig +143 -0
  42. package/src/lib/gen2/chance.zig +1314 -0
  43. package/src/lib/gen2/data/items.zig +14 -20
  44. package/src/lib/gen2/data/moves.zig +664 -570
  45. package/src/lib/gen2/data/species.zig +2 -2
  46. package/src/lib/gen2/data/types.zig +137 -34
  47. package/src/lib/gen2/data.zig +121 -85
  48. package/src/lib/gen2/generated.zig +857 -0
  49. package/src/lib/gen2/helpers.zig +16 -22
  50. package/src/lib/gen2/mechanics.zig +2998 -12
  51. package/src/lib/pkmn.zig +61 -2
@@ -1,8 +1,9 @@
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
 
@@ -12,13 +13,13 @@ const assert = std.debug.assert;
12
13
 
13
14
  const expectEqual = std.testing.expectEqual;
14
15
 
16
+ const showdown = pkmn.options.showdown;
17
+
15
18
  const Choice = common.Choice;
16
19
  const ID = common.ID;
17
20
  const Player = common.Player;
18
21
  const Result = common.Result;
19
22
 
20
- const showdown = options.showdown;
21
-
22
23
  const Boost = protocol.Boost;
23
24
  const Damage = protocol.Damage;
24
25
  const Heal = protocol.Heal;
@@ -56,19 +57,15 @@ 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);
@@ -76,19 +73,19 @@ pub fn update(battle: anytype, c1: Choice, c2: Choice, log: anytype) !Result {
76
73
  var r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
77
74
  var 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
 
@@ -100,10 +97,10 @@ fn start(battle: anytype, log: anytype) !Result {
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,9 +159,7 @@ 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
@@ -179,11 +171,7 @@ fn selectMove(
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;
@@ -278,17 +255,18 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, log: anyty
278
255
 
279
256
  foe.active.volatiles.Binding = false;
280
257
 
281
- try log.switched(battle.active(player), incoming);
258
+ try options.log.switched(battle.active(player), incoming);
259
+ options.chance.switched(player, side.order[0], out);
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, 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, options)) |r| return r;
376
353
  } else if (foe_choice.type == .Pass) return null;
377
354
 
355
+ 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, 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, 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,14 +421,13 @@ 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
433
  // index 1 and the current Pokémon has the same move in its first slot.
@@ -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,11 +482,21 @@ 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;
512
486
  // 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);
487
+ if (options.calc.modify(player, .sleep)) |extend| {
488
+ if (!extend) stored.status = 0;
489
+ } else {
490
+ stored.status -= 1;
491
+ }
492
+
493
+ const duration = Status.duration(stored.status);
494
+ if (!Status.is(stored.status, .EXT)) {
495
+ try options.chance.sleep(player, duration);
496
+ }
497
+
498
+ if (duration == 0) {
499
+ try log.curestatus(ident, before, .Message);
516
500
  stored.status = 0; // clears EXT if present
517
501
  } else {
518
502
  try log.cant(ident, .Sleep);
@@ -546,15 +530,21 @@ fn beforeMove(
546
530
  return .done;
547
531
  }
548
532
 
549
- if (volatiles.disabled_duration > 0) {
550
- volatiles.disabled_duration -= 1;
551
- if (volatiles.disabled_duration == 0) {
552
- volatiles.disabled_move = 0;
533
+ if (volatiles.disable_duration > 0) {
534
+ if (options.calc.modify(player, .disable)) |extend| {
535
+ if (!extend) volatiles.disable_duration = 0;
536
+ } else {
537
+ volatiles.disable_duration -= 1;
538
+ }
539
+ try options.chance.disable(player, volatiles.disable_duration);
540
+
541
+ if (volatiles.disable_duration == 0) {
542
+ volatiles.disable_move = 0;
553
543
  try log.end(ident, .Disable);
554
544
  }
555
545
  }
556
546
  // Pokémon Showdown's disable condition has a single onBeforeMove handler
557
- if (showdown and try disabled(side, ident, log)) return .done;
547
+ if (showdown and try disabled(side, ident, options)) return .done;
558
548
 
559
549
  // This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
560
550
  // before the side had a selected a move - we simply need to assume this leads to a desync
@@ -566,14 +556,20 @@ fn beforeMove(
566
556
  if (volatiles.Confusion) {
567
557
  assert(volatiles.confusion > 0);
568
558
 
569
- volatiles.confusion -= 1;
559
+ if (options.calc.modify(player, .confusion)) |extend| {
560
+ if (!extend) volatiles.confusion = 0;
561
+ } else {
562
+ volatiles.confusion -= 1;
563
+ }
564
+ try options.chance.confusion(player, volatiles.confusion);
565
+
570
566
  if (volatiles.confusion == 0) {
571
567
  volatiles.Confusion = false;
572
568
  try log.end(ident, .Confusion);
573
569
  } else {
574
570
  try log.activate(ident, .Confusion);
575
571
 
576
- if (Rolls.confused(battle)) {
572
+ if (try Rolls.confused(battle, player, options)) {
577
573
  assert(!volatiles.MultiHit);
578
574
  if (!volatiles.Rage) volatiles.state = 0;
579
575
  volatiles.Bide = false;
@@ -583,6 +579,7 @@ fn beforeMove(
583
579
  volatiles.Charging = false;
584
580
  volatiles.Binding = false;
585
581
  volatiles.Invulnerable = false;
582
+ options.chance.clearDurations(player, null);
586
583
  {
587
584
  // This feels (and is) disgusting but the cartridge literally just overwrites
588
585
  // the opponent's defense with the user's defense and resets it after. As a
@@ -602,7 +599,7 @@ fn beforeMove(
602
599
 
603
600
  const uncapped = battle.last_damage;
604
601
  // Skipping adjustDamage / randomizeDamage / checkHit
605
- _ = try applyDamage(battle, player, target, .Confusion, log);
602
+ _ = try applyDamage(battle, player, target, .Confusion, options);
606
603
  // Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
607
604
  if (showdown) battle.last_damage = uncapped;
608
605
 
@@ -611,14 +608,15 @@ fn beforeMove(
611
608
  }
612
609
  }
613
610
 
614
- if (!showdown and try disabled(side, ident, log)) return .done;
611
+ if (!showdown and try disabled(side, ident, options)) return .done;
615
612
 
616
- if (Status.is(stored.status, .PAR) and Rolls.paralyzed(battle)) {
613
+ if (Status.is(stored.status, .PAR) and try Rolls.paralyzed(battle, player, options)) {
617
614
  if (!volatiles.Rage) volatiles.state = 0;
618
615
  volatiles.Bide = false;
619
616
  volatiles.Thrashing = false;
620
617
  volatiles.Charging = false;
621
618
  volatiles.Binding = false;
619
+ options.chance.clearDurations(player, null);
622
620
  // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
623
621
  try log.cant(ident, .Paralysis);
624
622
  return .done;
@@ -627,11 +625,24 @@ fn beforeMove(
627
625
  if (volatiles.Bide) {
628
626
  assert(!volatiles.Thrashing and !volatiles.Rage);
629
627
 
630
- volatiles.state +%= battle.last_damage;
628
+ if (showdown) {
629
+ // Pokémon Showdown doesn't implement Bide potentially overflowing in the event of
630
+ // OHKO-move damage, but we can fake this incorrect behavior by simply saturating the
631
+ // addition because 65535 is sufficient to faint any Pokémon anyway
632
+ volatiles.state +|= battle.last_damage;
633
+ } else {
634
+ volatiles.state +%= battle.last_damage;
635
+ }
631
636
 
632
637
  assert(volatiles.attacks > 0);
633
638
 
634
- volatiles.attacks -= 1;
639
+ if (options.calc.modify(player, .attacking)) |extend| {
640
+ if (!extend) volatiles.attacks = 0;
641
+ } else {
642
+ volatiles.attacks -= 1;
643
+ }
644
+ try options.chance.attacking(player, volatiles.attacks);
645
+
635
646
  if (volatiles.attacks != 0) {
636
647
  try log.activate(ident, .Bide);
637
648
  return .done;
@@ -649,15 +660,15 @@ fn beforeMove(
649
660
  }
650
661
 
651
662
  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);
663
+ _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
664
+ if (foe.stored().hp > 0 and !sub) try buildRage(battle, player.foe(), options);
654
665
 
655
666
  // For reasons passing understanding, Pokémon Showdown still inflicts residual damage to
656
667
  // Bide's user even if the above damage has caused the foe to faint. It's simpler to always
657
668
  // run residual here regardless of whether the foe fainted and opt-out of the default flow
658
669
  if (showdown) {
659
670
  residual.* = false;
660
- try handleResidual(battle, player, log);
671
+ try handleResidual(battle, player, options);
661
672
  }
662
673
 
663
674
  return .done;
@@ -665,17 +676,23 @@ fn beforeMove(
665
676
 
666
677
  if (volatiles.Thrashing) {
667
678
  assert(volatiles.attacks > 0);
668
- assert(from != null);
669
- volatiles.attacks -= 1;
670
- if (!showdown and handleThrashing(battle, active)) {
679
+
680
+ if (options.calc.modify(player, .attacking)) |extend| {
681
+ if (!extend) volatiles.attacks = 0;
682
+ } else {
683
+ volatiles.attacks -= 1;
684
+ }
685
+ try options.chance.attacking(player, volatiles.attacks);
686
+
687
+ if (!showdown and handleThrashing(battle, active, player, options)) {
671
688
  try log.start(battle.active(player), .ConfusionSilent);
672
689
  }
673
- try log.move(ident, side.last_selected_move, battle.active(player.foe()), from);
690
+ try log.move(ident, side.last_selected_move, battle.active(player.foe()));
674
691
  if (showdown) {
675
692
  // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
676
693
  // used in useMove and doesn't have the notion of skipping canMove semantics
677
694
  side.last_used_move = side.last_selected_move;
678
- if (handleThrashing(battle, active)) {
695
+ if (handleThrashing(battle, active, player, options)) {
679
696
  try log.start(battle.active(player), .ConfusionSilent);
680
697
  }
681
698
  }
@@ -684,18 +701,20 @@ fn beforeMove(
684
701
 
685
702
  if (volatiles.Binding) {
686
703
  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
- );
704
+
705
+ if (options.calc.modify(player, .binding)) |extend| {
706
+ if (!extend) volatiles.attacks = 0;
707
+ } else {
708
+ volatiles.attacks -= 1;
709
+ }
710
+ try options.chance.binding(player, volatiles.attacks);
711
+
712
+ try log.move(battle.active(player), side.last_selected_move, battle.active(player.foe()));
694
713
  if (showdown or battle.last_damage != 0) {
695
714
  const sub = showdown and foe.active.volatiles.Substitute;
696
- _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
715
+ _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
697
716
  if (battle.foe(player).stored().hp > 0 and !sub) {
698
- try buildRage(battle, player.foe(), log);
717
+ try buildRage(battle, player.foe(), options);
699
718
  }
700
719
  }
701
720
  return .done;
@@ -710,69 +729,58 @@ fn canMove(
710
729
  mslot: u4,
711
730
  auto: bool,
712
731
  skip_pp: bool,
713
- from: ?Move,
732
+ from: Move,
714
733
  residual: *bool,
715
- log: anytype,
734
+ options: anytype,
716
735
  ) !bool {
717
736
  var side = battle.side(player);
718
737
  const player_ident = battle.active(player);
719
738
  const move = Move.get(side.last_selected_move);
720
739
 
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
740
  var skip = skip_pp;
726
741
  if (side.active.volatiles.Charging) {
727
742
  side.active.volatiles.Charging = false;
728
743
  side.active.volatiles.Invulnerable = false;
729
744
  } else if (move.effect == .Charge) {
730
- try log.move(player_ident, side.last_selected_move, .{}, from);
731
- try Effects.charge(battle, player, log);
745
+ try options.log.moveFrom(player_ident, side.last_selected_move, .{}, from);
746
+ setCounterable(battle, player, side, move);
747
+ try Effects.charge(battle, player, options);
732
748
  return false;
733
749
  }
734
750
 
735
- if (!showdown or !special) side.last_used_move = side.last_selected_move;
751
+ side.last_used_move = side.last_selected_move;
736
752
  if (!skip) decrementPP(side, mslot, auto);
737
753
 
738
754
  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
- }
755
+ try options.log.moveFrom(player_ident, side.last_selected_move, battle.active(target), from);
756
+ setCounterable(battle, player, side, move);
760
757
 
761
758
  if (move.effect.onBegin()) {
762
- try onBegin(battle, player, move, mslot, residual, log);
759
+ try onBegin(battle, player, move, mslot, residual, options);
763
760
  return false;
764
761
  }
765
762
 
766
763
  if (move.effect == .Thrashing) {
767
- Effects.thrashing(battle, player);
764
+ Effects.thrashing(battle, player, options);
768
765
  } else if (!showdown and move.effect == .Binding) {
769
766
  // Pokémon Showdown handles this after hit/miss checks and damage calculation
770
- Effects.binding(battle, player, false);
767
+ try Effects.binding(battle, player, false, options);
771
768
  }
772
769
 
773
770
  return true;
774
771
  }
775
772
 
773
+ fn setCounterable(battle: anytype, player: Player, side: *Side, move: Move.Data) void {
774
+ // The Counter desync is caused by the cartridge not calling GetCurrentMove until now, meaning
775
+ // in cases where an early return happens the data for a players last selected move does not get
776
+ // reloaded and HandleCounterMove actually bases its success/failure off of stale information.
777
+ // This boolean state we track here doesn't exist on the cartridge because it instead manifests
778
+ // as the desync results from actually having two separate battle states that subtly disagree
779
+ const counterable = side.last_selected_move != .Counter and move.bp > 0 and
780
+ (move.type == .Normal or move.type == .Fighting);
781
+ battle.lastMove(player).counterable = @intFromBool(counterable);
782
+ }
783
+
776
784
  fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
777
785
  if (side.last_selected_move == .Struggle) return;
778
786
 
@@ -782,12 +790,14 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
782
790
  assert(!volatiles.Rage and !volatiles.Thrashing and !volatiles.MultiHit);
783
791
  if (volatiles.Bide) return;
784
792
 
785
- assert(active.move(mslot).pp > 0 or auto);
786
- active.move(mslot).pp = @intCast(u6, active.move(mslot).pp) -% 1;
793
+ var move_slot = active.move(mslot);
794
+ assert(move_slot.pp > 0 or auto);
795
+ move_slot.pp = @as(u6, @intCast(move_slot.pp)) -% 1;
787
796
  if (volatiles.Transform) return;
788
797
 
789
- assert(side.stored().move(mslot).pp > 0 or auto);
790
- side.stored().move(mslot).pp = @intCast(u6, side.stored().move(mslot).pp) -% 1;
798
+ move_slot = side.stored().move(mslot);
799
+ assert(move_slot.pp > 0 or auto);
800
+ move_slot.pp = @as(u6, @intCast(move_slot.pp)) -% 1;
791
801
  assert(active.move(mslot).pp == side.stored().move(mslot).pp);
792
802
  }
793
803
 
@@ -795,12 +805,12 @@ fn incrementPP(side: *Side, mslot: u4) void {
795
805
  var active = &side.active;
796
806
  const volatiles = &active.volatiles;
797
807
 
798
- active.move(mslot).pp = @intCast(u6, active.move(mslot).pp) +% 1;
808
+ active.move(mslot).pp = @as(u6, @intCast(active.move(mslot).pp)) +% 1;
799
809
  // GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
800
810
  if (showdown and volatiles.Transform) return;
801
811
 
802
812
  assert(mslot > 0 and mslot <= 4);
803
- side.stored().moves[mslot - 1].pp = @intCast(u6, side.stored().moves[mslot - 1].pp) +% 1;
813
+ side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% 1;
804
814
  }
805
815
 
806
816
  // Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
@@ -811,8 +821,9 @@ fn doMove(
811
821
  rewrap: bool,
812
822
  auto: bool,
813
823
  residual: *bool,
814
- log: anytype,
824
+ options: anytype,
815
825
  ) !?Result {
826
+ var log = options.log;
816
827
  var side = battle.side(player);
817
828
  const foe = battle.foe(player);
818
829
 
@@ -827,23 +838,27 @@ fn doMove(
827
838
  var hits: u4 = 1;
828
839
  var effectiveness = Effectiveness.neutral;
829
840
 
841
+ // Due to control flow shenanigans we need to clear last_damage for Pokémon Showdown
842
+ if (showdown and !counter) battle.last_damage = 0;
843
+
830
844
  // The cartridge handles set damage moves in applyDamage but we short circuit to simplify things
831
845
  if (move.effect == .SuperFang or move.effect == .SpecialDamage) {
832
- return specialDamage(battle, player, move, log);
846
+ return specialDamage(battle, player, move, options);
833
847
  }
834
848
 
835
849
  // Pokémon Showdown runs invulnerability / immunity checks before checking accuracy - simply
836
850
  // calling moveHit early covers most of that but we also need to check type immunity first
837
851
  var miss = showdown and miss: {
838
852
  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);
853
+ (@intFromEnum(move.type.effectiveness(foe.active.types.type1)) == 0 or
854
+ @intFromEnum(move.type.effectiveness(foe.active.types.type2)) == 0);
841
855
  if (immune and move.effect != .Binding) break :miss true;
842
856
  if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) {
843
857
  battle.last_damage = 0;
844
858
  break :miss true;
845
859
  }
846
- break :miss move.target != .Self and !moveHit(battle, player, move, &immune, &mist);
860
+ break :miss (move.target != .Self and
861
+ !moveHit(battle, player, move, &immune, &mist, options));
847
862
  };
848
863
  assert(!immune or miss or (showdown and move.effect == .Binding));
849
864
 
@@ -851,16 +866,16 @@ fn doMove(
851
866
  const skip = status or immune;
852
867
  if ((!showdown or (!skip or counter)) and !miss) blk: {
853
868
  if (showdown and move.effect.isMulti()) {
854
- Effects.multiHit(battle, player, move);
869
+ try Effects.multiHit(battle, player, move, options);
855
870
  hits = side.active.volatiles.attacks;
856
871
  late = false;
857
872
  }
858
873
 
859
874
  // Cartridge rolls for crit even for moves that can't crit (Counter/Metronome/status/OHKO)
860
875
  const check = !showdown or (!counter and move.effect != .OHKO);
861
- if (check) crit = checkCriticalHit(battle, player, move);
876
+ if (check) crit = try checkCriticalHit(battle, player, move, options);
862
877
 
863
- if (counter) return counterDamage(battle, player, move, log);
878
+ if (counter) return counterDamage(battle, player, move, options);
864
879
 
865
880
  battle.last_damage = 0;
866
881
 
@@ -882,17 +897,15 @@ fn doMove(
882
897
  effectiveness = adjustDamage(battle, player);
883
898
  immune = effectiveness == 0;
884
899
  }
885
- randomizeDamage(battle);
900
+ try randomizeDamage(battle, player, options);
886
901
  }
887
902
  }
888
903
 
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
-
904
+ var zero = battle.last_damage == 0;
892
905
  miss = if (showdown or skip)
893
906
  miss
894
907
  else
895
- (!moveHit(battle, player, move, &immune, &mist) or battle.last_damage == 0);
908
+ (!moveHit(battle, player, move, &immune, &mist, options) or zero);
896
909
 
897
910
  assert(showdown or miss or battle.last_damage > 0 or skip);
898
911
  assert((!showdown and miss) or !(ohko and immune));
@@ -900,11 +913,11 @@ fn doMove(
900
913
 
901
914
  if (!showdown or !miss) {
902
915
  if (move.effect == .MirrorMove) {
903
- return mirrorMove(battle, player, mslot, rewrap, auto, residual, log);
916
+ return mirrorMove(battle, player, mslot, rewrap, auto, residual, options);
904
917
  } else if (move.effect == .Metronome) {
905
- return metronome(battle, player, mslot, rewrap, auto, residual, log);
918
+ return metronome(battle, player, mslot, rewrap, auto, residual, options);
906
919
  } else if (move.effect.onEnd()) {
907
- try onEnd(battle, player, move, log);
920
+ try onEnd(battle, player, move, options);
908
921
  return null;
909
922
  }
910
923
  }
@@ -923,29 +936,34 @@ fn doMove(
923
936
  if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
924
937
  try log.fail(foe_ident, .None);
925
938
  } else {
939
+ if (showdown or !zero) try options.chance.commit(player, .miss);
926
940
  try log.lastmiss();
927
941
  try log.miss(battle.active(player));
928
942
  }
929
943
  if (move.effect == .JumpKick) {
930
944
  // Recoil is supposed to be damage/8 but damage will always be 0 here
931
- assert(battle.last_damage == 0);
945
+ assert(showdown or battle.last_damage == 0);
932
946
  battle.last_damage = 1;
933
- _ = try applyDamage(battle, player, player.foe(), .None, log);
947
+ _ = try applyDamage(battle, player, player.foe(), .None, options);
934
948
  if (showdown and side.stored().hp == 0) residual.* = false;
935
949
  } else if (move.effect == .Explode) {
936
950
  try Effects.explode(battle, player);
937
- try buildRage(battle, player.foe(), log);
951
+ try buildRage(battle, player.foe(), options);
938
952
  } else if (showdown and move.effect == .Disable) {
939
- try buildRage(battle, player.foe(), log);
953
+ try buildRage(battle, player.foe(), options);
954
+ } else if (!showdown and immune and move.effect == .Binding) {
955
+ try options.chance.commit(player, .binding);
940
956
  }
941
957
  return null;
958
+ } else {
959
+ try options.chance.commit(player, .hit);
942
960
  }
943
961
 
944
962
  // On the cartridge MultiHit doesn't get set up until after damage has been applied for the
945
963
  // first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
946
964
  // it up above before damage calculation).
947
965
  if (!showdown and move.effect.isMulti()) {
948
- Effects.multiHit(battle, player, move);
966
+ try Effects.multiHit(battle, player, move, options);
949
967
  hits = side.active.volatiles.attacks;
950
968
  }
951
969
 
@@ -963,11 +981,13 @@ fn doMove(
963
981
  try log.resisted(battle.active(player.foe()));
964
982
  }
965
983
  }
966
- if (!skip) nullified = try applyDamage(battle, player.foe(), player.foe(), .None, log);
984
+ if (!skip) nullified = try applyDamage(battle, player.foe(), player.foe(), .None, options);
967
985
  if (hit == 0 and ohko) try log.ohko();
968
986
  hit += 1;
969
987
  if (foe.stored().hp == 0) break;
970
- if (!late and (!sub or move.effect == .Explode)) try buildRage(battle, player.foe(), log);
988
+ if (!late and (!sub or move.effect == .Explode)) {
989
+ try buildRage(battle, player.foe(), options);
990
+ }
971
991
  // If the substitute breaks during a multi-hit attack, the attack ends
972
992
  if (nullified) break;
973
993
  }
@@ -977,13 +997,13 @@ fn doMove(
977
997
  assert(nullified or foe.stored().hp == 0 or side.active.volatiles.attacks - hit == 0);
978
998
  side.active.volatiles.attacks = 0;
979
999
  if (showdown and move.effect == .Twineedle and !nullified and foe.stored().hp > 0) {
980
- try Effects.poison(battle, player, Move.get(.PoisonSting), log);
1000
+ try Effects.poison(battle, player, Move.get(.PoisonSting), options);
981
1001
  }
982
1002
  try log.hitcount(battle.active(player.foe()), hit);
983
1003
  } else if (showdown) {
984
1004
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
985
1005
  if (move.effect == .Binding) {
986
- Effects.binding(battle, player, rewrap);
1006
+ try Effects.binding(battle, player, rewrap, options);
987
1007
  if (immune) {
988
1008
  battle.last_damage = 0;
989
1009
  assert(foe.stored().hp > 0);
@@ -992,7 +1012,7 @@ fn doMove(
992
1012
  try log.activate(battle.active(player.foe()), .Substitute);
993
1013
  } else {
994
1014
  try log.damage(battle.active(player.foe()), foe.stored(), .None);
995
- try buildRage(battle, player.foe(), log);
1015
+ try buildRage(battle, player.foe(), options);
996
1016
  }
997
1017
  return null;
998
1018
  }
@@ -1006,12 +1026,12 @@ fn doMove(
1006
1026
  // On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
1007
1027
  // but this is only done to setup the MultiHit looping in the first place. Moving the MultiHit
1008
1028
  // 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);
1029
+ if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
1010
1030
 
1011
1031
  if (foe.stored().hp == 0) return null;
1012
1032
 
1013
1033
  // 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);
1034
+ if (late and !sub and move.effect != .Disable) try buildRage(battle, player.foe(), options);
1015
1035
 
1016
1036
  if (!move.effect.isSpecial()) {
1017
1037
  // On the cartridge Rage is not considered to be "special" and thus gets executed for a
@@ -1022,29 +1042,29 @@ fn doMove(
1022
1042
  // Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
1023
1043
  // puts the |-status| message before |-hitcount| instead of after.
1024
1044
  if (move.effect == .Twineedle) {
1025
- if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), log);
1045
+ if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1026
1046
  } else if (move.effect == .Disable) {
1027
- const result = try Effects.disable(battle, player, move, log);
1028
- if (showdown) try buildRage(battle, player.foe(), log);
1047
+ const result = try Effects.disable(battle, player, move, options);
1048
+ if (showdown) try buildRage(battle, player.foe(), options);
1029
1049
  return result;
1030
1050
  } else {
1031
- try moveEffect(battle, player, move, log);
1051
+ try moveEffect(battle, player, move, options);
1032
1052
  }
1033
1053
  }
1034
1054
 
1035
1055
  return null;
1036
1056
  }
1037
1057
 
1038
- fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
1058
+ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !bool {
1039
1059
  const side = battle.side(player);
1040
1060
 
1041
1061
  // 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;
1062
+ var rate = @as(u16, Species.chance(side.stored().species));
1063
+ // GLITCH: Focus Energy reduces critical hit rate instead of increasing it
1064
+ rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
1065
+ rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
1046
1066
 
1047
- return Rolls.criticalHit(battle, chance);
1067
+ return Rolls.criticalHit(battle, player, @intCast(rate), options);
1048
1068
  }
1049
1069
 
1050
1070
  fn calcDamage(
@@ -1110,7 +1130,7 @@ fn calcDamage(
1110
1130
  d = @min(997, d);
1111
1131
  d += 2;
1112
1132
 
1113
- battle.last_damage = @intCast(u16, d);
1133
+ battle.last_damage = @intCast(d);
1114
1134
 
1115
1135
  return true;
1116
1136
  }
@@ -1124,9 +1144,9 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
1124
1144
  var d = battle.last_damage;
1125
1145
  if (side.active.types.includes(move.type)) d +%= d / 2;
1126
1146
 
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));
1147
+ const neutral = @intFromEnum(Effectiveness.Neutral);
1148
+ const eff1: u16 = @intFromEnum(move.type.effectiveness(types.type1));
1149
+ const eff2: u16 = @intFromEnum(move.type.effectiveness(types.type2));
1130
1150
 
1131
1151
  // Type effectiveness matchup precedence only matters with (NVE, SE)
1132
1152
  if (!showdown and (eff1 + eff2) == Effectiveness.mismatch and
@@ -1146,17 +1166,20 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
1146
1166
  return if (types.type1 == types.type2) eff1 * neutral else eff1 * eff2;
1147
1167
  }
1148
1168
 
1149
- fn randomizeDamage(battle: anytype) void {
1169
+ fn randomizeDamage(battle: anytype, player: Player, options: anytype) !void {
1150
1170
  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);
1171
+ const random = try Rolls.damage(battle, player, options);
1172
+ options.calc.base(player.foe(), battle.last_damage);
1173
+ battle.last_damage = @intCast(@as(u32, battle.last_damage) *% random / 255);
1174
+ options.calc.final(player.foe(), battle.last_damage);
1153
1175
  }
1154
1176
 
1155
- fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
1177
+ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
1156
1178
  const side = battle.side(player);
1157
1179
  const foe = battle.foe(player);
1158
1180
 
1159
- if (!try checkHit(battle, player, move, log)) return null;
1181
+ if (!try checkHit(battle, player, move, options)) return null;
1182
+ try options.chance.commit(player, .hit);
1160
1183
 
1161
1184
  battle.last_damage = switch (side.last_selected_move) {
1162
1185
  .SuperFang => @max(foe.stored().hp / 2, 1),
@@ -1165,10 +1188,10 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
1165
1188
  .DragonRage => 40,
1166
1189
  // GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
1167
1190
  .Psywave => power: {
1168
- const max = @intCast(u8, @as(u16, side.stored().level) * 3 / 2);
1191
+ const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
1169
1192
  // GLITCH: Psywave infinite glitch loop
1170
1193
  if (!showdown and max <= 1) return Result.Error;
1171
- break :power Rolls.psywave(battle, max);
1194
+ break :power try Rolls.psywave(battle, player, max, options);
1172
1195
  },
1173
1196
  else => unreachable,
1174
1197
  };
@@ -1176,17 +1199,17 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
1176
1199
  if (battle.last_damage == 0) return if (showdown) null else Result.Error;
1177
1200
 
1178
1201
  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);
1202
+ _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
1203
+ if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), options);
1181
1204
 
1182
1205
  return null;
1183
1206
  }
1184
1207
 
1185
- fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
1208
+ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
1186
1209
  const foe = battle.foe(player);
1187
1210
 
1188
1211
  if (battle.last_damage == 0) {
1189
- try log.fail(battle.active(player), .None);
1212
+ try options.log.fail(battle.active(player), .None);
1190
1213
  return null;
1191
1214
  }
1192
1215
 
@@ -1197,36 +1220,33 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
1197
1220
  else
1198
1221
  foe.last_selected_move);
1199
1222
 
1200
- const used = (if (player.foe() == .P1)
1201
- battle.last_moves.p1_counterable
1202
- else
1203
- battle.last_moves.p2_counterable) != 0;
1204
-
1223
+ const used = battle.lastMove(player.foe()).counterable != 0;
1205
1224
  const selected = foe_last_selected_move.bp > 0 and
1206
1225
  foe.last_selected_move != .Counter and
1207
1226
  (foe_last_selected_move.type == .Normal or
1208
1227
  foe_last_selected_move.type == .Fighting);
1209
1228
 
1210
1229
  if (!used and !selected) {
1211
- try log.fail(battle.active(player), .None);
1230
+ try options.log.fail(battle.active(player), .None);
1212
1231
  return null;
1213
1232
  }
1214
1233
 
1215
1234
  if (!used or !selected) {
1216
1235
  // GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
1217
1236
  if (!showdown) return Result.Error;
1218
- try log.fail(battle.active(player), .None);
1237
+ try options.log.fail(battle.active(player), .None);
1219
1238
  return null;
1220
1239
  }
1221
1240
 
1222
- battle.last_damage = if (battle.last_damage > 0x7FFF) 0xFFFF else battle.last_damage * 2;
1241
+ battle.last_damage *|= 2;
1223
1242
 
1224
1243
  // Pokémon Showdown calls checkHit before Counter
1225
- if (!showdown and !try checkHit(battle, player, move, log)) return null;
1244
+ if (!showdown and !try checkHit(battle, player, move, options)) return null;
1245
+ try options.chance.commit(player, .hit);
1226
1246
 
1227
1247
  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);
1248
+ _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
1249
+ if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), options);
1230
1250
  return null;
1231
1251
  }
1232
1252
 
@@ -1235,7 +1255,7 @@ fn applyDamage(
1235
1255
  target_player: Player,
1236
1256
  sub_player: Player,
1237
1257
  reason: Damage,
1238
- log: anytype,
1258
+ options: anytype,
1239
1259
  ) !bool {
1240
1260
  assert(showdown or battle.last_damage != 0);
1241
1261
 
@@ -1250,19 +1270,20 @@ fn applyDamage(
1250
1270
  subbed.active.volatiles.substitute = 0;
1251
1271
  subbed.active.volatiles.Substitute = false;
1252
1272
  // battle.last_damage is not updated with the amount of HP the Substitute had
1253
- try log.end(battle.active(sub_player), .Substitute);
1273
+ try options.log.end(battle.active(sub_player), .Substitute);
1274
+ options.calc.capped(target_player);
1254
1275
  return true;
1255
1276
  } else {
1256
1277
  // 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);
1278
+ subbed.active.volatiles.substitute -= @intCast(battle.last_damage);
1279
+ try options.log.activate(battle.active(sub_player), .Substitute);
1259
1280
  return false;
1260
1281
  }
1261
1282
  }
1262
1283
 
1263
1284
  if (battle.last_damage > target.stored().hp) battle.last_damage = target.stored().hp;
1264
1285
  target.stored().hp -= battle.last_damage;
1265
- try log.damage(battle.active(target_player), target.stored(), reason);
1286
+ try options.log.damage(battle.active(target_player), target.stored(), reason);
1266
1287
  return false;
1267
1288
  }
1268
1289
 
@@ -1273,22 +1294,24 @@ fn mirrorMove(
1273
1294
  rewrap: bool,
1274
1295
  auto: bool,
1275
1296
  residual: *bool,
1276
- log: anytype,
1277
- ) !?Result {
1297
+ options: anytype,
1298
+ ) anyerror!?Result {
1278
1299
  var side = battle.side(player);
1279
1300
  const foe = battle.foe(player);
1280
1301
 
1281
1302
  side.last_selected_move = foe.last_used_move;
1282
1303
 
1283
1304
  if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
1284
- try log.fail(battle.active(player), .None);
1305
+ try options.log.fail(battle.active(player), .None);
1285
1306
  return null;
1286
1307
  } else if (!showdown or foe.last_used_move != .Struggle) {
1287
1308
  incrementPP(side, mslot);
1288
1309
  }
1289
1310
 
1290
- if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
1291
- return doMove(battle, player, mslot, rewrap, auto, residual, log);
1311
+ if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
1312
+ return null;
1313
+ }
1314
+ return doMove(battle, player, mslot, rewrap, auto, residual, options);
1292
1315
  }
1293
1316
 
1294
1317
  fn metronome(
@@ -1298,35 +1321,48 @@ fn metronome(
1298
1321
  rewrap: bool,
1299
1322
  auto: bool,
1300
1323
  residual: *bool,
1301
- log: anytype,
1302
- ) !?Result {
1324
+ options: anytype,
1325
+ ) anyerror!?Result {
1303
1326
  var side = battle.side(player);
1304
1327
 
1305
- side.last_selected_move = Rolls.metronome(battle);
1328
+ side.last_selected_move = try Rolls.metronome(battle, player, options);
1306
1329
  incrementPP(side, mslot);
1307
1330
 
1308
- if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
1309
- return doMove(battle, player, mslot, rewrap, auto, residual, log);
1331
+ if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
1332
+ return null;
1333
+ }
1334
+ return doMove(battle, player, mslot, rewrap, auto, residual, options);
1310
1335
  }
1311
1336
 
1312
- fn checkHit(battle: anytype, player: Player, move: Move.Data, log: anytype) !bool {
1337
+ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !bool {
1313
1338
  var immune = false;
1314
1339
  var mist = false;
1315
- if (moveHit(battle, player, move, &immune, &mist)) return true;
1340
+
1341
+ if (moveHit(battle, player, move, &immune, &mist, options)) return true;
1342
+
1316
1343
  assert(!immune);
1317
1344
  if (mist) {
1318
1345
  assert(!showdown);
1319
1346
  const foe_ident = battle.active(player.foe());
1320
- try log.activate(foe_ident, .Mist);
1321
- try log.fail(foe_ident, .None);
1347
+ try options.log.activate(foe_ident, .Mist);
1348
+ try options.log.fail(foe_ident, .None);
1322
1349
  } else {
1323
- try log.lastmiss();
1324
- try log.miss(battle.active(player));
1350
+ try options.chance.commit(player, .miss);
1351
+ try options.log.lastmiss();
1352
+ try options.log.miss(battle.active(player));
1325
1353
  }
1354
+
1326
1355
  return false;
1327
1356
  }
1328
1357
 
1329
- fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist: *bool) bool {
1358
+ fn moveHit(
1359
+ battle: anytype,
1360
+ player: Player,
1361
+ move: Move.Data,
1362
+ immune: *bool,
1363
+ mist: *bool,
1364
+ options: anytype,
1365
+ ) bool {
1330
1366
  var side = battle.side(player);
1331
1367
  const foe = battle.foe(player);
1332
1368
 
@@ -1365,10 +1401,10 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
1365
1401
  const overwritten = overwrite and state > 0;
1366
1402
  assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
1367
1403
 
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)];
1404
+ var accuracy = if (overwritten) state else @as(u16, move.accuracy);
1405
+ var boost = BOOSTS[@as(u4, @intCast(@as(i8, side.active.boosts.accuracy) + 6))];
1370
1406
  accuracy = accuracy * boost[0] / boost[1];
1371
- boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
1407
+ boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
1372
1408
  accuracy = accuracy * boost[0] / boost[1];
1373
1409
  accuracy = @min(255, @max(1, accuracy));
1374
1410
 
@@ -1377,7 +1413,7 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
1377
1413
  if (overwrite) side.active.volatiles.state = accuracy;
1378
1414
 
1379
1415
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1380
- break :miss !Rolls.hit(battle, accuracy);
1416
+ break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
1381
1417
  };
1382
1418
 
1383
1419
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1387,7 +1423,7 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
1387
1423
  }
1388
1424
 
1389
1425
  if (!miss) return true;
1390
- battle.last_damage = 0;
1426
+ if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
1391
1427
  side.active.volatiles.Binding = false;
1392
1428
  return false;
1393
1429
  }
@@ -1395,8 +1431,8 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
1395
1431
  fn checkFaint(
1396
1432
  battle: anytype,
1397
1433
  player: Player,
1398
- log: anytype,
1399
- ) @TypeOf(log).Error!?Result {
1434
+ options: anytype,
1435
+ ) @TypeOf(options.log).Error!?Result {
1400
1436
  const side = battle.side(player);
1401
1437
  if (side.stored().hp > 0) return null;
1402
1438
 
@@ -1404,33 +1440,32 @@ fn checkFaint(
1404
1440
  const foe_fainted = foe.stored().hp == 0;
1405
1441
 
1406
1442
  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;
1443
+ const foe_out = foe_fainted and findFirstAlive(foe) == 0;
1444
+ const more = player_out or foe_out;
1410
1445
 
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;
1446
+ if (try faint(battle, player, !(more or foe_fainted), options)) |r| return r;
1447
+ if (foe_fainted) if (try faint(battle, player.foe(), !more, options)) |r| return r;
1413
1448
 
1414
1449
  assert(!side.active.volatiles.MultiHit);
1415
1450
  assert(!foe.active.volatiles.MultiHit);
1416
1451
 
1417
- if (tie) {
1418
- try log.tie();
1452
+ if (player_out and foe_out) {
1453
+ try options.log.tie();
1419
1454
  return Result.Tie;
1420
1455
  } else if (player_out) {
1421
- try log.win(player.foe());
1456
+ try options.log.win(player.foe());
1422
1457
  return if (player == .P1) Result.Lose else Result.Win;
1423
1458
  } else if (foe_out) {
1424
- try log.win(player);
1459
+ try options.log.win(player);
1425
1460
  return if (player == .P1) Result.Win else Result.Lose;
1426
1461
  }
1427
1462
 
1428
1463
  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 };
1464
+ if (player == .P1) return .{ .p1 = .Switch, .p2 = foe_choice };
1465
+ return .{ .p1 = foe_choice, .p2 = .Switch };
1431
1466
  }
1432
1467
 
1433
- fn faint(battle: anytype, player: Player, log: anytype, done: bool) !?Result {
1468
+ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
1434
1469
  var side = battle.side(player);
1435
1470
  var foe = battle.foe(player);
1436
1471
  assert(side.stored().hp == 0);
@@ -1463,32 +1498,31 @@ fn faint(battle: anytype, player: Player, log: anytype, done: bool) !?Result {
1463
1498
  side.stored().stats.spe;
1464
1499
  }
1465
1500
 
1466
- try log.faint(battle.active(player), done);
1501
+ try options.log.faint(battle.active(player), done);
1502
+ options.calc.capped(player);
1467
1503
  return null;
1468
1504
  }
1469
1505
 
1470
- fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1506
+ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1471
1507
  var side = battle.side(player);
1472
1508
  var stored = side.stored();
1473
1509
  const ident = battle.active(player);
1474
1510
  var volatiles = &side.active.volatiles;
1475
1511
 
1476
1512
  const brn = Status.is(stored.status, .BRN);
1477
- if (brn or Status.is(stored.status, .PSN)) blk: {
1513
+ if (brn or Status.is(stored.status, .PSN)) {
1478
1514
  var damage = @max(stored.stats.hp / 16, 1);
1479
-
1480
1515
  if (volatiles.Toxic) {
1481
1516
  volatiles.toxic += 1;
1482
1517
  damage *= volatiles.toxic;
1483
1518
  }
1519
+ damage = @min(damage, stored.hp);
1484
1520
 
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);
1521
+ if (!showdown or damage > 0) {
1522
+ stored.hp -= damage;
1523
+ // Pokémon Showdown uses damageOf here but its not relevant in Generation I
1524
+ try options.log.damage(ident, stored, if (brn) Damage.Burn else Damage.Poison);
1525
+ }
1492
1526
  }
1493
1527
 
1494
1528
  if (volatiles.LeechSeed) {
@@ -1513,42 +1547,42 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1513
1547
  stored.hp -= amount;
1514
1548
 
1515
1549
  // As above, Pokémon Showdown uses damageOf but its not relevant
1516
- if (amount > 0) try log.damage(ident, stored, .LeechSeed);
1550
+ if (amount > 0) try options.log.damage(ident, stored, .LeechSeed);
1517
1551
 
1518
1552
  const before = foe_stored.hp;
1519
1553
  // Uncapped damage is added back to the foe
1520
1554
  foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
1521
1555
  // 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);
1556
+ if (foe_stored.hp > before) try options.log.heal(foe_ident, foe_stored, .Silent);
1523
1557
  }
1524
1558
  }
1525
1559
 
1526
- fn endTurn(battle: anytype, log: anytype) @TypeOf(log).Error!Result {
1560
+ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result {
1527
1561
  assert(!battle.side(.P1).active.volatiles.MultiHit);
1528
1562
  assert(!battle.side(.P2).active.volatiles.MultiHit);
1529
1563
 
1530
- if (showdown and options.ebc and checkEBC(battle)) {
1531
- try log.tie();
1564
+ battle.turn += 1;
1565
+
1566
+ if (showdown and pkmn.options.ebc and checkEBC(battle)) {
1567
+ try options.log.tie();
1532
1568
  return Result.Tie;
1533
1569
  }
1534
1570
 
1535
- battle.turn += 1;
1536
-
1537
1571
  if (showdown and battle.turn >= 1000) {
1538
- try log.tie();
1572
+ try options.log.tie();
1539
1573
  return Result.Tie;
1540
1574
  } else if (battle.turn >= 65535) {
1541
1575
  return Result.Error;
1542
1576
  }
1543
1577
 
1544
- try log.turn(battle.turn);
1578
+ try options.log.turn(battle.turn);
1545
1579
 
1546
1580
  return Result.Default;
1547
1581
  }
1548
1582
 
1549
1583
  fn checkEBC(battle: anytype) bool {
1550
1584
  for (battle.sides, 0..) |side, i| {
1551
- const foe = battle.sides[~@intCast(u1, i)];
1585
+ const foe = battle.sides[~@as(u1, @intCast(i))];
1552
1586
 
1553
1587
  var foe_all_ghosts = true;
1554
1588
  var foe_all_transform = true;
@@ -1600,10 +1634,10 @@ fn checkEBC(battle: anytype) bool {
1600
1634
  return false;
1601
1635
  }
1602
1636
 
1603
- fn disabled(side: *Side, ident: ID, log: anytype) !bool {
1604
- if (side.active.volatiles.disabled_move != 0) {
1637
+ fn disabled(side: *Side, ident: ID, options: anytype) !bool {
1638
+ if (side.active.volatiles.disable_move != 0) {
1605
1639
  // 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;
1640
+ const m = side.active.moves[side.active.volatiles.disable_move - 1].id;
1607
1641
  // side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
1608
1642
  const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
1609
1643
  m
@@ -1611,127 +1645,127 @@ fn disabled(side: *Side, ident: ID, log: anytype) !bool {
1611
1645
  side.last_selected_move;
1612
1646
  if (m != .None and m == last) {
1613
1647
  side.active.volatiles.Charging = false;
1614
- try log.disabled(ident, last);
1648
+ try options.log.disabled(ident, last);
1615
1649
  return true;
1616
1650
  }
1617
1651
  }
1618
1652
  return false;
1619
1653
  }
1620
1654
 
1621
- inline fn buildRage(battle: anytype, who: Player, log: anytype) !void {
1655
+ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1622
1656
  const side = battle.side(who);
1623
1657
  if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
1624
- try Effects.boost(battle, who, Move.get(.Rage), log);
1658
+ try Effects.boost(battle, who, Move.get(.Rage), options);
1625
1659
  }
1626
1660
  }
1627
1661
 
1628
- fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
1662
+ fn handleThrashing(battle: anytype, active: *ActivePokemon, player: Player, options: anytype) bool {
1629
1663
  var volatiles = &active.volatiles;
1630
1664
  assert(volatiles.Thrashing);
1631
1665
  if (volatiles.attacks > 0) return false;
1632
1666
 
1633
1667
  volatiles.Thrashing = false;
1634
1668
  volatiles.Confusion = true;
1635
- volatiles.confusion = Rolls.confusionDuration(battle);
1669
+ volatiles.confusion = Rolls.confusionDuration(battle, player, true, options);
1636
1670
  return true;
1637
1671
  }
1638
1672
 
1639
- inline fn onBegin(
1673
+ fn onBegin(
1640
1674
  battle: anytype,
1641
1675
  player: Player,
1642
1676
  move: Move.Data,
1643
1677
  mslot: u8,
1644
1678
  residual: *bool,
1645
- log: anytype,
1679
+ options: anytype,
1646
1680
  ) !void {
1647
1681
  assert(move.effect.onBegin());
1648
1682
  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),
1683
+ .Confusion => Effects.confusion(battle, player, move, options),
1684
+ .Conversion => Effects.conversion(battle, player, options),
1685
+ .FocusEnergy => Effects.focusEnergy(battle, player, options),
1686
+ .Haze => Effects.haze(battle, player, options),
1687
+ .Heal => Effects.heal(battle, player, options),
1688
+ .LeechSeed => Effects.leechSeed(battle, player, move, options),
1689
+ .LightScreen => Effects.lightScreen(battle, player, options),
1690
+ .Mimic => Effects.mimic(battle, player, move, mslot, options),
1691
+ .Mist => Effects.mist(battle, player, options),
1692
+ .Paralyze => Effects.paralyze(battle, player, move, options),
1693
+ .Poison => Effects.poison(battle, player, move, options),
1694
+ .Reflect => Effects.reflect(battle, player, options),
1695
+ .Splash => Effects.splash(battle, player, options),
1696
+ .Substitute => Effects.substitute(battle, player, residual, options),
1697
+ .SwitchAndTeleport => Effects.switchAndTeleport(battle, player, move, options),
1698
+ .Transform => Effects.transform(battle, player, options),
1665
1699
  else => unreachable,
1666
1700
  };
1667
1701
  }
1668
1702
 
1669
- inline fn onEnd(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
1703
+ fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1670
1704
  assert(move.effect.onEnd());
1671
1705
  return switch (move.effect) {
1672
- .Bide => Effects.bide(battle, player, log),
1706
+ .Bide => Effects.bide(battle, player, options),
1673
1707
  // zig fmt: off
1674
1708
  .AttackUp1, .AttackUp2, .DefenseUp1, .DefenseUp2,
1675
1709
  .EvasionUp1, .SpecialUp1, .SpecialUp2, .SpeedUp2 =>
1676
- Effects.boost(battle, player, move, log),
1710
+ Effects.boost(battle, player, move, options),
1677
1711
  .AccuracyDown1, .AttackDown1, .DefenseDown1, .DefenseDown2, .SpeedDown1 =>
1678
- Effects.unboost(battle, player, move, log),
1712
+ Effects.unboost(battle, player, move, options),
1679
1713
  // zig fmt: on
1680
- .Sleep => Effects.sleep(battle, player, move, log),
1714
+ .Sleep => Effects.sleep(battle, player, move, options),
1681
1715
  else => unreachable,
1682
1716
  };
1683
1717
  }
1684
1718
 
1685
- inline fn alwaysHappens(
1719
+ fn alwaysHappens(
1686
1720
  battle: anytype,
1687
1721
  player: Player,
1688
1722
  move: Move.Data,
1689
1723
  residual: *bool,
1690
- log: anytype,
1724
+ options: anytype,
1691
1725
  ) !void {
1692
1726
  assert(move.effect.alwaysHappens());
1693
1727
  return switch (move.effect) {
1694
- .DrainHP, .DreamEater => Effects.drainHP(battle, player, log),
1728
+ .DrainHP, .DreamEater => Effects.drainHP(battle, player, options),
1695
1729
  .Explode => Effects.explode(battle, player),
1696
- .PayDay => Effects.payDay(battle, player, log),
1730
+ .PayDay => Effects.payDay(battle, player, options),
1697
1731
  .Rage => Effects.rage(battle, player),
1698
- .Recoil => Effects.recoil(battle, player, residual, log),
1732
+ .Recoil => Effects.recoil(battle, player, residual, options),
1699
1733
  .JumpKick, .Binding => {},
1700
1734
  else => unreachable,
1701
1735
  };
1702
1736
  }
1703
1737
 
1704
- inline fn moveEffect(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
1738
+ fn moveEffect(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1705
1739
  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),
1740
+ .BurnChance1, .BurnChance2 => Effects.burnChance(battle, player, move, options),
1741
+ .ConfusionChance => Effects.confusion(battle, player, move, options),
1742
+ .FlinchChance1, .FlinchChance2 => Effects.flinchChance(battle, player, move, options),
1743
+ .FreezeChance => Effects.freezeChance(battle, player, move, options),
1744
+ .HyperBeam => Effects.hyperBeam(battle, player, options),
1711
1745
  .MultiHit, .DoubleHit, .Twineedle => unreachable,
1712
- .ParalyzeChance1, .ParalyzeChance2 => Effects.paralyzeChance(battle, player, move, log),
1713
- .PoisonChance1, .PoisonChance2 => Effects.poison(battle, player, move, log),
1746
+ .ParalyzeChance1, .ParalyzeChance2 => Effects.paralyzeChance(battle, player, move, options),
1747
+ .PoisonChance1, .PoisonChance2 => Effects.poison(battle, player, move, options),
1714
1748
  // zig fmt: off
1715
1749
  .AttackDownChance, .DefenseDownChance, .SpecialDownChance, .SpeedDownChance =>
1716
- Effects.unboost(battle, player, move, log),
1750
+ Effects.unboost(battle, player, move, options),
1717
1751
  // zig fmt: on
1718
1752
  else => {},
1719
1753
  };
1720
1754
  }
1721
1755
 
1722
1756
  pub const Effects = struct {
1723
- fn bide(battle: anytype, player: Player, log: anytype) !void {
1757
+ fn bide(battle: anytype, player: Player, options: anytype) !void {
1724
1758
  var side = battle.side(player);
1725
1759
 
1726
1760
  side.active.volatiles.Bide = true;
1727
1761
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1728
1762
  side.active.volatiles.state = 0;
1729
- side.active.volatiles.attacks = Rolls.bideThrashDuration(battle);
1763
+ side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
1730
1764
 
1731
- try log.start(battle.active(player), .Bide);
1765
+ try options.log.start(battle.active(player), .Bide);
1732
1766
  }
1733
1767
 
1734
- fn burnChance(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
1768
+ fn burnChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1735
1769
  var foe = battle.foe(player);
1736
1770
  var foe_stored = foe.stored();
1737
1771
 
@@ -1742,83 +1776,97 @@ pub const Effects = struct {
1742
1776
  // GLITCH: Freeze top move selection desync can occur if thawed Pokémon is slower
1743
1777
  if (Status.is(foe_stored.status, .FRZ)) {
1744
1778
  assert(move.type == .Fire);
1745
- try log.curestatus(battle.active(player.foe()), foe_stored.status, .Message);
1779
+ try options.log.curestatus(
1780
+ battle.active(player.foe()),
1781
+ foe_stored.status,
1782
+ .Message,
1783
+ );
1746
1784
  foe_stored.status = 0;
1747
1785
  }
1748
1786
  return;
1749
1787
  }
1750
1788
 
1751
1789
  if (foe.active.types.includes(move.type)) return;
1752
- if (!Rolls.secondaryChance(battle, move.effect == .BurnChance1)) return;
1790
+ if (!try Rolls.secondaryChance(battle, player, move.effect == .BurnChance1, options)) {
1791
+ return;
1792
+ }
1753
1793
 
1754
1794
  foe_stored.status = Status.init(.BRN);
1755
1795
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
1756
1796
 
1757
- try log.status(battle.active(player.foe()), foe_stored.status, .None);
1797
+ try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
1758
1798
  }
1759
1799
 
1760
- fn charge(battle: anytype, player: Player, log: anytype) !void {
1800
+ fn charge(battle: anytype, player: Player, options: anytype) !void {
1761
1801
  var side = battle.side(player);
1762
1802
  var volatiles = &side.active.volatiles;
1763
1803
 
1764
1804
  volatiles.Charging = true;
1765
1805
  const move = side.last_selected_move;
1766
1806
  if (move == .Fly or move == .Dig) volatiles.Invulnerable = true;
1767
- try log.laststill();
1768
- try log.prepare(battle.active(player), move);
1807
+ try options.log.laststill();
1808
+ try options.log.prepare(battle.active(player), move);
1769
1809
  }
1770
1810
 
1771
- fn confusion(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
1811
+ fn confusion(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1772
1812
  var foe = battle.foe(player);
1773
1813
  const sub = foe.active.volatiles.Substitute;
1774
1814
 
1775
1815
  if (move.effect == .ConfusionChance) {
1776
- if (!Rolls.confusionChance(battle)) return;
1816
+ if (!try Rolls.confusionChance(battle, player, options)) return;
1777
1817
  } else {
1778
1818
  if (showdown) {
1779
- if (!try checkHit(battle, player, move, log)) {
1819
+ if (!try checkHit(battle, player, move, options)) {
1780
1820
  return;
1781
1821
  } else if (sub) {
1782
- return log.fail(battle.active(player.foe()), .None);
1822
+ return options.log.fail(battle.active(player.foe()), .None);
1783
1823
  }
1784
1824
  } else {
1785
1825
  if (sub) {
1786
- return log.fail(battle.active(player.foe()), .None);
1787
- } else if (!try checkHit(battle, player, move, log)) {
1826
+ return options.log.fail(battle.active(player.foe()), .None);
1827
+ } else if (!try checkHit(battle, player, move, options)) {
1788
1828
  return;
1789
1829
  }
1790
1830
  }
1831
+ try options.chance.commit(player, .hit);
1791
1832
  }
1792
1833
 
1793
1834
  if (foe.active.volatiles.Confusion) return;
1794
1835
  foe.active.volatiles.Confusion = true;
1795
- foe.active.volatiles.confusion = Rolls.confusionDuration(battle);
1836
+ foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
1796
1837
 
1797
- try log.start(battle.active(player.foe()), .Confusion);
1838
+ try options.log.start(battle.active(player.foe()), .Confusion);
1798
1839
  }
1799
1840
 
1800
- fn conversion(battle: anytype, player: Player, log: anytype) !void {
1841
+ fn conversion(battle: anytype, player: Player, options: anytype) !void {
1801
1842
  const foe = battle.foe(player);
1802
1843
 
1803
1844
  if (foe.active.volatiles.Invulnerable) {
1804
- try log.lastmiss();
1805
- return log.miss(battle.active(player));
1845
+ try options.log.lastmiss();
1846
+ return options.log.miss(battle.active(player));
1806
1847
  }
1807
1848
 
1808
1849
  battle.side(player).active.types = foe.active.types;
1809
- return log.typechange(battle.active(player), foe.active.types, battle.active(player.foe()));
1850
+ return options.log.typechange(
1851
+ battle.active(player),
1852
+ foe.active.types,
1853
+ battle.active(player.foe()),
1854
+ );
1810
1855
  }
1811
1856
 
1812
- fn disable(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
1857
+ fn disable(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
1813
1858
  var foe = battle.foe(player);
1814
1859
  var volatiles = &foe.active.volatiles;
1815
1860
  const foe_ident = battle.active(player.foe());
1816
1861
 
1817
1862
  // Pokémon Showdown handles hit/miss earlier in doMove
1818
- if (!showdown and !try checkHit(battle, player, move, log)) return null;
1863
+ if (!showdown) {
1864
+ if (!try checkHit(battle, player, move, options)) return null;
1865
+ try options.chance.commit(player, .hit);
1866
+ }
1819
1867
 
1820
- if (volatiles.disabled_move != 0) {
1821
- try log.fail(foe_ident, .None);
1868
+ if (volatiles.disable_move != 0) {
1869
+ try options.log.fail(foe_ident, .None);
1822
1870
  return null;
1823
1871
  }
1824
1872
 
@@ -1834,7 +1882,7 @@ pub const Effects = struct {
1834
1882
  // Technically this is still considered simply a "miss" on the cartridge,
1835
1883
  // but diverging from Pokémon Showdown here would mostly just be pedantic
1836
1884
  if (n == 0) {
1837
- try log.fail(foe_ident, .None);
1885
+ try options.log.fail(foe_ident, .None);
1838
1886
  return null;
1839
1887
  } else if (err) {
1840
1888
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
@@ -1842,16 +1890,17 @@ pub const Effects = struct {
1842
1890
  return Result.Error;
1843
1891
  }
1844
1892
 
1845
- volatiles.disabled_move = @intCast(u3, Rolls.moveSlot(battle, &foe.active.moves, n));
1846
- volatiles.disabled_duration = Rolls.disableDuration(battle);
1893
+ volatiles.disable_move =
1894
+ @intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
1895
+ volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
1847
1896
 
1848
- try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
1897
+ const id = foe.active.move(volatiles.disable_move).id;
1898
+ try options.log.startEffect(foe_ident, .Disable, id);
1849
1899
  return null;
1850
1900
  }
1851
1901
 
1852
- fn drainHP(battle: anytype, player: Player, log: anytype) !void {
1853
- var side = battle.side(player);
1854
- var stored = side.stored();
1902
+ fn drainHP(battle: anytype, player: Player, options: anytype) !void {
1903
+ var stored = battle.side(player).stored();
1855
1904
 
1856
1905
  if (battle.last_damage == 0) {
1857
1906
  assert(showdown);
@@ -1864,7 +1913,7 @@ pub const Effects = struct {
1864
1913
  if (stored.hp == stored.stats.hp) return;
1865
1914
  stored.hp = @min(stored.stats.hp, stored.hp + drain);
1866
1915
 
1867
- try log.drain(battle.active(player), stored, battle.active(player.foe()));
1916
+ try options.log.drain(battle.active(player), stored, battle.active(player.foe()));
1868
1917
  }
1869
1918
 
1870
1919
  fn explode(battle: anytype, player: Player) !void {
@@ -1878,26 +1927,28 @@ pub const Effects = struct {
1878
1927
  side.active.volatiles.LeechSeed = false;
1879
1928
  }
1880
1929
 
1881
- fn flinchChance(battle: anytype, player: Player, move: Move.Data) void {
1930
+ fn flinchChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1882
1931
  var volatiles = &battle.foe(player).active.volatiles;
1883
1932
 
1884
1933
  if (volatiles.Substitute) return;
1885
- if (!Rolls.secondaryChance(battle, move.effect == .FlinchChance1)) return;
1934
+ if (!try Rolls.secondaryChance(battle, player, move.effect == .FlinchChance1, options)) {
1935
+ return;
1936
+ }
1886
1937
 
1887
1938
  volatiles.Flinch = true;
1888
1939
  volatiles.Recharging = false;
1889
1940
  }
1890
1941
 
1891
- fn focusEnergy(battle: anytype, player: Player, log: anytype) !void {
1942
+ fn focusEnergy(battle: anytype, player: Player, options: anytype) !void {
1892
1943
  var side = battle.side(player);
1893
1944
 
1894
1945
  if (side.active.volatiles.FocusEnergy) return;
1895
1946
  side.active.volatiles.FocusEnergy = true;
1896
1947
 
1897
- try log.start(battle.active(player), .FocusEnergy);
1948
+ try options.log.start(battle.active(player), .FocusEnergy);
1898
1949
  }
1899
1950
 
1900
- fn freezeChance(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
1951
+ fn freezeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1901
1952
  var foe = battle.foe(player);
1902
1953
  var foe_stored = foe.stored();
1903
1954
  const foe_ident = battle.active(player.foe());
@@ -1909,17 +1960,18 @@ pub const Effects = struct {
1909
1960
  }
1910
1961
 
1911
1962
  if (foe.active.types.includes(move.type)) return;
1912
- if (!Rolls.secondaryChance(battle, true)) return;
1963
+ if (!try Rolls.secondaryChance(battle, player, true, options)) return;
1913
1964
  // Freeze Clause Mod
1914
1965
  if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1915
1966
 
1916
1967
  foe_stored.status = Status.init(.FRZ);
1917
1968
  // GLITCH: Hyper Beam recharging status is not cleared
1918
1969
 
1919
- try log.status(foe_ident, foe_stored.status, .None);
1970
+ try options.log.status(foe_ident, foe_stored.status, .None);
1920
1971
  }
1921
1972
 
1922
- fn haze(battle: anytype, player: Player, log: anytype) !void {
1973
+ fn haze(battle: anytype, player: Player, options: anytype) !void {
1974
+ var log = options.log;
1923
1975
  var side = battle.side(player);
1924
1976
  var foe = battle.foe(player);
1925
1977
 
@@ -1941,7 +1993,7 @@ pub const Effects = struct {
1941
1993
  // Pokémon Showdown clears P1 then P2 instead of status -> side -> foe
1942
1994
  if (showdown) {
1943
1995
  for (&battle.sides, 0..) |*s, i| {
1944
- const p = @intToEnum(Player, i);
1996
+ const p: Player = @enumFromInt(i);
1945
1997
  // Pokémon Showdown incorrectly does not prevent sleep/freeze from moving
1946
1998
  if (p != player and Status.any(s.stored().status)) {
1947
1999
  try log.curestatus(foe_ident, foe_stored.status, .Silent);
@@ -1950,7 +2002,7 @@ pub const Effects = struct {
1950
2002
  s.stored().status = Status.init(.PSN);
1951
2003
  try log.status(battle.active(p), s.stored().status, .None);
1952
2004
  }
1953
- try clearVolatiles(battle, p, log);
2005
+ try clearVolatiles(battle, p, p != player, options);
1954
2006
  }
1955
2007
  } else {
1956
2008
  if (Status.any(foe_stored.status)) {
@@ -1960,71 +2012,76 @@ pub const Effects = struct {
1960
2012
  try log.curestatus(foe_ident, foe_stored.status, .Silent);
1961
2013
  foe_stored.status = 0;
1962
2014
  }
1963
- try clearVolatiles(battle, player, log);
1964
- try clearVolatiles(battle, player.foe(), log);
2015
+ try clearVolatiles(battle, player, false, options);
2016
+ try clearVolatiles(battle, player.foe(), true, options);
1965
2017
  }
1966
2018
  }
1967
2019
 
1968
- fn heal(battle: anytype, player: Player, log: anytype) !void {
2020
+ fn heal(battle: anytype, player: Player, options: anytype) !void {
1969
2021
  var side = battle.side(player);
1970
2022
  var stored = side.stored();
1971
2023
  const ident = battle.active(player);
1972
2024
 
1973
2025
  // GLITCH: HP recovery move failure glitches
1974
2026
  const delta = stored.stats.hp - stored.hp;
1975
- if (delta == 0 or delta & 255 == 255) return try log.fail(ident, .None);
2027
+ if (delta == 0 or (delta & 255 == 255 and stored.hp % 256 != 0)) {
2028
+ return try options.log.fail(ident, .None);
2029
+ }
1976
2030
 
1977
2031
  const rest = side.last_selected_move == .Rest;
1978
2032
  if (rest) {
1979
2033
  // Adding the sleep status runs the sleep condition handler to roll duration
1980
2034
  if (showdown) battle.rng.advance(1);
1981
2035
  stored.status = Status.slf(2);
1982
- try log.statusFrom(ident, stored.status, Move.Rest);
2036
+ try options.log.statusFrom(ident, stored.status, Move.Rest);
1983
2037
  stored.hp = stored.stats.hp;
1984
2038
  } else {
1985
2039
  stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
1986
2040
  }
1987
- try log.heal(ident, stored, if (rest) Heal.Silent else Heal.None);
2041
+ try options.log.heal(ident, stored, if (rest) Heal.Silent else Heal.None);
1988
2042
  }
1989
2043
 
1990
- fn hyperBeam(battle: anytype, player: Player, log: anytype) !void {
2044
+ fn hyperBeam(battle: anytype, player: Player, options: anytype) !void {
1991
2045
  battle.side(player).active.volatiles.Recharging = true;
1992
- try log.mustrecharge(battle.active(player));
2046
+ try options.log.mustrecharge(battle.active(player));
1993
2047
  }
1994
2048
 
1995
- fn leechSeed(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
2049
+ fn leechSeed(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1996
2050
  var foe = battle.foe(player);
1997
2051
 
1998
2052
  if (showdown) {
1999
2053
  // Invulnerability trumps type immunity on Pokémon Showdown
2000
2054
  if (!foe.active.volatiles.Invulnerable and foe.active.types.includes(.Grass)) {
2001
- return log.immune(battle.active(player.foe()), .None);
2055
+ return options.log.immune(battle.active(player.foe()), .None);
2002
2056
  }
2003
- if (!try checkHit(battle, player, move, log)) return;
2057
+ if (!try checkHit(battle, player, move, options)) return;
2004
2058
  if (foe.active.volatiles.LeechSeed) return;
2005
2059
  } else {
2006
- if (!try checkHit(battle, player, move, log)) return;
2060
+ if (!try checkHit(battle, player, move, options)) return;
2007
2061
  if (foe.active.types.includes(.Grass) or foe.active.volatiles.LeechSeed) {
2008
- try log.lastmiss();
2009
- return log.miss(battle.active(player));
2062
+ try options.log.lastmiss();
2063
+ return options.log.miss(battle.active(player));
2010
2064
  }
2011
2065
  }
2012
2066
 
2067
+ try options.chance.commit(player, .hit);
2013
2068
  foe.active.volatiles.LeechSeed = true;
2014
2069
 
2015
- try log.start(battle.active(player.foe()), .LeechSeed);
2070
+ try options.log.start(battle.active(player.foe()), .LeechSeed);
2016
2071
  }
2017
2072
 
2018
- fn lightScreen(battle: anytype, player: Player, log: anytype) !void {
2073
+ fn lightScreen(battle: anytype, player: Player, options: anytype) !void {
2019
2074
  var side = battle.side(player);
2020
2075
 
2021
- if (side.active.volatiles.LightScreen) return log.fail(battle.active(player), .None);
2076
+ if (side.active.volatiles.LightScreen) {
2077
+ return options.log.fail(battle.active(player), .None);
2078
+ }
2022
2079
  side.active.volatiles.LightScreen = true;
2023
2080
 
2024
- try log.start(battle.active(player), .LightScreen);
2081
+ try options.log.start(battle.active(player), .LightScreen);
2025
2082
  }
2026
2083
 
2027
- fn mimic(battle: anytype, player: Player, move: Move.Data, mslot: u8, log: anytype) !void {
2084
+ fn mimic(battle: anytype, player: Player, move: Move.Data, mslot: u8, options: anytype) !void {
2028
2085
  var side = battle.side(player);
2029
2086
  var foe = battle.foe(player);
2030
2087
 
@@ -2040,7 +2097,7 @@ pub const Effects = struct {
2040
2097
  const has_mimic = has_mimic: {
2041
2098
  for (side.active.moves, 0..) |m, i| {
2042
2099
  if (m.id == .Mimic) {
2043
- oslot = @intCast(u8, i + 1);
2100
+ oslot = @intCast(i + 1);
2044
2101
  break :has_mimic true;
2045
2102
  }
2046
2103
  }
@@ -2048,38 +2105,43 @@ pub const Effects = struct {
2048
2105
  };
2049
2106
  if (!has_mimic) {
2050
2107
  // 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);
2108
+ if (!try checkHit(battle, player, move, options)) return;
2109
+ try options.chance.commit(player, .hit);
2110
+ return try options.log.fail(battle.active(player.foe()), .None);
2053
2111
  }
2054
2112
  }
2055
- if (!try checkHit(battle, player, move, log)) return;
2113
+ if (!try checkHit(battle, player, move, options)) return;
2114
+ try options.chance.commit(player, .hit);
2056
2115
 
2057
- const rslot = Rolls.moveSlot(battle, &foe.active.moves, 0);
2116
+ const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
2058
2117
  side.active.move(oslot).id = foe.active.move(rslot).id;
2059
2118
 
2060
- try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
2119
+ try options.log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
2061
2120
  }
2062
2121
 
2063
- fn mist(battle: anytype, player: Player, log: anytype) !void {
2122
+ fn mist(battle: anytype, player: Player, options: anytype) !void {
2064
2123
  var side = battle.side(player);
2065
2124
 
2066
2125
  if (side.active.volatiles.Mist) return;
2067
2126
  side.active.volatiles.Mist = true;
2068
2127
 
2069
- try log.start(battle.active(player), .Mist);
2128
+ try options.log.start(battle.active(player), .Mist);
2070
2129
  }
2071
2130
 
2072
- fn multiHit(battle: anytype, player: Player, move: Move.Data) void {
2131
+ fn multiHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
2073
2132
  var side = battle.side(player);
2074
2133
 
2075
2134
  assert(!side.active.volatiles.MultiHit);
2076
2135
  side.active.volatiles.MultiHit = true;
2077
2136
 
2078
- side.active.volatiles.attacks =
2079
- if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
2137
+ side.active.volatiles.attacks = if (move.effect == .MultiHit)
2138
+ try Rolls.distribution(battle, .MultiHit, player, options)
2139
+ else
2140
+ 2;
2080
2141
  }
2081
2142
 
2082
- fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
2143
+ fn paralyze(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
2144
+ var log = options.log;
2083
2145
  var foe = battle.foe(player);
2084
2146
  var foe_stored = foe.stored();
2085
2147
  const foe_ident = battle.active(player.foe());
@@ -2090,7 +2152,7 @@ pub const Effects = struct {
2090
2152
  if (showdown) {
2091
2153
  // Invulnerability trumps type immunity on Pokémon Showdown
2092
2154
  if (immune and !foe.active.volatiles.Invulnerable) return log.immune(foe_ident, .None);
2093
- if (!try checkHit(battle, player, move, log)) return;
2155
+ if (!try checkHit(battle, player, move, options)) return;
2094
2156
  }
2095
2157
  if (Status.any(foe_stored.status)) {
2096
2158
  return log.fail(
@@ -2100,8 +2162,9 @@ pub const Effects = struct {
2100
2162
  }
2101
2163
  if (!showdown) {
2102
2164
  if (immune) return log.immune(foe_ident, .None);
2103
- if (!try checkHit(battle, player, move, log)) return;
2165
+ if (!try checkHit(battle, player, move, options)) return;
2104
2166
  }
2167
+ try options.chance.commit(player, .hit);
2105
2168
 
2106
2169
  foe_stored.status = Status.init(.PAR);
2107
2170
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
@@ -2109,7 +2172,7 @@ pub const Effects = struct {
2109
2172
  try log.status(foe_ident, foe_stored.status, .None);
2110
2173
  }
2111
2174
 
2112
- fn paralyzeChance(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
2175
+ fn paralyzeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
2113
2176
  var foe = battle.foe(player);
2114
2177
  var foe_stored = foe.stored();
2115
2178
 
@@ -2121,25 +2184,30 @@ pub const Effects = struct {
2121
2184
 
2122
2185
  // Body Slam can't paralyze a Normal type Pokémon
2123
2186
  if (foe.active.types.includes(move.type)) return;
2124
- if (!Rolls.secondaryChance(battle, move.effect == .ParalyzeChance1)) return;
2187
+ if (!try Rolls.secondaryChance(battle, player, move.effect == .ParalyzeChance1, options)) {
2188
+ return;
2189
+ }
2125
2190
 
2126
2191
  foe_stored.status = Status.init(.PAR);
2127
2192
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2128
2193
 
2129
- try log.status(battle.active(player.foe()), foe_stored.status, .None);
2194
+ try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
2130
2195
  }
2131
2196
 
2132
- fn payDay(battle: anytype, player: Player, log: anytype) !void {
2133
- if (!showdown or !battle.foe(player).active.volatiles.Substitute) try log.fieldactivate();
2197
+ fn payDay(battle: anytype, player: Player, options: anytype) !void {
2198
+ if (!showdown or !battle.foe(player).active.volatiles.Substitute) {
2199
+ try options.log.fieldactivate();
2200
+ }
2134
2201
  }
2135
2202
 
2136
- fn poison(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
2203
+ fn poison(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
2204
+ var log = options.log;
2137
2205
  var foe = battle.foe(player);
2138
2206
  var foe_stored = foe.stored();
2139
2207
  const foe_ident = battle.active(player.foe());
2140
2208
  const toxic = battle.side(player).last_selected_move == .Toxic;
2141
2209
 
2142
- if (showdown and move.effect == .Poison and !try checkHit(battle, player, move, log)) {
2210
+ if (showdown and move.effect == .Poison and !try checkHit(battle, player, move, options)) {
2143
2211
  return;
2144
2212
  } else if (foe.active.volatiles.Substitute) {
2145
2213
  if (move.effect != .Poison) return;
@@ -2163,8 +2231,13 @@ pub const Effects = struct {
2163
2231
  }
2164
2232
 
2165
2233
  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;
2234
+ if (!showdown and !try checkHit(battle, player, move, options)) return;
2235
+ try options.chance.commit(player, .hit);
2236
+ } else {
2237
+ if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
2238
+ return;
2239
+ }
2240
+ }
2168
2241
 
2169
2242
  foe_stored.status = Status.init(.PSN);
2170
2243
  if (toxic) {
@@ -2182,28 +2255,36 @@ pub const Effects = struct {
2182
2255
  volatiles.Rage = true;
2183
2256
  }
2184
2257
 
2185
- fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
2258
+ fn recoil(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
2186
2259
  var side = battle.side(player);
2187
2260
  var stored = side.stored();
2188
2261
 
2189
- const damage = @intCast(i16, @max(battle.last_damage /
2262
+ assert(showdown or battle.last_damage > 0);
2263
+ if (showdown and battle.last_damage == 0) return;
2264
+
2265
+ const damage: i16 = @intCast(@max(battle.last_damage /
2190
2266
  @as(u8, if (side.last_selected_move == .Struggle) 2 else 4), 1));
2191
- stored.hp = @intCast(u16, @max(@intCast(i16, stored.hp) - damage, 0));
2267
+ stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
2192
2268
 
2193
- try log.damageOf(battle.active(player), stored, .RecoilOf, battle.active(player.foe()));
2269
+ try options.log.damageOf(
2270
+ battle.active(player),
2271
+ stored,
2272
+ .RecoilOf,
2273
+ battle.active(player.foe()),
2274
+ );
2194
2275
  if (showdown and stored.hp == 0) residual.* = false;
2195
2276
  }
2196
2277
 
2197
- fn reflect(battle: anytype, player: Player, log: anytype) !void {
2278
+ fn reflect(battle: anytype, player: Player, options: anytype) !void {
2198
2279
  var side = battle.side(player);
2199
2280
 
2200
- if (side.active.volatiles.Reflect) return log.fail(battle.active(player), .None);
2281
+ if (side.active.volatiles.Reflect) return options.log.fail(battle.active(player), .None);
2201
2282
  side.active.volatiles.Reflect = true;
2202
2283
 
2203
- try log.start(battle.active(player), .Reflect);
2284
+ try options.log.start(battle.active(player), .Reflect);
2204
2285
  }
2205
2286
 
2206
- fn sleep(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
2287
+ fn sleep(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
2207
2288
  var foe = battle.foe(player);
2208
2289
  var foe_stored = foe.stored();
2209
2290
  const foe_ident = battle.active(player.foe());
@@ -2214,13 +2295,14 @@ pub const Effects = struct {
2214
2295
  // the Sleep Clause Mod might activate, causing us to not actually bypass
2215
2296
  } else {
2216
2297
  if (Status.any(foe_stored.status)) {
2217
- return log.fail(
2298
+ return options.log.fail(
2218
2299
  foe_ident,
2219
2300
  if (Status.is(foe_stored.status, .SLP)) .Sleep else .None,
2220
2301
  );
2221
2302
  }
2222
2303
  // 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;
2304
+ if (!showdown and !try checkHit(battle, player, move, options)) return;
2305
+ try options.chance.commit(player, .hit);
2224
2306
  }
2225
2307
 
2226
2308
  // Sleep Clause Mod
@@ -2231,31 +2313,32 @@ pub const Effects = struct {
2231
2313
  }
2232
2314
  foe.active.volatiles.Recharging = false;
2233
2315
 
2234
- foe_stored.status = Status.slp(Rolls.sleepDuration(battle));
2235
- try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
2316
+ foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2317
+ const last = battle.side(player).last_selected_move;
2318
+ try options.log.statusFrom(foe_ident, foe_stored.status, last);
2236
2319
  }
2237
2320
 
2238
- fn splash(battle: anytype, player: Player, log: anytype) !void {
2239
- try log.activate(battle.active(player), .Splash);
2321
+ fn splash(battle: anytype, player: Player, options: anytype) !void {
2322
+ try options.log.activate(battle.active(player), .Splash);
2240
2323
  }
2241
2324
 
2242
- fn substitute(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
2325
+ fn substitute(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
2243
2326
  var side = battle.side(player);
2244
2327
  if (side.active.volatiles.Substitute) {
2245
- try log.fail(battle.active(player), .Substitute);
2328
+ try options.log.fail(battle.active(player), .Substitute);
2246
2329
  return;
2247
2330
  }
2248
2331
 
2249
2332
  assert(side.stored().stats.hp <= 1023);
2250
2333
  // 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);
2334
+ const hp: u8 = @intCast(side.stored().stats.hp / 4);
2252
2335
  // Pokénon Showdown incorrectly checks for 1/4 HP based on `target.maxhp / 4` which returns
2253
2336
  // a floating point value and thus only correctly implements the Substitute 1/4 glitch when
2254
2337
  // the target's HP is exactly divisible by 4 (here we're using an inlined divCeil routine to
2255
2338
  // avoid having to convert to floating point)
2256
2339
  const required_hp = if (showdown) @divFloor(side.stored().stats.hp - 1, 4) + 1 else hp;
2257
2340
  if (side.stored().hp < required_hp) {
2258
- try log.fail(battle.active(player), .Weak);
2341
+ try options.log.fail(battle.active(player), .Weak);
2259
2342
  return;
2260
2343
  }
2261
2344
 
@@ -2263,31 +2346,37 @@ pub const Effects = struct {
2263
2346
  side.stored().hp -= hp;
2264
2347
  side.active.volatiles.substitute = hp + 1;
2265
2348
  side.active.volatiles.Substitute = true;
2266
- try log.start(battle.active(player), .Substitute);
2349
+ try options.log.start(battle.active(player), .Substitute);
2267
2350
  if (hp > 0) {
2268
- try log.damage(battle.active(player), side.stored(), .None);
2351
+ try options.log.damage(battle.active(player), side.stored(), .None);
2269
2352
  if (showdown and side.stored().hp == 0) residual.* = false;
2270
2353
  }
2271
2354
  }
2272
2355
 
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
-
2356
+ fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
2276
2357
  // 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;
2358
+ if (showdown) {
2359
+ if (battle.side(player).last_selected_move == .Teleport) return;
2360
+ if (try checkHit(battle, player, move, options)) {
2361
+ try options.chance.commit(player, .hit);
2362
+ }
2363
+ battle.last_damage = 0;
2364
+ } else {
2365
+ try options.log.fail(battle.active(player), .None);
2366
+ try options.log.laststill();
2367
+ }
2279
2368
  }
2280
2369
 
2281
- fn thrashing(battle: anytype, player: Player) void {
2370
+ fn thrashing(battle: anytype, player: Player, options: anytype) void {
2282
2371
  var volatiles = &battle.side(player).active.volatiles;
2283
2372
  assert(!volatiles.Thrashing);
2284
2373
  assert(!volatiles.Bide);
2285
2374
 
2286
2375
  volatiles.Thrashing = true;
2287
- volatiles.attacks = Rolls.bideThrashDuration(battle);
2376
+ volatiles.attacks = Rolls.attackingDuration(battle, player, options);
2288
2377
  }
2289
2378
 
2290
- fn transform(battle: anytype, player: Player, log: anytype) !void {
2379
+ fn transform(battle: anytype, player: Player, options: anytype) !void {
2291
2380
  var side = battle.side(player);
2292
2381
  const foe = battle.foe(player);
2293
2382
  const foe_ident = battle.active(player.foe());
@@ -2313,10 +2402,10 @@ pub const Effects = struct {
2313
2402
  side.active.moves[i].pp = if (m.id != .None) 5 else 0;
2314
2403
  }
2315
2404
 
2316
- try log.transform(battle.active(player), foe_ident);
2405
+ try options.log.transform(battle.active(player), foe_ident);
2317
2406
  }
2318
2407
 
2319
- fn binding(battle: anytype, player: Player, rewrap: bool) void {
2408
+ fn binding(battle: anytype, player: Player, rewrap: bool, options: anytype) !void {
2320
2409
  var side = battle.side(player);
2321
2410
  var foe = battle.foe(player);
2322
2411
 
@@ -2328,10 +2417,12 @@ pub const Effects = struct {
2328
2417
  foe.active.volatiles.Recharging = false;
2329
2418
  } else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
2330
2419
 
2331
- side.active.volatiles.attacks = Rolls.distribution(battle) - 1;
2420
+ side.active.volatiles.attacks =
2421
+ try Rolls.distribution(battle, .Binding, player, options) - 1;
2332
2422
  }
2333
2423
 
2334
- fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
2424
+ fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
2425
+ var log = options.log;
2335
2426
  var side = battle.side(player);
2336
2427
  const ident = battle.active(player);
2337
2428
 
@@ -2343,7 +2434,7 @@ pub const Effects = struct {
2343
2434
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2344
2435
  if (boosts.atk == 6) return try log.fail(ident, .None);
2345
2436
  const n: u2 = if (move.effect == .AttackUp2) 2 else 1;
2346
- boosts.atk = @intCast(i4, @min(6, @as(i8, boosts.atk) + n));
2437
+ boosts.atk = @as(i4, @intCast(@min(6, @as(i8, boosts.atk) + n)));
2347
2438
  const reason = if (move.effect == .Rage) Boost.Rage else Boost.Attack;
2348
2439
  if (stats.atk == MAX_STAT_VALUE) {
2349
2440
  boosts.atk -= 1;
@@ -2354,7 +2445,7 @@ pub const Effects = struct {
2354
2445
  }
2355
2446
  return try log.fail(ident, .None);
2356
2447
  }
2357
- var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2448
+ var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2358
2449
  const stat = unmodifiedStats(battle, side).atk;
2359
2450
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2360
2451
  try log.boost(ident, reason, n);
@@ -2365,7 +2456,7 @@ pub const Effects = struct {
2365
2456
  assert(boosts.def >= -6 and boosts.def <= 6);
2366
2457
  if (boosts.def == 6) return try log.fail(ident, .None);
2367
2458
  const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
2368
- boosts.def = @intCast(i4, @min(6, @as(i8, boosts.def) + n));
2459
+ boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
2369
2460
  if (stats.def == MAX_STAT_VALUE) {
2370
2461
  boosts.def -= 1;
2371
2462
  if (showdown) {
@@ -2374,7 +2465,7 @@ pub const Effects = struct {
2374
2465
  }
2375
2466
  return try log.fail(ident, .None);
2376
2467
  }
2377
- var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2468
+ var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2378
2469
  const stat = unmodifiedStats(battle, side).def;
2379
2470
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2380
2471
  try log.boost(ident, .Defense, n);
@@ -2382,7 +2473,7 @@ pub const Effects = struct {
2382
2473
  .SpeedUp2 => {
2383
2474
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2384
2475
  if (boosts.spe == 6) return try log.fail(ident, .None);
2385
- boosts.spe = @intCast(i4, @min(6, @as(i8, boosts.spe) + 2));
2476
+ boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
2386
2477
  if (stats.spe == MAX_STAT_VALUE) {
2387
2478
  boosts.spe -= 1;
2388
2479
  if (showdown) {
@@ -2391,7 +2482,7 @@ pub const Effects = struct {
2391
2482
  }
2392
2483
  return try log.fail(ident, .None);
2393
2484
  }
2394
- var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2485
+ var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2395
2486
  const stat = unmodifiedStats(battle, side).spe;
2396
2487
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2397
2488
  try log.boost(ident, .Speed, 2);
@@ -2400,7 +2491,7 @@ pub const Effects = struct {
2400
2491
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2401
2492
  if (boosts.spc == 6) return try log.fail(ident, .None);
2402
2493
  const n: u2 = if (move.effect == .SpecialUp2) 2 else 1;
2403
- boosts.spc = @intCast(i4, @min(6, @as(i8, boosts.spc) + n));
2494
+ boosts.spc = @intCast(@min(6, @as(i8, boosts.spc) + n));
2404
2495
  if (stats.spc == MAX_STAT_VALUE) {
2405
2496
  boosts.spc -= 1;
2406
2497
  if (showdown) {
@@ -2411,7 +2502,7 @@ pub const Effects = struct {
2411
2502
  }
2412
2503
  return try log.fail(ident, .None);
2413
2504
  }
2414
- var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2505
+ var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2415
2506
  const stat = unmodifiedStats(battle, side).spc;
2416
2507
  stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2417
2508
  try log.boost(ident, .SpecialAttack, n);
@@ -2420,7 +2511,7 @@ pub const Effects = struct {
2420
2511
  .EvasionUp1 => {
2421
2512
  assert(boosts.evasion >= -6 and boosts.evasion <= 6);
2422
2513
  if (boosts.evasion == 6) return try log.fail(ident, .None);
2423
- boosts.evasion = @intCast(i4, @min(6, @as(i8, boosts.evasion) + 1));
2514
+ boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
2424
2515
  try log.boost(ident, .Evasion, 1);
2425
2516
  },
2426
2517
  else => unreachable,
@@ -2430,28 +2521,32 @@ pub const Effects = struct {
2430
2521
  statusModify(battle.foe(player).stored().status, &battle.foe(player).active.stats);
2431
2522
  }
2432
2523
 
2433
- fn unboost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
2524
+ fn unboost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
2525
+ var log = options.log;
2434
2526
  var foe = battle.foe(player);
2435
2527
  const foe_ident = battle.active(player.foe());
2528
+ const secondary = move.effect.isStatDownChance();
2436
2529
 
2437
- if (foe.active.volatiles.Substitute) {
2438
- return if (!move.effect.isStatDownChance()) log.fail(foe_ident, .None);
2439
- }
2530
+ if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(foe_ident, .None);
2440
2531
 
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
2532
+ if (secondary) {
2533
+ const proc = try Rolls.unboost(battle, player, options);
2534
+ if (!proc or foe.active.volatiles.Invulnerable) return;
2535
+ } else {
2536
+ // checkHit already checks for Invulnerable
2537
+ if (!showdown and !try checkHit(battle, player, move, options)) return;
2538
+ try options.chance.commit(player, .hit);
2445
2539
  }
2446
2540
 
2447
2541
  var stats = &foe.active.stats;
2448
2542
  var boosts = &foe.active.boosts;
2543
+ const fail = showdown or !secondary;
2449
2544
 
2450
2545
  switch (move.effect) {
2451
2546
  .AttackDown1, .AttackDownChance => {
2452
2547
  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));
2548
+ if (boosts.atk == -6) return if (fail) try log.fail(foe_ident, .None);
2549
+ boosts.atk = @intCast(@max(-6, @as(i8, boosts.atk) - 1));
2455
2550
  if (stats.atk == 1) {
2456
2551
  boosts.atk += 1;
2457
2552
  if (showdown) {
@@ -2460,16 +2555,16 @@ pub const Effects = struct {
2460
2555
  }
2461
2556
  return try log.fail(foe_ident, .None);
2462
2557
  }
2463
- var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2558
+ var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2464
2559
  const stat = unmodifiedStats(battle, foe).atk;
2465
2560
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2466
2561
  try log.boost(foe_ident, .Attack, -1);
2467
2562
  },
2468
2563
  .DefenseDown1, .DefenseDown2, .DefenseDownChance => {
2469
2564
  assert(boosts.def >= -6 and boosts.def <= 6);
2470
- if (boosts.def == -6) return try log.fail(foe_ident, .None);
2565
+ if (boosts.def == -6) return if (fail) try log.fail(foe_ident, .None);
2471
2566
  const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
2472
- boosts.def = @intCast(i4, @max(-6, @as(i8, boosts.def) - n));
2567
+ boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
2473
2568
  if (stats.def == 1) {
2474
2569
  boosts.def += 1;
2475
2570
  if (showdown) {
@@ -2478,15 +2573,15 @@ pub const Effects = struct {
2478
2573
  }
2479
2574
  return try log.fail(foe_ident, .None);
2480
2575
  }
2481
- var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2576
+ var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2482
2577
  const stat = unmodifiedStats(battle, foe).def;
2483
2578
  stats.def = @max(1, stat * mod[0] / mod[1]);
2484
2579
  try log.boost(foe_ident, .Defense, -@as(i8, n));
2485
2580
  },
2486
2581
  .SpeedDown1, .SpeedDownChance => {
2487
2582
  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));
2583
+ if (boosts.spe == -6) return if (fail) try log.fail(foe_ident, .None);
2584
+ boosts.spe = @intCast(@max(-6, @as(i8, boosts.spe) - 1));
2490
2585
  if (stats.spe == 1) {
2491
2586
  boosts.spe += 1;
2492
2587
  if (showdown) {
@@ -2495,7 +2590,7 @@ pub const Effects = struct {
2495
2590
  }
2496
2591
  return try log.fail(foe_ident, .None);
2497
2592
  }
2498
- var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2593
+ var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2499
2594
  const stat = unmodifiedStats(battle, foe).spe;
2500
2595
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2501
2596
  try log.boost(foe_ident, .Speed, -1);
@@ -2503,8 +2598,8 @@ pub const Effects = struct {
2503
2598
  },
2504
2599
  .SpecialDownChance => {
2505
2600
  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));
2601
+ if (boosts.spc == -6) return if (fail) try log.fail(foe_ident, .None);
2602
+ boosts.spc = @intCast(@max(-6, @as(i8, boosts.spc) - 1));
2508
2603
  if (stats.spc == 1) {
2509
2604
  boosts.spc += 1;
2510
2605
  if (showdown) {
@@ -2515,7 +2610,7 @@ pub const Effects = struct {
2515
2610
  }
2516
2611
  return try log.fail(foe_ident, .None);
2517
2612
  }
2518
- var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2613
+ var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2519
2614
  const stat = unmodifiedStats(battle, foe).spc;
2520
2615
  stats.spc = @max(1, stat * mod[0] / mod[1]);
2521
2616
  try log.boost(foe_ident, .SpecialAttack, -1);
@@ -2523,8 +2618,8 @@ pub const Effects = struct {
2523
2618
  },
2524
2619
  .AccuracyDown1 => {
2525
2620
  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));
2621
+ if (boosts.accuracy == -6) return if (fail) try log.fail(foe_ident, .None);
2622
+ boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
2528
2623
  try log.boost(foe_ident, .Accuracy, -1);
2529
2624
  },
2530
2625
  else => unreachable,
@@ -2549,19 +2644,22 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
2549
2644
  }
2550
2645
  }
2551
2646
 
2552
- inline fn isForced(active: ActivePokemon) bool {
2647
+ fn isForced(active: anytype) bool {
2553
2648
  return active.volatiles.Recharging or active.volatiles.Rage or
2554
2649
  active.volatiles.Thrashing or active.volatiles.Charging;
2555
2650
  }
2556
2651
 
2557
- fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
2652
+ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !void {
2653
+ var log = options.log;
2558
2654
  var side = battle.side(who);
2559
2655
  var volatiles = &side.active.volatiles;
2560
2656
  const ident = battle.active(who);
2561
2657
 
2562
- if (volatiles.disabled_move != 0) {
2563
- volatiles.disabled_move = 0;
2564
- volatiles.disabled_duration = 0;
2658
+ options.chance.clearDurations(who, clear);
2659
+
2660
+ if (volatiles.disable_move != 0) {
2661
+ volatiles.disable_move = 0;
2662
+ volatiles.disable_duration = 0;
2565
2663
  try log.end(ident, .DisableSilent);
2566
2664
  }
2567
2665
  if (volatiles.Confusion) {
@@ -2571,15 +2669,15 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
2571
2669
  }
2572
2670
  if (volatiles.Mist) {
2573
2671
  volatiles.Mist = false;
2574
- try log.end(ident, .Mist);
2672
+ try log.end(ident, .MistSilent);
2575
2673
  }
2576
2674
  if (volatiles.FocusEnergy) {
2577
2675
  volatiles.FocusEnergy = false;
2578
- try log.end(ident, .FocusEnergy);
2676
+ try log.end(ident, .FocusEnergySilent);
2579
2677
  }
2580
2678
  if (volatiles.LeechSeed) {
2581
2679
  volatiles.LeechSeed = false;
2582
- try log.end(ident, .LeechSeed);
2680
+ try log.end(ident, .LeechSeedSilent);
2583
2681
  }
2584
2682
  if (!showdown and volatiles.Toxic) {
2585
2683
  volatiles.Toxic = false;
@@ -2587,152 +2685,313 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
2587
2685
  }
2588
2686
  if (volatiles.LightScreen) {
2589
2687
  volatiles.LightScreen = false;
2590
- try log.end(ident, .LightScreen);
2688
+ try log.end(ident, .LightScreenSilent);
2591
2689
  }
2592
2690
  if (volatiles.Reflect) {
2593
2691
  volatiles.Reflect = false;
2594
- try log.end(ident, .Reflect);
2692
+ try log.end(ident, .ReflectSilent);
2595
2693
  }
2596
2694
  if (showdown and volatiles.Toxic) {
2597
2695
  volatiles.Toxic = false;
2598
2696
  // Pokémon Showdown erroneously clears the toxic counter
2599
2697
  volatiles.toxic = 0;
2600
- try log.end(ident, .Toxic);
2698
+ try log.end(ident, .ToxicSilent);
2601
2699
  }
2602
2700
  }
2603
2701
 
2604
2702
  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
- }
2703
+ fn speedTie(battle: anytype, options: anytype) !bool {
2704
+ const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
2705
+ player == .P1
2706
+ else if (showdown)
2707
+ battle.rng.range(u8, 0, 2) == 0
2708
+ else
2709
+ battle.rng.next() < Gen12.percent(50) + 1;
2609
2710
 
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;
2711
+ try options.chance.speedTie(p1);
2712
+ return p1;
2613
2713
  }
2614
2714
 
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
- }
2715
+ fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2716
+ const crit = if (options.calc.overridden(player, .critical_hit)) |val|
2717
+ val == .true
2718
+ else if (showdown)
2719
+ battle.rng.chance(u8, rate, 256)
2720
+ else
2721
+ std.math.rotl(u8, battle.rng.next(), 3) < rate;
2622
2722
 
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;
2723
+ try options.chance.criticalHit(player, crit, rate);
2724
+ return crit;
2626
2725
  }
2627
2726
 
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
- }
2727
+ fn damage(battle: anytype, player: Player, options: anytype) !u8 {
2728
+ const roll = if (options.calc.overridden(player, .damage)) |val|
2729
+ val
2730
+ else roll: {
2731
+ if (showdown) break :roll battle.rng.range(u8, 217, 256);
2732
+ while (true) {
2733
+ const r = std.math.rotr(u8, battle.rng.next(), 1);
2734
+ if (r >= 217) break :roll r;
2735
+ }
2736
+ };
2632
2737
 
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;
2738
+ assert(roll >= 217 and roll <= 255);
2739
+ try options.chance.damage(player, roll);
2740
+ return roll;
2636
2741
  }
2637
2742
 
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);
2743
+ fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) bool {
2744
+ const ok = if (options.calc.overridden(player, .hit)) |val|
2745
+ val == .true
2746
+ else if (showdown)
2747
+ battle.rng.chance(u8, accuracy, 256)
2748
+ else
2749
+ battle.rng.next() < accuracy;
2750
+
2751
+ options.chance.hit(ok, accuracy);
2752
+ return ok;
2641
2753
  }
2642
2754
 
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);
2755
+ fn confused(battle: anytype, player: Player, options: anytype) !bool {
2756
+ const cfz = if (options.calc.overridden(player, .confused)) |val|
2757
+ val == .true
2758
+ else if (showdown)
2759
+ !battle.rng.chance(u8, 128, 256)
2760
+ else
2761
+ battle.rng.next() >= Gen12.percent(50) + 1;
2762
+
2763
+ try options.chance.confused(player, cfz);
2764
+ return cfz;
2646
2765
  }
2647
2766
 
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));
2767
+ fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2768
+ const par = if (options.calc.overridden(player, .paralyzed)) |val|
2769
+ val == .true
2770
+ else if (showdown)
2771
+ battle.rng.chance(u8, 63, 256)
2772
+ else
2773
+ battle.rng.next() < Gen12.percent(25);
2774
+
2775
+ try options.chance.paralyzed(player, par);
2776
+ return par;
2651
2777
  }
2652
2778
 
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));
2779
+ fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2780
+ const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2781
+ val == .true
2782
+ else if (showdown)
2783
+ battle.rng.chance(u8, 25, 256)
2784
+ else
2785
+ battle.rng.next() < Gen12.percent(10);
2786
+
2787
+ try options.chance.secondaryChance(player, proc, 25);
2788
+ return proc;
2656
2789
  }
2657
2790
 
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
- }
2791
+ fn secondaryChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2792
+ const rate: u8 = if (low) 26 else 77;
2793
+
2794
+ const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2795
+ val == .true
2796
+ else if (showdown)
2797
+ battle.rng.chance(u8, rate, 256)
2798
+ else
2799
+ battle.rng.next() < 1 + (if (low) Gen12.percent(10) else Gen12.percent(30));
2800
+
2801
+ try options.chance.secondaryChance(player, proc, rate);
2802
+ return proc;
2664
2803
  }
2665
2804
 
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);
2805
+ fn poisonChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2806
+ const rate: u8 = if (low) 52 else 103;
2807
+
2808
+ const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2809
+ val == .true
2810
+ else if (showdown)
2811
+ battle.rng.chance(u8, rate, 256)
2812
+ else
2813
+ battle.rng.next() < 1 + (if (low) Gen12.percent(20) else Gen12.percent(40));
2814
+
2815
+ try options.chance.secondaryChance(player, proc, rate);
2816
+ return proc;
2669
2817
  }
2670
2818
 
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);
2819
+ fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2820
+ const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2821
+ val == .true
2822
+ else if (showdown)
2823
+ battle.rng.chance(u8, 85, 256)
2824
+ else
2825
+ battle.rng.next() < Gen12.percent(33) + 1;
2826
+
2827
+ try options.chance.secondaryChance(player, proc, 85);
2828
+ return proc;
2829
+ }
2830
+
2831
+ fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2832
+ const move: Move = if (options.calc.overridden(player, .metronome)) |val|
2833
+ val
2834
+ else if (showdown)
2835
+ Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
2836
+ else move: {
2837
+ while (true) {
2838
+ const r = battle.rng.next();
2839
+ if (r == 0 or r == @intFromEnum(Move.Metronome)) continue;
2840
+ if (r >= @intFromEnum(Move.Struggle)) continue;
2841
+ break :move @enumFromInt(r);
2691
2842
  }
2692
- }
2843
+ };
2693
2844
 
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
- }
2845
+ try options.chance.metronome(player, move);
2846
+ return move;
2698
2847
  }
2699
2848
 
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);
2849
+ fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2850
+ const power = if (options.calc.overridden(player, .psywave)) |val|
2851
+ val - 1
2852
+ else if (showdown)
2853
+ battle.rng.range(u8, 0, max)
2854
+ else power: {
2855
+ while (true) {
2856
+ const r = battle.rng.next();
2857
+ if (r < max) break :power r;
2858
+ }
2859
+ };
2860
+
2861
+ assert(power < max);
2862
+ try options.chance.psywave(player, power, max);
2863
+ return power;
2703
2864
  }
2704
2865
 
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
- }
2866
+ fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2867
+ const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2868
+ @intCast(val)
2869
+ else if (showdown)
2870
+ battle.rng.range(u3, 1, 8)
2871
+ else duration: {
2872
+ while (true) {
2873
+ const r = battle.rng.next() & 7;
2874
+ if (r != 0) break :duration @intCast(r);
2875
+ }
2876
+ };
2877
+
2878
+ assert(duration >= 1 and duration <= 7);
2879
+ options.chance.duration(.sleep, player, player.foe(), duration);
2880
+ return duration;
2717
2881
  }
2718
2882
 
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
- }
2883
+ fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2884
+ const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
2885
+ val
2886
+ else if (showdown)
2887
+ battle.rng.range(u4, 1, 9)
2888
+ else
2889
+ @intCast((battle.rng.next() & 7) + 1);
2890
+
2891
+ assert(duration >= 1 and duration <= 8);
2892
+ options.chance.duration(.disable, player, player.foe(), duration);
2893
+ return duration;
2725
2894
  }
2726
2895
 
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);
2896
+ fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
2897
+ const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2898
+ @intCast(val)
2899
+ else if (showdown)
2900
+ battle.rng.range(u3, 2, 6)
2901
+ else
2902
+ @intCast((battle.rng.next() & 3) + 2);
2903
+
2904
+ assert(duration >= 2 and duration <= 5);
2905
+ options.chance.duration(.confusion, player, if (self) player else player.foe(), duration);
2906
+ return duration;
2731
2907
  }
2732
2908
 
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;
2909
+ fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2910
+ const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2911
+ @intCast(val)
2912
+ else if (showdown)
2913
+ battle.rng.range(u3, 2, 4)
2914
+ else
2915
+ @intCast((battle.rng.next() & 1) + 2);
2916
+
2917
+ assert(duration >= 2 and duration <= 3);
2918
+ options.chance.duration(.attacking, player, player, duration);
2919
+ return duration;
2920
+ }
2921
+
2922
+ const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
2923
+
2924
+ fn distribution(
2925
+ battle: anytype,
2926
+ comptime effect: Move.Effect,
2927
+ player: Player,
2928
+ options: anytype,
2929
+ ) !u3 {
2930
+ const roll = if (effect == .Binding) .duration else .multi_hit;
2931
+ const n: u3 = if (options.calc.overridden(player, roll)) |val|
2932
+ @intCast(val)
2933
+ else if (showdown)
2934
+ DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
2935
+ else n: {
2936
+ const r = (battle.rng.next() & 3);
2937
+ break :n @intCast((if (r < 2) r else battle.rng.next() & 3) + 2);
2938
+ };
2939
+
2940
+ assert(n >= 2 and n <= 5);
2941
+ if (effect == .Binding) {
2942
+ options.chance.duration(.binding, player, player, n);
2943
+ } else {
2944
+ try options.chance.multiHit(player, n);
2945
+ }
2946
+ return n;
2947
+ }
2948
+
2949
+ fn moveSlot(
2950
+ battle: anytype,
2951
+ player: Player,
2952
+ moves: []MoveSlot,
2953
+ check_pp: u4,
2954
+ options: anytype,
2955
+ ) !u4 {
2956
+ // TODO: consider throwing error instead of rerolling?
2957
+ const overridden = if (options.calc.overridden(player, .move_slot)) |val|
2958
+ if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
2959
+ val
2960
+ else
2961
+ null
2962
+ else
2963
+ null;
2964
+
2965
+ const slot: u4 = overridden orelse slot: {
2966
+ if (showdown) {
2967
+ if (check_pp == 0) {
2968
+ var i: usize = moves.len;
2969
+ while (i > 0) {
2970
+ i -= 1;
2971
+ if (moves[i].id != .None) {
2972
+ break :slot battle.rng.range(u4, 0, @as(u4, @intCast(i + 1))) + 1;
2973
+ }
2974
+ }
2975
+ } else {
2976
+ var r = battle.rng.range(u4, 0, check_pp) + 1;
2977
+ var i: usize = 0;
2978
+ while (i < moves.len and r > 0) : (i += 1) {
2979
+ if (moves[i].pp > 0) {
2980
+ r -= 1;
2981
+ if (r == 0) break :slot @intCast(i + 1);
2982
+ }
2983
+ }
2984
+ }
2985
+ }
2986
+
2987
+ while (true) {
2988
+ const r: u4 = @intCast(battle.rng.next() & 3);
2989
+ if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) break :slot r + 1;
2990
+ }
2991
+ };
2992
+
2993
+ try options.chance.moveSlot(player, slot, moves, check_pp);
2994
+ return slot;
2736
2995
  }
2737
2996
  };
2738
2997
 
@@ -2740,14 +2999,14 @@ test "RNG agreement" {
2740
2999
  if (!showdown) return;
2741
3000
  var expected: [256]u32 = undefined;
2742
3001
  for (0..expected.len) |i| {
2743
- expected[i] = @intCast(u32, i * 0x1000000);
3002
+ expected[i] = @intCast(i * 0x1000000);
2744
3003
  }
2745
3004
 
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 };
3005
+ var spe: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3006
+ var cfz: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3007
+ var par: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3008
+ var brn: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3009
+ var eff: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
2751
3010
 
2752
3011
  for (0..expected.len) |i| {
2753
3012
  try expectEqual(spe.range(u8, 0, 2) == 0, i < Gen12.percent(50) + 1);
@@ -2783,7 +3042,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
2783
3042
  const side = battle.side(player);
2784
3043
  const foe = battle.foe(player);
2785
3044
 
2786
- var active = side.active;
3045
+ var active = &side.active;
2787
3046
  const stored = side.stored();
2788
3047
 
2789
3048
  // While players are not given any input options on the cartridge in these cases,
@@ -2797,7 +3056,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
2797
3056
  // strictly correct as the player would not have been presented the option to move or
2798
3057
  // switch at all.
2799
3058
  if (isForced(active)) {
2800
- out[n] = .{ .type = .Move, .data = @boolToInt(showdown) };
3059
+ out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
2801
3060
  n += 1;
2802
3061
  return n;
2803
3062
  }
@@ -2837,7 +3096,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
2837
3096
  if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
2838
3097
  // Pokémon Showdown displays Struggle if limited to Bide but unable to pick
2839
3098
  const struggle =
2840
- m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);
3099
+ m.id == .Bide and (m.pp == 0 or active.volatiles.disable_move == slot);
2841
3100
  const s = if (struggle) 0 else slot;
2842
3101
  out[n] = .{ .type = .Move, .data = s };
2843
3102
  n += 1;
@@ -2852,7 +3111,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
2852
3111
  const m = active.moves[slot - 1];
2853
3112
  if (m.id == .None) break;
2854
3113
  if (m.pp == 0) continue;
2855
- if (active.volatiles.disabled_move == slot) continue;
3114
+ if (active.volatiles.disable_move == slot) continue;
2856
3115
  out[n] = .{ .type = .Move, .data = slot };
2857
3116
  n += 1;
2858
3117
  }