@pkmn/engine 0.1.0-dev.84cc1a02 → 0.1.0-dev.86ae1278

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