@pkmn/engine 0.1.0-dev.84cc1a02 → 0.1.0-dev.8b192003

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