@pkmn/engine 0.1.0-dev.cf6f5d2c → 0.1.0-dev.d56bdadd

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