@pkmn/engine 0.1.0-dev.b9fb6eab → 0.1.0-dev.c42deb42

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