@pkmn/engine 0.1.0-dev.d97c6e03 → 0.1.0-dev.dd5b9081

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