@pkmn/engine 0.1.0-dev.84cc1a02 → 0.1.0-dev.886adbe3

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