@pkmn/engine 0.1.0-dev.4eeb346e → 0.1.0-dev.526bb2e1

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