@pkmn/engine 0.1.0-dev.53335a32 → 0.1.0-dev.53787794

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 +299 -154
  38. package/package.json +101 -48
  39. package/src/bin/install-pkmn-engine +83 -41
  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 +955 -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 +1309 -872
  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 -782
  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);
591
+ try log.activate(.{ ident, .Confusion });
539
592
 
540
- const confused = if (showdown)
541
- !battle.rng.chance(u8, 128, 256)
542
- else
543
- battle.rng.next() >= Gen12.percent(50) + 1;
544
-
545
- if (confused) {
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,41 @@ 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);
1044
+ try log.hitcount(.{ battle.active(player.foe()), hit });
925
1045
  } else if (showdown) {
926
1046
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
927
1047
  if (move.effect == .Binding) {
928
- Effects.binding(battle, player);
1048
+ try Effects.binding(battle, player, rewrap, options);
929
1049
  if (immune) {
930
1050
  battle.last_damage = 0;
1051
+ assert(foe.stored().hp > 0);
931
1052
  // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
932
- try log.damage(battle.active(player.foe()), foe.stored(), .None);
1053
+ if (sub) {
1054
+ try log.activate(.{ battle.active(player.foe()), .Substitute });
1055
+ } else {
1056
+ try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
1057
+ try buildRage(battle, player.foe(), options);
1058
+ }
933
1059
  return null;
934
1060
  }
935
1061
  }
936
1062
  }
937
1063
 
938
1064
  // 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.
1065
+ // if an effect was intended to "always happen" it will still get skipped
940
1066
  if (nullified) return null;
941
1067
 
942
1068
  // On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
943
1069
  // 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);
1070
+ // setup before the loop means we can avoid having to waste time doing no-op handler searches
1071
+ if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
946
1072
 
947
1073
  if (foe.stored().hp == 0) return null;
948
1074
 
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);
1075
+ // Pokémon Showdown builds Rage at the wrong time for non-MultiHit move
1076
+ if (late and !sub and move.effect != .Disable) try buildRage(battle, player.foe(), options);
951
1077
 
952
1078
  if (!move.effect.isSpecial()) {
953
1079
  // On the cartridge Rage is not considered to be "special" and thus gets executed for a
@@ -956,32 +1082,33 @@ fn doMove(
956
1082
  // below). For Twineedle we change the data to that of one with PoisonChance1 given its
957
1083
  // MultiHit behavior is complete after the loop above, though Pokémon Showdown handles the
958
1084
  // Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
959
- // puts the |-status| message before |-hitcount| instead of after.
1085
+ // puts the |-status| message before |-hitcount| instead of after
960
1086
  if (move.effect == .Twineedle) {
961
- if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), log);
1087
+ if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
962
1088
  } else if (move.effect == .Disable) {
963
- const result = try Effects.disable(battle, player, move, log);
964
- if (showdown) try buildRage(battle, player.foe(), log);
1089
+ const result = try Effects.disable(battle, player, move, options);
1090
+ if (showdown) try buildRage(battle, player.foe(), options);
965
1091
  return result;
966
1092
  } else {
967
- try moveEffect(battle, player, move, log);
1093
+ try moveEffect(battle, player, move, options);
968
1094
  }
969
1095
  }
970
1096
 
971
1097
  return null;
972
1098
  }
973
1099
 
974
- fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
1100
+ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !bool {
975
1101
  const side = battle.side(player);
976
1102
 
977
1103
  // 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;
1104
+ var rate: u16 = Species.chance(side.stored().species);
1105
+ // GLITCH: Focus Energy reduces critical hit rate instead of increasing it
1106
+ rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
1107
+ rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
1108
+
1109
+ const spurious =
1110
+ !showdown and ((move.bp == 0 and move.effect != .OHKO) or move.target == .Depends);
1111
+ return Rolls.criticalHit(battle, player, @intCast(rate), spurious, options);
985
1112
  }
986
1113
 
987
1114
  fn calcDamage(
@@ -990,6 +1117,7 @@ fn calcDamage(
990
1117
  target_player: Player,
991
1118
  m: ?Move.Data,
992
1119
  crit: bool,
1120
+ options: anytype,
993
1121
  ) bool {
994
1122
  // Confusion (indicated when m == null) just needs a 40 BP physical move
995
1123
  const cfz = m == null;
@@ -1001,28 +1129,8 @@ fn calcDamage(
1001
1129
 
1002
1130
  const special = move.type.special();
1003
1131
 
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
1132
+ var atk = attack(side, crit, special);
1133
+ var def = defense(target, crit, special, cfz);
1026
1134
 
1027
1135
  // Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
1028
1136
  // will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
@@ -1033,9 +1141,9 @@ fn calcDamage(
1033
1141
  def = @max((def / 4) & 255, if (showdown) 1 else 0);
1034
1142
  }
1035
1143
 
1036
- const lvl = @as(u32, side.stored().level * @as(u2, if (crit) 2 else 1));
1144
+ const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
1037
1145
 
1038
- def = @as(u32, if (move.effect == .Explode) @max(def / 2, 1) else def);
1146
+ def = if (move.effect == .Explode) @max(def / 2, 1) else def;
1039
1147
 
1040
1148
  if (def == 0) return false;
1041
1149
 
@@ -1047,11 +1155,41 @@ fn calcDamage(
1047
1155
  d = @min(997, d);
1048
1156
  d += 2;
1049
1157
 
1050
- battle.last_damage = @intCast(u16, d);
1158
+ // Whether or not a critical hit occurred can change whether a division-by-zero freeze is
1159
+ // encountered so we need to make sure that in the event of a miss we commit the status of the
1160
+ // crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
1161
+ // that results in counter intuitive probabilities in the common case
1162
+ if (pkmn.options.chance and !showdown) {
1163
+ atk = attack(side, !crit, special);
1164
+ def = defense(target, !crit, special, false);
1165
+ if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
1166
+ }
1167
+
1168
+ battle.last_damage = @intCast(d);
1051
1169
 
1052
1170
  return true;
1053
1171
  }
1054
1172
 
1173
+ fn attack(side: *Side, crit: bool, special: bool) u32 {
1174
+ return if (crit)
1175
+ if (special) side.stored().stats.spc else side.stored().stats.atk
1176
+ else if (special) side.active.stats.spc else side.active.stats.atk;
1177
+ }
1178
+
1179
+ fn defense(target: *Side, crit: bool, special: bool, cfz: bool) u32 {
1180
+ return if (crit)
1181
+ if (special) target.stored().stats.spc else target.stored().stats.def
1182
+ else
1183
+ // GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
1184
+ if (special)
1185
+ target.active.stats.spc *
1186
+ @as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
1187
+ else
1188
+ // Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
1189
+ target.active.stats.def *
1190
+ @as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
1191
+ }
1192
+
1055
1193
  fn adjustDamage(battle: anytype, player: Player) u16 {
1056
1194
  const side = battle.side(player);
1057
1195
  const foe = battle.foe(player);
@@ -1061,9 +1199,9 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
1061
1199
  var d = battle.last_damage;
1062
1200
  if (side.active.types.includes(move.type)) d +%= d / 2;
1063
1201
 
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));
1202
+ const neutral = @intFromEnum(Effectiveness.Neutral);
1203
+ const eff1: u16 = @intFromEnum(move.type.effectiveness(types.type1));
1204
+ const eff2: u16 = @intFromEnum(move.type.effectiveness(types.type2));
1067
1205
 
1068
1206
  // Type effectiveness matchup precedence only matters with (NVE, SE)
1069
1207
  if (!showdown and (eff1 + eff2) == Effectiveness.mismatch and
@@ -1083,101 +1221,85 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
1083
1221
  return if (types.type1 == types.type2) eff1 * neutral else eff1 * eff2;
1084
1222
  }
1085
1223
 
1086
- fn randomizeDamage(battle: anytype) void {
1224
+ fn randomizeDamage(battle: anytype, player: Player, options: anytype) !void {
1087
1225
  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);
1226
+ const random = try Rolls.damage(battle, player, options);
1227
+ options.calc.base(player.foe(), battle.last_damage);
1228
+ battle.last_damage = @intCast(@as(u32, battle.last_damage) *% random / 255);
1229
+ options.calc.final(player.foe(), battle.last_damage);
1099
1230
  }
1100
1231
 
1101
- fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
1232
+ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
1102
1233
  const side = battle.side(player);
1234
+ const foe = battle.foe(player);
1103
1235
 
1104
- if (!try checkHit(battle, player, move, log)) return null;
1236
+ if (!try checkHit(battle, player, move, options)) return null;
1105
1237
 
1106
1238
  battle.last_damage = switch (side.last_selected_move) {
1107
- .SuperFang => @max(battle.foe(player).stored().hp / 2, 1),
1239
+ .SuperFang => @max(foe.stored().hp / 2, 1),
1108
1240
  .SeismicToss, .NightShade => side.stored().level,
1109
1241
  .SonicBoom => 20,
1110
1242
  .DragonRage => 40,
1111
1243
  // GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
1112
1244
  .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
- }
1245
+ const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
1246
+ // GLITCH: Psywave infinite glitch loop
1247
+ if (!showdown and max <= 1) return Result.Error;
1248
+ break :power try Rolls.psywave(battle, player, max, options);
1124
1249
  },
1125
1250
  else => unreachable,
1126
1251
  };
1127
1252
 
1128
1253
  if (battle.last_damage == 0) return if (showdown) null else Result.Error;
1129
1254
 
1130
- _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
1131
- if (battle.foe(player).stored().hp > 0) try buildRage(battle, player.foe(), log);
1255
+ const sub = showdown and foe.active.volatiles.Substitute;
1256
+ _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
1257
+ if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), options);
1132
1258
 
1133
1259
  return null;
1134
1260
  }
1135
1261
 
1136
- fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
1262
+ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
1137
1263
  const foe = battle.foe(player);
1138
1264
 
1139
1265
  if (battle.last_damage == 0) {
1140
- try log.fail(battle.active(player), .None);
1266
+ try options.log.fail(.{ battle.active(player), .None });
1141
1267
  return null;
1142
1268
  }
1143
1269
 
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);
1270
+ // Pretend Splash was used as a stand-in when no move has been used to fail below with 0 BP
1147
1271
  const foe_last_selected_move =
1148
1272
  Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
1149
- .Counter
1273
+ .Splash
1150
1274
  else
1151
1275
  foe.last_selected_move);
1152
1276
 
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
-
1277
+ const used = battle.lastMove(player.foe()).counterable != 0;
1158
1278
  const selected = foe_last_selected_move.bp > 0 and
1159
1279
  foe.last_selected_move != .Counter and
1160
1280
  (foe_last_selected_move.type == .Normal or
1161
1281
  foe_last_selected_move.type == .Fighting);
1162
1282
 
1163
1283
  if (!used and !selected) {
1164
- try log.fail(battle.active(player), .None);
1284
+ try options.log.fail(.{ battle.active(player), .None });
1165
1285
  return null;
1166
1286
  }
1167
1287
 
1168
1288
  if (!used or !selected) {
1169
1289
  // GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
1170
1290
  if (!showdown) return Result.Error;
1171
- try log.fail(battle.active(player), .None);
1291
+ try options.log.fail(.{ battle.active(player), .None });
1172
1292
  return null;
1173
1293
  }
1174
1294
 
1175
- battle.last_damage = if (battle.last_damage > 0x7FFF) 0xFFFF else battle.last_damage * 2;
1295
+ battle.last_damage *|= 2;
1176
1296
 
1177
- // Pokémon Showdown calls checkHit before Counter
1178
- if (!showdown and !try checkHit(battle, player, move, log)) return null;
1297
+ // Pokémon Showdown calls moveHit before Counter
1298
+ if (!showdown and !try checkHit(battle, player, move, options)) return null;
1179
1299
 
1180
- _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
1300
+ const sub = showdown and foe.active.volatiles.Substitute;
1301
+ _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
1302
+ if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), options);
1181
1303
  return null;
1182
1304
  }
1183
1305
 
@@ -1186,7 +1308,7 @@ fn applyDamage(
1186
1308
  target_player: Player,
1187
1309
  sub_player: Player,
1188
1310
  reason: Damage,
1189
- log: anytype,
1311
+ options: anytype,
1190
1312
  ) !bool {
1191
1313
  assert(showdown or battle.last_damage != 0);
1192
1314
 
@@ -1201,19 +1323,20 @@ fn applyDamage(
1201
1323
  subbed.active.volatiles.substitute = 0;
1202
1324
  subbed.active.volatiles.Substitute = false;
1203
1325
  // battle.last_damage is not updated with the amount of HP the Substitute had
1204
- try log.end(battle.active(sub_player), .Substitute);
1326
+ try options.log.end(.{ battle.active(sub_player), .Substitute });
1327
+ if (reason != .Confusion) options.calc.capped(sub_player);
1205
1328
  return true;
1206
1329
  } else {
1207
1330
  // 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);
1331
+ subbed.active.volatiles.substitute -= @intCast(battle.last_damage);
1332
+ try options.log.activate(.{ battle.active(sub_player), .Substitute });
1210
1333
  return false;
1211
1334
  }
1212
1335
  }
1213
1336
 
1214
1337
  if (battle.last_damage > target.stored().hp) battle.last_damage = target.stored().hp;
1215
1338
  target.stored().hp -= battle.last_damage;
1216
- try log.damage(battle.active(target_player), target.stored(), reason);
1339
+ try options.log.damage(.{ battle.active(target_player), target.stored(), reason });
1217
1340
  return false;
1218
1341
  }
1219
1342
 
@@ -1221,73 +1344,81 @@ fn mirrorMove(
1221
1344
  battle: anytype,
1222
1345
  player: Player,
1223
1346
  mslot: u4,
1347
+ rewrap: bool,
1224
1348
  auto: bool,
1225
1349
  residual: *bool,
1226
- log: anytype,
1227
- ) !?Result {
1350
+ options: anytype,
1351
+ ) anyerror!?Result {
1228
1352
  var side = battle.side(player);
1229
1353
  const foe = battle.foe(player);
1230
1354
 
1231
1355
  side.last_selected_move = foe.last_used_move;
1232
1356
 
1233
1357
  if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
1234
- try log.fail(battle.active(player), .None);
1358
+ try options.log.fail(.{ battle.active(player), .None });
1235
1359
  return null;
1360
+ } else if (!showdown or foe.last_used_move != .Struggle) {
1361
+ incrementPP(side, player, mslot, options);
1236
1362
  }
1237
1363
 
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);
1364
+ if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
1365
+ return null;
1366
+ }
1367
+ return doMove(battle, player, mslot, rewrap, auto, residual, options);
1242
1368
  }
1243
1369
 
1244
1370
  fn metronome(
1245
1371
  battle: anytype,
1246
1372
  player: Player,
1247
1373
  mslot: u4,
1374
+ rewrap: bool,
1248
1375
  auto: bool,
1249
1376
  residual: *bool,
1250
- log: anytype,
1251
- ) !?Result {
1377
+ options: anytype,
1378
+ ) anyerror!?Result {
1252
1379
  var side = battle.side(player);
1253
1380
 
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
- };
1381
+ side.last_selected_move = try Rolls.metronome(battle, player, options);
1382
+ incrementPP(side, player, mslot, options);
1266
1383
 
1267
- incrementPP(side, mslot);
1268
-
1269
- if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
1270
- return doMove(battle, player, mslot, auto, residual, log);
1384
+ if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
1385
+ return null;
1386
+ }
1387
+ return doMove(battle, player, mslot, rewrap, auto, residual, options);
1271
1388
  }
1272
1389
 
1273
- fn checkHit(battle: anytype, player: Player, move: Move.Data, log: anytype) !bool {
1390
+ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !bool {
1274
1391
  var immune = false;
1275
1392
  var mist = false;
1276
- if (moveHit(battle, player, move, &immune, &mist)) return true;
1393
+
1394
+ if (try moveHit(battle, player, move, &immune, &mist, options)) {
1395
+ if (!showdown) try options.chance.commit(player, .hit);
1396
+ return true;
1397
+ }
1398
+
1277
1399
  assert(!immune);
1278
1400
  if (mist) {
1279
1401
  assert(!showdown);
1280
1402
  const foe_ident = battle.active(player.foe());
1281
- try log.activate(foe_ident, .Mist);
1282
- try log.fail(foe_ident, .None);
1403
+ try options.log.activate(.{ foe_ident, .Mist });
1404
+ try options.log.fail(.{ foe_ident, .None });
1283
1405
  } else {
1284
- try log.lastmiss();
1285
- try log.miss(battle.active(player));
1406
+ try options.log.lastmiss(.{});
1407
+ try options.log.miss(.{battle.active(player)});
1286
1408
  }
1409
+
1410
+ if (!showdown) try options.chance.commit(player, .miss);
1287
1411
  return false;
1288
1412
  }
1289
1413
 
1290
- fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist: *bool) bool {
1414
+ fn moveHit(
1415
+ battle: anytype,
1416
+ player: Player,
1417
+ move: Move.Data,
1418
+ immune: *bool,
1419
+ mist: *bool,
1420
+ options: anytype,
1421
+ ) !bool {
1291
1422
  var side = battle.side(player);
1292
1423
  const foe = battle.foe(player);
1293
1424
 
@@ -1321,23 +1452,24 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
1321
1452
  }
1322
1453
 
1323
1454
  // 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)];
1455
+ const state = side.active.volatiles.state;
1456
+ var overwrite = move.effect == .Rage or move.effect == .Thrashing;
1457
+ const overwritten = overwrite and state > 0;
1458
+ assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
1459
+
1460
+ var accuracy: u16 = if (overwritten) state else move.accuracy;
1461
+ var boost = BOOSTS[@as(u4, @intCast(@as(i8, side.active.boosts.accuracy) + 6))];
1329
1462
  accuracy = accuracy * boost[0] / boost[1];
1330
- boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
1463
+ boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
1331
1464
  accuracy = accuracy * boost[0] / boost[1];
1332
1465
  accuracy = @min(255, @max(1, accuracy));
1333
1466
 
1334
- if (move.effect == .Thrashing or move.effect == .Rage) state.* = accuracy;
1467
+ // Pokémon Showdown only overwrites if the volatile is present
1468
+ if (showdown) overwrite = side.active.volatiles.Rage or side.active.volatiles.Thrashing;
1469
+ if (overwrite) side.active.volatiles.state = accuracy;
1335
1470
 
1336
1471
  // 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;
1472
+ break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
1341
1473
  };
1342
1474
 
1343
1475
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1347,16 +1479,20 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
1347
1479
  }
1348
1480
 
1349
1481
  if (!miss) return true;
1350
- battle.last_damage = 0;
1482
+ if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
1483
+
1351
1484
  side.active.volatiles.Binding = false;
1485
+ options.chance.observe(.binding, player, .None);
1486
+
1352
1487
  return false;
1353
1488
  }
1354
1489
 
1355
1490
  fn checkFaint(
1356
1491
  battle: anytype,
1357
1492
  player: Player,
1358
- log: anytype,
1359
- ) @TypeOf(log).Error!?Result {
1493
+ cap: bool,
1494
+ options: anytype,
1495
+ ) @TypeOf(options.log).Error!?Result {
1360
1496
  const side = battle.side(player);
1361
1497
  if (side.stored().hp > 0) return null;
1362
1498
 
@@ -1364,33 +1500,32 @@ fn checkFaint(
1364
1500
  const foe_fainted = foe.stored().hp == 0;
1365
1501
 
1366
1502
  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;
1503
+ const foe_out = foe_fainted and findFirstAlive(foe) == 0;
1504
+ const more = player_out or foe_out;
1370
1505
 
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;
1506
+ if (try faint(battle, player, !(more or foe_fainted), cap, options)) |r| return r;
1507
+ if (foe_fainted) if (try faint(battle, player.foe(), !more, false, options)) |r| return r;
1373
1508
 
1374
1509
  assert(!side.active.volatiles.MultiHit);
1375
1510
  assert(!foe.active.volatiles.MultiHit);
1376
1511
 
1377
- if (tie) {
1378
- try log.tie();
1512
+ if (player_out and foe_out) {
1513
+ try options.log.tie(.{});
1379
1514
  return Result.Tie;
1380
1515
  } else if (player_out) {
1381
- try log.win(player.foe());
1516
+ try options.log.win(.{player.foe()});
1382
1517
  return if (player == .P1) Result.Lose else Result.Win;
1383
1518
  } else if (foe_out) {
1384
- try log.win(player);
1519
+ try options.log.win(.{player});
1385
1520
  return if (player == .P1) Result.Win else Result.Lose;
1386
1521
  }
1387
1522
 
1388
1523
  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 };
1524
+ if (player == .P1) return .{ .p1 = .Switch, .p2 = foe_choice };
1525
+ return .{ .p1 = foe_choice, .p2 = .Switch };
1391
1526
  }
1392
1527
 
1393
- fn faint(battle: anytype, player: Player, log: anytype, done: bool) !?Result {
1528
+ fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytype) !?Result {
1394
1529
  var side = battle.side(player);
1395
1530
  var foe = battle.foe(player);
1396
1531
  assert(side.stored().hp == 0);
@@ -1423,32 +1558,33 @@ fn faint(battle: anytype, player: Player, log: anytype, done: bool) !?Result {
1423
1558
  side.stored().stats.spe;
1424
1559
  }
1425
1560
 
1426
- try log.faint(battle.active(player), done);
1561
+ try options.log.faint(.{ battle.active(player), done });
1562
+ // We can only safely set capped if the faint occured as the direct result of an opponent's
1563
+ // move (as opposed to due to residual damage, recoil, Explosion, etc)
1564
+ if (cap) options.calc.capped(player);
1427
1565
  return null;
1428
1566
  }
1429
1567
 
1430
- fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1568
+ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1431
1569
  var side = battle.side(player);
1432
1570
  var stored = side.stored();
1433
1571
  const ident = battle.active(player);
1434
1572
  var volatiles = &side.active.volatiles;
1435
1573
 
1436
1574
  const brn = Status.is(stored.status, .BRN);
1437
- if (brn or Status.is(stored.status, .PSN)) blk: {
1575
+ if (brn or Status.is(stored.status, .PSN)) {
1438
1576
  var damage = @max(stored.stats.hp / 16, 1);
1439
-
1440
1577
  if (volatiles.Toxic) {
1441
1578
  volatiles.toxic += 1;
1442
1579
  damage *= volatiles.toxic;
1443
1580
  }
1581
+ damage = @min(damage, stored.hp);
1444
1582
 
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);
1583
+ if (!showdown or damage > 0) {
1584
+ stored.hp -= damage;
1585
+ // Pokémon Showdown uses damage [of] here but its not relevant in Generation I
1586
+ try options.log.damage(.{ ident, stored, if (brn) Damage.Burn else Damage.Poison });
1587
+ }
1452
1588
  }
1453
1589
 
1454
1590
  if (volatiles.LeechSeed) {
@@ -1469,49 +1605,46 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1469
1605
  damage *= volatiles.toxic;
1470
1606
  }
1471
1607
 
1472
- // Pokémon Showdown erroneously updates last damage with uncapped Leech Seed damage
1473
- if (showdown) battle.last_damage = damage;
1474
-
1475
1608
  const amount = @min(damage, stored.hp);
1476
1609
  stored.hp -= amount;
1477
1610
 
1478
- // As above, Pokémon Showdown uses damageOf but its not relevant
1479
- if (amount > 0) try log.damage(ident, stored, .LeechSeed);
1611
+ // As above, Pokémon Showdown uses damage [of] but its not relevant
1612
+ if (amount > 0) try options.log.damage(.{ ident, stored, .LeechSeed });
1480
1613
 
1481
1614
  const before = foe_stored.hp;
1482
1615
  // Uncapped damage is added back to the foe
1483
1616
  foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
1484
1617
  // 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);
1618
+ if (foe_stored.hp > before) try options.log.heal(.{ foe_ident, foe_stored, .Silent });
1486
1619
  }
1487
1620
  }
1488
1621
 
1489
- fn endTurn(battle: anytype, log: anytype) @TypeOf(log).Error!Result {
1622
+ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result {
1490
1623
  assert(!battle.side(.P1).active.volatiles.MultiHit);
1491
1624
  assert(!battle.side(.P2).active.volatiles.MultiHit);
1492
1625
 
1493
- if (showdown and options.ebc and checkEBC(battle)) {
1494
- try log.tie();
1626
+ battle.turn += 1;
1627
+
1628
+ if (showdown and pkmn.options.ebc and checkEBC(battle)) {
1629
+ try options.log.tie(.{});
1495
1630
  return Result.Tie;
1496
1631
  }
1497
1632
 
1498
- battle.turn += 1;
1499
-
1500
1633
  if (showdown and battle.turn >= 1000) {
1501
- try log.tie();
1634
+ try options.log.tie(.{});
1502
1635
  return Result.Tie;
1503
1636
  } else if (battle.turn >= 65535) {
1504
1637
  return Result.Error;
1505
1638
  }
1506
1639
 
1507
- try log.turn(battle.turn);
1640
+ try options.log.turn(.{battle.turn});
1508
1641
 
1509
1642
  return Result.Default;
1510
1643
  }
1511
1644
 
1512
1645
  fn checkEBC(battle: anytype) bool {
1513
1646
  for (battle.sides, 0..) |side, i| {
1514
- const foe = battle.sides[~@intCast(u1, i)];
1647
+ const foe = battle.sides[~@as(u1, @intCast(i))];
1515
1648
 
1516
1649
  var foe_all_ghosts = true;
1517
1650
  var foe_all_transform = true;
@@ -1563,105 +1696,126 @@ fn checkEBC(battle: anytype) bool {
1563
1696
  return false;
1564
1697
  }
1565
1698
 
1566
- inline fn onBegin(
1699
+ fn disabled(side: *Side, ident: ID, options: anytype) !bool {
1700
+ if (side.active.volatiles.disable_move != 0) {
1701
+ // A Pokémon that transforms after being disabled may end up with less move slots
1702
+ const m = side.active.moves[side.active.volatiles.disable_move - 1].id;
1703
+ // side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
1704
+ const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
1705
+ m
1706
+ else
1707
+ side.last_selected_move;
1708
+ if (m != .None and m == last) {
1709
+ side.active.volatiles.Charging = false;
1710
+ try options.log.cant(.{ ident, .Disable, last });
1711
+ return true;
1712
+ }
1713
+ }
1714
+ return false;
1715
+ }
1716
+
1717
+ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1718
+ const side = battle.side(who);
1719
+ if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
1720
+ try Effects.boost(battle, who, Move.get(.Rage), options);
1721
+ }
1722
+ }
1723
+
1724
+ fn onBegin(
1567
1725
  battle: anytype,
1568
1726
  player: Player,
1569
1727
  move: Move.Data,
1570
1728
  mslot: u8,
1571
1729
  residual: *bool,
1572
- log: anytype,
1730
+ options: anytype,
1573
1731
  ) !void {
1574
- assert(move.effect.onBegin());
1575
1732
  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),
1733
+ .Confusion => Effects.confusion(battle, player, move, options),
1734
+ .Conversion => Effects.conversion(battle, player, options),
1735
+ .FocusEnergy => Effects.focusEnergy(battle, player, options),
1736
+ .Haze => Effects.haze(battle, player, options),
1737
+ .Heal => Effects.heal(battle, player, options),
1738
+ .LeechSeed => Effects.leechSeed(battle, player, move, options),
1739
+ .LightScreen => Effects.lightScreen(battle, player, options),
1740
+ .Mimic => Effects.mimic(battle, player, move, mslot, options),
1741
+ .Mist => Effects.mist(battle, player, options),
1742
+ .Paralyze => Effects.paralyze(battle, player, move, options),
1743
+ .Poison => Effects.poison(battle, player, move, options),
1744
+ .Reflect => Effects.reflect(battle, player, options),
1745
+ .Splash => Effects.splash(battle, player, options),
1746
+ .Substitute => Effects.substitute(battle, player, residual, options),
1747
+ .SwitchAndTeleport => Effects.switchAndTeleport(battle, player, move, options),
1748
+ .Transform => Effects.transform(battle, player, options),
1592
1749
  else => unreachable,
1593
1750
  };
1594
1751
  }
1595
1752
 
1596
- inline fn onEnd(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
1597
- assert(move.effect.onEnd());
1753
+ fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1598
1754
  return switch (move.effect) {
1599
- .Bide => Effects.bide(battle, player, log),
1755
+ .Bide => Effects.bide(battle, player, options),
1600
1756
  // zig fmt: off
1601
1757
  .AttackUp1, .AttackUp2, .DefenseUp1, .DefenseUp2,
1602
1758
  .EvasionUp1, .SpecialUp1, .SpecialUp2, .SpeedUp2 =>
1603
- Effects.boost(battle, player, move, log),
1759
+ Effects.boost(battle, player, move, options),
1604
1760
  .AccuracyDown1, .AttackDown1, .DefenseDown1, .DefenseDown2, .SpeedDown1 =>
1605
- Effects.unboost(battle, player, move, log),
1761
+ Effects.unboost(battle, player, move, options),
1606
1762
  // zig fmt: on
1607
- .Sleep => Effects.sleep(battle, player, move, log),
1763
+ .Sleep => Effects.sleep(battle, player, move, options),
1608
1764
  else => unreachable,
1609
1765
  };
1610
1766
  }
1611
1767
 
1612
- inline fn alwaysHappens(
1768
+ fn alwaysHappens(
1613
1769
  battle: anytype,
1614
1770
  player: Player,
1615
1771
  move: Move.Data,
1616
1772
  residual: *bool,
1617
- log: anytype,
1773
+ options: anytype,
1618
1774
  ) !void {
1619
- assert(move.effect.alwaysHappens());
1620
1775
  return switch (move.effect) {
1621
- .DrainHP, .DreamEater => Effects.drainHP(battle, player, log),
1776
+ .DrainHP, .DreamEater => Effects.drainHP(battle, player, options),
1622
1777
  .Explode => Effects.explode(battle, player),
1623
- .PayDay => Effects.payDay(battle, player, log),
1778
+ .PayDay => Effects.payDay(battle, player, options),
1624
1779
  .Rage => Effects.rage(battle, player),
1625
- .Recoil => Effects.recoil(battle, player, residual, log),
1780
+ .Recoil => Effects.recoil(battle, player, residual, options),
1626
1781
  .JumpKick, .Binding => {},
1627
1782
  else => unreachable,
1628
1783
  };
1629
1784
  }
1630
1785
 
1631
- inline fn moveEffect(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
1786
+ fn moveEffect(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1632
1787
  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),
1788
+ .BurnChance1, .BurnChance2 => Effects.burnChance(battle, player, move, options),
1789
+ .ConfusionChance => Effects.confusion(battle, player, move, options),
1790
+ .FlinchChance1, .FlinchChance2 => Effects.flinchChance(battle, player, move, options),
1791
+ .FreezeChance => Effects.freezeChance(battle, player, move, options),
1792
+ .HyperBeam => Effects.hyperBeam(battle, player, options),
1638
1793
  .MultiHit, .DoubleHit, .Twineedle => unreachable,
1639
- .ParalyzeChance1, .ParalyzeChance2 => Effects.paralyzeChance(battle, player, move, log),
1640
- .PoisonChance1, .PoisonChance2 => Effects.poison(battle, player, move, log),
1794
+ .ParalyzeChance1, .ParalyzeChance2 => Effects.paralyzeChance(battle, player, move, options),
1795
+ .PoisonChance1, .PoisonChance2 => Effects.poison(battle, player, move, options),
1641
1796
  // zig fmt: off
1642
1797
  .AttackDownChance, .DefenseDownChance, .SpecialDownChance, .SpeedDownChance =>
1643
- Effects.unboost(battle, player, move, log),
1798
+ Effects.unboost(battle, player, move, options),
1644
1799
  // zig fmt: on
1645
1800
  else => {},
1646
1801
  };
1647
1802
  }
1648
1803
 
1649
1804
  pub const Effects = struct {
1650
- fn bide(battle: anytype, player: Player, log: anytype) !void {
1805
+ fn bide(battle: anytype, player: Player, options: anytype) !void {
1651
1806
  var side = battle.side(player);
1652
1807
 
1653
1808
  side.active.volatiles.Bide = true;
1654
1809
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1810
+
1655
1811
  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);
1812
+ side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
1813
+ options.chance.observe(.attacking, player, .started);
1660
1814
 
1661
- try log.start(battle.active(player), .Bide);
1815
+ try options.log.start(.{ battle.active(player), .Bide });
1662
1816
  }
1663
1817
 
1664
- fn burnChance(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
1818
+ fn burnChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1665
1819
  var foe = battle.foe(player);
1666
1820
  var foe_stored = foe.stored();
1667
1821
 
@@ -1672,96 +1826,90 @@ pub const Effects = struct {
1672
1826
  // GLITCH: Freeze top move selection desync can occur if thawed Pokémon is slower
1673
1827
  if (Status.is(foe_stored.status, .FRZ)) {
1674
1828
  assert(move.type == .Fire);
1675
- try log.curestatus(battle.active(player.foe()), foe_stored.status, .Message);
1829
+ try options.log.curestatus(.{
1830
+ battle.active(player.foe()),
1831
+ foe_stored.status,
1832
+ .Message,
1833
+ });
1676
1834
  foe_stored.status = 0;
1677
1835
  }
1678
1836
  return;
1679
1837
  }
1680
1838
 
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;
1839
+ if (foe.active.types.includes(move.type)) return;
1840
+ if (!try Rolls.secondaryChance(battle, player, move.effect == .BurnChance1, options)) {
1841
+ return;
1842
+ }
1689
1843
 
1690
1844
  foe_stored.status = Status.init(.BRN);
1691
1845
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
1692
1846
 
1693
- try log.status(battle.active(player.foe()), foe_stored.status, .None);
1847
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
1694
1848
  }
1695
1849
 
1696
- fn charge(battle: anytype, player: Player, log: anytype) !void {
1850
+ fn charge(battle: anytype, player: Player, options: anytype) !void {
1697
1851
  var side = battle.side(player);
1698
1852
  var volatiles = &side.active.volatiles;
1699
1853
 
1700
1854
  volatiles.Charging = true;
1701
1855
  const move = side.last_selected_move;
1702
1856
  if (move == .Fly or move == .Dig) volatiles.Invulnerable = true;
1703
- try log.laststill();
1704
- try log.prepare(battle.active(player), move);
1857
+ try options.log.laststill(.{});
1858
+ try options.log.prepare(.{ battle.active(player), move });
1705
1859
  }
1706
1860
 
1707
- fn confusion(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
1861
+ fn confusion(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1708
1862
  var foe = battle.foe(player);
1709
1863
  const sub = foe.active.volatiles.Substitute;
1710
1864
 
1711
1865
  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;
1866
+ if (!try Rolls.confusionChance(battle, player, options)) return;
1717
1867
  } else {
1718
1868
  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
- }
1869
+ if (!try checkHit(battle, player, move, options)) return;
1870
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1724
1871
  } 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
- }
1872
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1873
+ if (!try checkHit(battle, player, move, options)) return;
1730
1874
  }
1731
1875
  }
1732
1876
 
1733
1877
  if (foe.active.volatiles.Confusion) return;
1734
1878
  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
1879
 
1740
- try log.start(battle.active(player.foe()), .Confusion);
1880
+ foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
1881
+ options.chance.observe(.confusion, player.foe(), .started);
1882
+
1883
+ try options.log.start(.{ battle.active(player.foe()), .Confusion });
1741
1884
  }
1742
1885
 
1743
- fn conversion(battle: anytype, player: Player, log: anytype) !void {
1886
+ fn conversion(battle: anytype, player: Player, options: anytype) !void {
1744
1887
  const foe = battle.foe(player);
1745
1888
 
1746
1889
  if (foe.active.volatiles.Invulnerable) {
1747
- try log.lastmiss();
1748
- return log.miss(battle.active(player));
1890
+ try options.log.lastmiss(.{});
1891
+ return options.log.miss(.{battle.active(player)});
1749
1892
  }
1750
1893
 
1751
1894
  battle.side(player).active.types = foe.active.types;
1752
- return log.typechange(battle.active(player), foe.active.types, battle.active(player.foe()));
1895
+ return options.log.start(.{
1896
+ battle.active(player),
1897
+ .TypeChange,
1898
+ foe.active.types,
1899
+ battle.active(player.foe()),
1900
+ });
1753
1901
  }
1754
1902
 
1755
- fn disable(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
1903
+ fn disable(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
1756
1904
  var foe = battle.foe(player);
1757
1905
  var volatiles = &foe.active.volatiles;
1758
1906
  const foe_ident = battle.active(player.foe());
1759
1907
 
1760
1908
  // Pokémon Showdown handles hit/miss earlier in doMove
1761
- if (!showdown and !try checkHit(battle, player, move, log)) return null;
1909
+ if (!showdown and !try checkHit(battle, player, move, options)) return null;
1762
1910
 
1763
- if (volatiles.disabled_move != 0) {
1764
- try log.fail(foe_ident, .None);
1911
+ if (volatiles.disable_move != 0) {
1912
+ try options.log.fail(.{ foe_ident, .None });
1765
1913
  return null;
1766
1914
  }
1767
1915
 
@@ -1777,7 +1925,7 @@ pub const Effects = struct {
1777
1925
  // Technically this is still considered simply a "miss" on the cartridge,
1778
1926
  // but diverging from Pokémon Showdown here would mostly just be pedantic
1779
1927
  if (n == 0) {
1780
- try log.fail(foe_ident, .None);
1928
+ try options.log.fail(.{ foe_ident, .None });
1781
1929
  return null;
1782
1930
  } else if (err) {
1783
1931
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
@@ -1785,23 +1933,23 @@ pub const Effects = struct {
1785
1933
  return Result.Error;
1786
1934
  }
1787
1935
 
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);
1936
+ volatiles.disable_move =
1937
+ @intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
1938
+ volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
1939
+ options.chance.observe(.disable, player.foe(), .started);
1793
1940
 
1794
- try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
1941
+ const id = foe.active.move(volatiles.disable_move).id;
1942
+ try options.log.start(.{ foe_ident, .Disable, id });
1795
1943
  return null;
1796
1944
  }
1797
1945
 
1798
- fn drainHP(battle: anytype, player: Player, log: anytype) !void {
1799
- var side = battle.side(player);
1800
- var stored = side.stored();
1946
+ fn drainHP(battle: anytype, player: Player, options: anytype) !void {
1947
+ var stored = battle.side(player).stored();
1948
+ const ident = battle.active(player);
1801
1949
 
1802
1950
  if (battle.last_damage == 0) {
1803
1951
  assert(showdown);
1804
- return;
1952
+ if (!battle.foe(player).active.volatiles.Substitute) return;
1805
1953
  }
1806
1954
 
1807
1955
  const drain = @max(battle.last_damage / 2, 1);
@@ -1810,7 +1958,7 @@ pub const Effects = struct {
1810
1958
  if (stored.hp == stored.stats.hp) return;
1811
1959
  stored.hp = @min(stored.stats.hp, stored.hp + drain);
1812
1960
 
1813
- try log.drain(battle.active(player), stored, battle.active(player.foe()));
1961
+ try options.log.heal(.{ ident, stored, .Drain, battle.active(player.foe()) });
1814
1962
  }
1815
1963
 
1816
1964
  fn explode(battle: anytype, player: Player) !void {
@@ -1824,34 +1972,28 @@ pub const Effects = struct {
1824
1972
  side.active.volatiles.LeechSeed = false;
1825
1973
  }
1826
1974
 
1827
- fn flinchChance(battle: anytype, player: Player, move: Move.Data) void {
1975
+ fn flinchChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1828
1976
  var volatiles = &battle.foe(player).active.volatiles;
1829
1977
 
1830
1978
  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;
1979
+ if (!try Rolls.secondaryChance(battle, player, move.effect == .FlinchChance1, options)) {
1980
+ return;
1981
+ }
1840
1982
 
1841
1983
  volatiles.Flinch = true;
1842
1984
  volatiles.Recharging = false;
1843
1985
  }
1844
1986
 
1845
- fn focusEnergy(battle: anytype, player: Player, log: anytype) !void {
1987
+ fn focusEnergy(battle: anytype, player: Player, options: anytype) !void {
1846
1988
  var side = battle.side(player);
1847
1989
 
1848
1990
  if (side.active.volatiles.FocusEnergy) return;
1849
1991
  side.active.volatiles.FocusEnergy = true;
1850
1992
 
1851
- try log.start(battle.active(player), .FocusEnergy);
1993
+ try options.log.start(.{ battle.active(player), .FocusEnergy });
1852
1994
  }
1853
1995
 
1854
- fn freezeChance(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
1996
+ fn freezeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1855
1997
  var foe = battle.foe(player);
1856
1998
  var foe_stored = foe.stored();
1857
1999
  const foe_ident = battle.active(player.foe());
@@ -1862,26 +2004,23 @@ pub const Effects = struct {
1862
2004
  return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
1863
2005
  }
1864
2006
 
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
-
2007
+ if (foe.active.types.includes(move.type)) return;
2008
+ if (!try Rolls.secondaryChance(battle, player, true, options)) return;
1871
2009
  // Freeze Clause Mod
1872
2010
  if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1873
2011
 
1874
2012
  foe_stored.status = Status.init(.FRZ);
1875
2013
  // GLITCH: Hyper Beam recharging status is not cleared
1876
2014
 
1877
- try log.status(foe_ident, foe_stored.status, .None);
2015
+ try options.log.status(.{ foe_ident, foe_stored.status, .None });
1878
2016
  }
1879
2017
 
1880
- fn haze(battle: anytype, player: Player, log: anytype) !void {
2018
+ fn haze(battle: anytype, player: Player, options: anytype) !void {
2019
+ var log = options.log;
1881
2020
  var side = battle.side(player);
1882
2021
  var foe = battle.foe(player);
1883
2022
 
1884
- var side_stored = side.stored();
2023
+ const side_stored = side.stored();
1885
2024
  var foe_stored = foe.stored();
1886
2025
 
1887
2026
  const player_ident = battle.active(player);
@@ -1893,96 +2032,101 @@ pub const Effects = struct {
1893
2032
  side.active.stats = side_stored.stats;
1894
2033
  foe.active.stats = foe_stored.stats;
1895
2034
 
1896
- try log.activate(player_ident, .Haze);
1897
- try log.clearallboost();
2035
+ try log.activate(.{ player_ident, .Haze });
2036
+ try log.clearallboost(.{});
1898
2037
 
1899
2038
  // Pokémon Showdown clears P1 then P2 instead of status -> side -> foe
1900
2039
  if (showdown) {
1901
2040
  for (&battle.sides, 0..) |*s, i| {
1902
- const p = @intToEnum(Player, i);
2041
+ const p: Player = @enumFromInt(i);
1903
2042
  // Pokémon Showdown incorrectly does not prevent sleep/freeze from moving
1904
2043
  if (p != player and Status.any(s.stored().status)) {
1905
- try log.curestatus(foe_ident, foe_stored.status, .Silent);
2044
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
1906
2045
  s.stored().status = 0;
2046
+ options.chance.observe(.sleep, p, .None);
1907
2047
  } else if (showdown and s.stored().status == Status.TOX) {
1908
2048
  s.stored().status = Status.init(.PSN);
1909
- try log.status(battle.active(p), s.stored().status, .None);
2049
+ try log.status(.{ battle.active(p), s.stored().status, .None });
1910
2050
  }
1911
- try clearVolatiles(battle, p, log);
2051
+ try clearVolatiles(battle, p, options);
1912
2052
  }
1913
2053
  } else {
1914
2054
  if (Status.any(foe_stored.status)) {
1915
2055
  if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
1916
2056
  foe.last_selected_move = .SKIP_TURN;
2057
+ options.chance.observe(.sleep, player.foe(), .None);
1917
2058
  }
1918
- try log.curestatus(foe_ident, foe_stored.status, .Silent);
2059
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
1919
2060
  foe_stored.status = 0;
1920
2061
  }
1921
- try clearVolatiles(battle, player, log);
1922
- try clearVolatiles(battle, player.foe(), log);
2062
+ try clearVolatiles(battle, player, options);
2063
+ try clearVolatiles(battle, player.foe(), options);
1923
2064
  }
1924
2065
  }
1925
2066
 
1926
- fn heal(battle: anytype, player: Player, log: anytype) !void {
2067
+ fn heal(battle: anytype, player: Player, options: anytype) !void {
1927
2068
  var side = battle.side(player);
1928
2069
  var stored = side.stored();
1929
2070
  const ident = battle.active(player);
1930
2071
 
1931
2072
  // GLITCH: HP recovery move failure glitches
1932
2073
  const delta = stored.stats.hp - stored.hp;
1933
- if (delta == 0 or delta & 255 == 255) return try log.fail(ident, .None);
2074
+ if (delta == 0 or (delta & 255 == 255 and stored.hp % 256 != 0)) {
2075
+ return options.log.fail(.{ ident, .None });
2076
+ }
1934
2077
 
1935
2078
  const rest = side.last_selected_move == .Rest;
1936
2079
  if (rest) {
1937
2080
  // Adding the sleep status runs the sleep condition handler to roll duration
1938
2081
  if (showdown) battle.rng.advance(1);
1939
2082
  stored.status = Status.slf(2);
1940
- try log.statusFrom(ident, stored.status, Move.Rest);
2083
+ try options.log.status(.{ ident, stored.status, .From, Move.Rest });
1941
2084
  stored.hp = stored.stats.hp;
1942
2085
  } else {
1943
2086
  stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
1944
2087
  }
1945
- try log.heal(ident, stored, if (rest) Heal.Silent else Heal.None);
2088
+ try options.log.heal(.{ ident, stored, if (rest) Heal.Silent else Heal.None });
1946
2089
  }
1947
2090
 
1948
- fn hyperBeam(battle: anytype, player: Player, log: anytype) !void {
2091
+ fn hyperBeam(battle: anytype, player: Player, options: anytype) !void {
1949
2092
  battle.side(player).active.volatiles.Recharging = true;
1950
- try log.mustrecharge(battle.active(player));
2093
+ try options.log.mustrecharge(.{battle.active(player)});
1951
2094
  }
1952
2095
 
1953
- fn leechSeed(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
2096
+ fn leechSeed(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1954
2097
  var foe = battle.foe(player);
1955
2098
 
1956
2099
  if (showdown) {
1957
2100
  // Invulnerability trumps type immunity on Pokémon Showdown
1958
2101
  if (!foe.active.volatiles.Invulnerable and foe.active.types.includes(.Grass)) {
1959
- return log.immune(battle.active(player.foe()), .None);
2102
+ return options.log.immune(.{ battle.active(player.foe()), .None });
1960
2103
  }
1961
- if (!try checkHit(battle, player, move, log)) return;
2104
+ if (!try checkHit(battle, player, move, options)) return;
1962
2105
  if (foe.active.volatiles.LeechSeed) return;
1963
2106
  } else {
1964
- if (!try checkHit(battle, player, move, log)) return;
2107
+ if (!try checkHit(battle, player, move, options)) return;
1965
2108
  if (foe.active.types.includes(.Grass) or foe.active.volatiles.LeechSeed) {
1966
- try log.lastmiss();
1967
- return log.miss(battle.active(player));
2109
+ try options.log.lastmiss(.{});
2110
+ return options.log.miss(.{battle.active(player)});
1968
2111
  }
1969
2112
  }
1970
2113
 
1971
2114
  foe.active.volatiles.LeechSeed = true;
1972
2115
 
1973
- try log.start(battle.active(player.foe()), .LeechSeed);
2116
+ try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
1974
2117
  }
1975
2118
 
1976
- fn lightScreen(battle: anytype, player: Player, log: anytype) !void {
2119
+ fn lightScreen(battle: anytype, player: Player, options: anytype) !void {
1977
2120
  var side = battle.side(player);
2121
+ const ident = battle.active(player);
1978
2122
 
1979
- if (side.active.volatiles.LightScreen) return log.fail(battle.active(player), .None);
2123
+ if (side.active.volatiles.LightScreen) return options.log.fail(.{ ident, .None });
1980
2124
  side.active.volatiles.LightScreen = true;
1981
2125
 
1982
- try log.start(battle.active(player), .LightScreen);
2126
+ try options.log.start(.{ ident, .LightScreen });
1983
2127
  }
1984
2128
 
1985
- fn mimic(battle: anytype, player: Player, move: Move.Data, mslot: u8, log: anytype) !void {
2129
+ fn mimic(battle: anytype, player: Player, move: Move.Data, mslot: u8, options: anytype) !void {
1986
2130
  var side = battle.side(player);
1987
2131
  var foe = battle.foe(player);
1988
2132
 
@@ -1998,45 +2142,49 @@ pub const Effects = struct {
1998
2142
  const has_mimic = has_mimic: {
1999
2143
  for (side.active.moves, 0..) |m, i| {
2000
2144
  if (m.id == .Mimic) {
2001
- oslot = @intCast(u8, i + 1);
2145
+ oslot = @intCast(i + 1);
2002
2146
  break :has_mimic true;
2003
2147
  }
2004
2148
  }
2005
2149
  break :has_mimic false;
2006
2150
  };
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);
2151
+ if (!has_mimic) {
2152
+ // Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
2153
+ if (!try checkHit(battle, player, move, options)) return;
2154
+ return options.log.fail(.{ battle.active(player.foe()), .None });
2011
2155
  }
2012
2156
  }
2013
- if (!try checkHit(battle, player, move, log)) return;
2157
+ if (!try checkHit(battle, player, move, options)) return;
2014
2158
 
2015
- const rslot = randomMoveSlot(&battle.rng, &foe.active.moves, 0);
2159
+ const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
2016
2160
  side.active.move(oslot).id = foe.active.move(rslot).id;
2017
2161
 
2018
- try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
2162
+ try options.log.start(.{ battle.active(player), .Mimic, side.active.move(oslot).id });
2019
2163
  }
2020
2164
 
2021
- fn mist(battle: anytype, player: Player, log: anytype) !void {
2165
+ fn mist(battle: anytype, player: Player, options: anytype) !void {
2022
2166
  var side = battle.side(player);
2023
2167
 
2024
2168
  if (side.active.volatiles.Mist) return;
2025
2169
  side.active.volatiles.Mist = true;
2026
2170
 
2027
- try log.start(battle.active(player), .Mist);
2171
+ try options.log.start(.{ battle.active(player), .Mist });
2028
2172
  }
2029
2173
 
2030
- fn multiHit(battle: anytype, player: Player, move: Move.Data) void {
2174
+ fn multiHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
2031
2175
  var side = battle.side(player);
2032
2176
 
2033
2177
  assert(!side.active.volatiles.MultiHit);
2034
2178
  side.active.volatiles.MultiHit = true;
2035
2179
 
2036
- side.active.volatiles.attacks = if (move.effect == .MultiHit) distribution(battle) else 2;
2180
+ side.active.volatiles.attacks = if (move.effect == .MultiHit)
2181
+ try Rolls.distribution(battle, .MultiHit, player, options)
2182
+ else
2183
+ 2;
2037
2184
  }
2038
2185
 
2039
- fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
2186
+ fn paralyze(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
2187
+ var log = options.log;
2040
2188
  var foe = battle.foe(player);
2041
2189
  var foe_stored = foe.stored();
2042
2190
  const foe_ident = battle.active(player.foe());
@@ -2046,27 +2194,28 @@ pub const Effects = struct {
2046
2194
 
2047
2195
  if (showdown) {
2048
2196
  // 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;
2197
+ if (immune and !foe.active.volatiles.Invulnerable) {
2198
+ return log.immune(.{ foe_ident, .None });
2199
+ } else if (!try checkHit(battle, player, move, options)) return;
2051
2200
  }
2052
2201
  if (Status.any(foe_stored.status)) {
2053
- return log.fail(
2202
+ return log.fail(.{
2054
2203
  foe_ident,
2055
- if (Status.is(foe_stored.status, .PAR)) .Paralysis else .None,
2056
- );
2204
+ if (Status.is(foe_stored.status, .PAR)) Fail.Paralysis else Fail.None,
2205
+ });
2057
2206
  }
2058
2207
  if (!showdown) {
2059
- if (immune) return log.immune(foe_ident, .None);
2060
- if (!try checkHit(battle, player, move, log)) return;
2208
+ if (immune) return log.immune(.{ foe_ident, .None });
2209
+ if (!try checkHit(battle, player, move, options)) return;
2061
2210
  }
2062
2211
 
2063
2212
  foe_stored.status = Status.init(.PAR);
2064
2213
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2065
2214
 
2066
- try log.status(foe_ident, foe_stored.status, .None);
2215
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2067
2216
  }
2068
2217
 
2069
- fn paralyzeChance(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
2218
+ fn paralyzeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
2070
2219
  var foe = battle.foe(player);
2071
2220
  var foe_stored = foe.stored();
2072
2221
 
@@ -2077,65 +2226,59 @@ pub const Effects = struct {
2077
2226
  }
2078
2227
 
2079
2228
  // 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;
2229
+ if (foe.active.types.includes(move.type)) return;
2230
+ if (!try Rolls.secondaryChance(battle, player, move.effect == .ParalyzeChance1, options)) {
2231
+ return;
2232
+ }
2088
2233
 
2089
2234
  foe_stored.status = Status.init(.PAR);
2090
2235
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2091
2236
 
2092
- try log.status(battle.active(player.foe()), foe_stored.status, .None);
2237
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
2093
2238
  }
2094
2239
 
2095
- fn payDay(battle: anytype, player: Player, log: anytype) !void {
2096
- if (!showdown or !battle.foe(player).active.volatiles.Substitute) try log.fieldactivate();
2240
+ fn payDay(battle: anytype, player: Player, options: anytype) !void {
2241
+ if (!showdown or !battle.foe(player).active.volatiles.Substitute) {
2242
+ try options.log.fieldactivate(.{});
2243
+ }
2097
2244
  }
2098
2245
 
2099
- fn poison(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
2246
+ fn poison(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
2247
+ var log = options.log;
2100
2248
  var foe = battle.foe(player);
2101
2249
  var foe_stored = foe.stored();
2102
2250
  const foe_ident = battle.active(player.foe());
2103
2251
  const toxic = battle.side(player).last_selected_move == .Toxic;
2104
2252
 
2105
- if (showdown and move.effect == .Poison and !try checkHit(battle, player, move, log)) {
2253
+ if (showdown and move.effect == .Poison and !try checkHit(battle, player, move, options)) {
2106
2254
  return;
2107
2255
  } else if (foe.active.volatiles.Substitute) {
2108
2256
  if (move.effect != .Poison) return;
2109
- return log.fail(foe_ident, .None);
2257
+ return log.fail(.{ foe_ident, .None });
2110
2258
  } else if (Status.any(foe_stored.status)) {
2111
2259
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2112
2260
  // Pokémon Showdown considers Toxic to be a status even in Generation I and so
2113
2261
  // 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))
2262
+ return log.fail(.{ foe_ident, if (Status.is(foe_stored.status, .PSN))
2115
2263
  if (!showdown)
2116
- .Poison
2264
+ Fail.Poison
2117
2265
  else if (toxic == (foe_stored.status == Status.TOX))
2118
- if (toxic) .Toxic else .Poison
2266
+ if (toxic) Fail.Toxic else Fail.Poison
2119
2267
  else
2120
- .None
2268
+ Fail.None
2121
2269
  else
2122
- .None);
2270
+ Fail.None });
2123
2271
  } else if (foe.active.types.includes(.Poison)) {
2124
2272
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2125
- return log.immune(foe_ident, .None);
2273
+ return log.immune(.{ foe_ident, .None });
2126
2274
  }
2127
2275
 
2128
2276
  if (move.effect == .Poison) {
2129
- if (!showdown and !try checkHit(battle, player, move, log)) return;
2277
+ if (!showdown and !try checkHit(battle, player, move, options)) return;
2130
2278
  } 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;
2279
+ if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
2280
+ return;
2281
+ }
2139
2282
  }
2140
2283
 
2141
2284
  foe_stored.status = Status.init(.PSN);
@@ -2145,39 +2288,46 @@ pub const Effects = struct {
2145
2288
  foe.active.volatiles.toxic = 0;
2146
2289
  }
2147
2290
 
2148
- try log.status(foe_ident, foe_stored.status, .None);
2291
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2149
2292
  }
2150
2293
 
2151
2294
  fn rage(battle: anytype, player: Player) !void {
2152
2295
  var volatiles = &battle.side(player).active.volatiles;
2153
- if (volatiles.Rage) return;
2154
- volatiles.Rage = true;
2155
2296
  assert(!volatiles.Bide);
2156
- volatiles.state = 0;
2297
+ volatiles.Rage = true;
2157
2298
  }
2158
2299
 
2159
- fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
2300
+ fn recoil(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
2160
2301
  var side = battle.side(player);
2161
2302
  var stored = side.stored();
2162
2303
 
2163
- const damage = @intCast(i16, @max(battle.last_damage /
2304
+ assert(showdown or battle.last_damage > 0);
2305
+ if (showdown and battle.last_damage == 0) return;
2306
+
2307
+ const damage: i16 = @intCast(@max(battle.last_damage /
2164
2308
  @as(u8, if (side.last_selected_move == .Struggle) 2 else 4), 1));
2165
- stored.hp = @intCast(u16, @max(@intCast(i16, stored.hp) - damage, 0));
2309
+ stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
2166
2310
 
2167
- try log.damageOf(battle.active(player), stored, .RecoilOf, battle.active(player.foe()));
2311
+ try options.log.damage(.{
2312
+ battle.active(player),
2313
+ stored,
2314
+ .RecoilOf,
2315
+ battle.active(player.foe()),
2316
+ });
2168
2317
  if (showdown and stored.hp == 0) residual.* = false;
2169
2318
  }
2170
2319
 
2171
- fn reflect(battle: anytype, player: Player, log: anytype) !void {
2320
+ fn reflect(battle: anytype, player: Player, options: anytype) !void {
2172
2321
  var side = battle.side(player);
2322
+ const ident = battle.active(player);
2173
2323
 
2174
- if (side.active.volatiles.Reflect) return log.fail(battle.active(player), .None);
2324
+ if (side.active.volatiles.Reflect) return options.log.fail(.{ ident, .None });
2175
2325
  side.active.volatiles.Reflect = true;
2176
2326
 
2177
- try log.start(battle.active(player), .Reflect);
2327
+ try options.log.start(.{ ident, .Reflect });
2178
2328
  }
2179
2329
 
2180
- fn sleep(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
2330
+ fn sleep(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
2181
2331
  var foe = battle.foe(player);
2182
2332
  var foe_stored = foe.stored();
2183
2333
  const foe_ident = battle.active(player.foe());
@@ -2188,13 +2338,13 @@ pub const Effects = struct {
2188
2338
  // the Sleep Clause Mod might activate, causing us to not actually bypass
2189
2339
  } else {
2190
2340
  if (Status.any(foe_stored.status)) {
2191
- return log.fail(
2341
+ return options.log.fail(.{
2192
2342
  foe_ident,
2193
- if (Status.is(foe_stored.status, .SLP)) .Sleep else .None,
2194
- );
2343
+ if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
2344
+ });
2195
2345
  }
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;
2346
+ // If moveHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2347
+ if (!showdown and !try checkHit(battle, player, move, options)) return;
2198
2348
  }
2199
2349
 
2200
2350
  // Sleep Clause Mod
@@ -2205,76 +2355,70 @@ pub const Effects = struct {
2205
2355
  }
2206
2356
  foe.active.volatiles.Recharging = false;
2207
2357
 
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
- });
2358
+ foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2359
+ options.chance.observe(.sleep, player.foe(), .started);
2216
2360
 
2217
- foe_stored.status = Status.slp(duration);
2218
- try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
2361
+ const last = battle.side(player).last_selected_move;
2362
+ try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
2219
2363
  }
2220
2364
 
2221
- fn splash(battle: anytype, player: Player, log: anytype) !void {
2222
- try log.activate(battle.active(player), .Splash);
2365
+ fn splash(battle: anytype, player: Player, options: anytype) !void {
2366
+ try options.log.activate(.{ battle.active(player), .Splash });
2223
2367
  }
2224
2368
 
2225
- fn substitute(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
2369
+ fn substitute(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
2226
2370
  var side = battle.side(player);
2227
2371
  if (side.active.volatiles.Substitute) {
2228
- try log.fail(battle.active(player), .Substitute);
2229
- return;
2372
+ return options.log.fail(.{ battle.active(player), .Substitute });
2230
2373
  }
2231
2374
 
2232
2375
  assert(side.stored().stats.hp <= 1023);
2233
2376
  // 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);
2377
+ const hp: u8 = @intCast(side.stored().stats.hp / 4);
2235
2378
  // Pokénon Showdown incorrectly checks for 1/4 HP based on `target.maxhp / 4` which returns
2236
2379
  // a floating point value and thus only correctly implements the Substitute 1/4 glitch when
2237
2380
  // the target's HP is exactly divisible by 4 (here we're using an inlined divCeil routine to
2238
2381
  // avoid having to convert to floating point)
2239
2382
  const required_hp = if (showdown) @divFloor(side.stored().stats.hp - 1, 4) + 1 else hp;
2240
2383
  if (side.stored().hp < required_hp) {
2241
- try log.fail(battle.active(player), .Weak);
2242
- return;
2384
+ return options.log.fail(.{ battle.active(player), .Weak });
2243
2385
  }
2244
2386
 
2245
2387
  // GLITCH: can leave the user with 0 HP (faints later) because didn't check '<=' above
2246
2388
  side.stored().hp -= hp;
2247
2389
  side.active.volatiles.substitute = hp + 1;
2248
2390
  side.active.volatiles.Substitute = true;
2249
- try log.start(battle.active(player), .Substitute);
2391
+ try options.log.start(.{ battle.active(player), .Substitute });
2250
2392
  if (hp > 0) {
2251
- try log.damage(battle.active(player), side.stored(), .None);
2393
+ try options.log.damage(.{ battle.active(player), side.stored(), .None });
2252
2394
  if (showdown and side.stored().hp == 0) residual.* = false;
2253
2395
  }
2254
2396
  }
2255
2397
 
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
-
2398
+ fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
2259
2399
  // 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;
2400
+ if (showdown) {
2401
+ if (battle.side(player).last_selected_move == .Teleport) return;
2402
+ _ = try checkHit(battle, player, move, options);
2403
+ battle.last_damage = 0;
2404
+ } else {
2405
+ try options.log.fail(.{ battle.active(player), .None });
2406
+ try options.log.laststill(.{});
2407
+ }
2262
2408
  }
2263
2409
 
2264
- fn thrashing(battle: anytype, player: Player) void {
2410
+ fn thrashing(battle: anytype, player: Player, options: anytype) void {
2265
2411
  var volatiles = &battle.side(player).active.volatiles;
2266
2412
  assert(!volatiles.Thrashing);
2267
2413
  assert(!volatiles.Bide);
2268
2414
 
2269
2415
  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);
2416
+ volatiles.attacks = Rolls.attackingDuration(battle, player, options);
2417
+
2418
+ options.chance.observe(.attacking, player, .started);
2275
2419
  }
2276
2420
 
2277
- fn transform(battle: anytype, player: Player, log: anytype) !void {
2421
+ fn transform(battle: anytype, player: Player, options: anytype) !void {
2278
2422
  var side = battle.side(player);
2279
2423
  const foe = battle.foe(player);
2280
2424
  const foe_ident = battle.active(player.foe());
@@ -2300,10 +2444,10 @@ pub const Effects = struct {
2300
2444
  side.active.moves[i].pp = if (m.id != .None) 5 else 0;
2301
2445
  }
2302
2446
 
2303
- try log.transform(battle.active(player), foe_ident);
2447
+ try options.log.transform(.{ battle.active(player), foe_ident });
2304
2448
  }
2305
2449
 
2306
- fn binding(battle: anytype, player: Player) void {
2450
+ fn binding(battle: anytype, player: Player, rewrap: bool, options: anytype) !void {
2307
2451
  var side = battle.side(player);
2308
2452
  var foe = battle.foe(player);
2309
2453
 
@@ -2311,12 +2455,17 @@ pub const Effects = struct {
2311
2455
  side.active.volatiles.Binding = true;
2312
2456
  // GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
2313
2457
  // (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
2314
- if (!showdown) foe.active.volatiles.Recharging = false;
2458
+ if (!showdown) {
2459
+ foe.active.volatiles.Recharging = false;
2460
+ } else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
2315
2461
 
2316
- side.active.volatiles.attacks = distribution(battle) - 1;
2462
+ side.active.volatiles.attacks =
2463
+ try Rolls.distribution(battle, .Binding, player, options) - 1;
2464
+ options.chance.observe(.binding, player, .started);
2317
2465
  }
2318
2466
 
2319
- fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
2467
+ fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
2468
+ var log = options.log;
2320
2469
  var side = battle.side(player);
2321
2470
  const ident = battle.active(player);
2322
2471
 
@@ -2326,85 +2475,87 @@ pub const Effects = struct {
2326
2475
  switch (move.effect) {
2327
2476
  .AttackUp1, .AttackUp2, .Rage => {
2328
2477
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2329
- if (boosts.atk == 6) return try log.fail(ident, .None);
2478
+ if (boosts.atk == 6) return log.fail(.{ ident, .None });
2330
2479
  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;
2480
+ boosts.atk = @as(i4, @intCast(@min(6, @as(i8, boosts.atk) + n)));
2481
+ const reason: Boost = if (move.effect == .Rage) .Rage else .Attack;
2333
2482
  if (stats.atk == MAX_STAT_VALUE) {
2334
2483
  boosts.atk -= 1;
2335
2484
  if (showdown) {
2336
- try log.boost(ident, reason, n);
2337
- try log.boost(ident, Boost.Attack, -1);
2485
+ try log.boost(.{ ident, reason, n });
2486
+ try log.boost(.{ ident, .Attack, -1 });
2338
2487
  if (move.effect == .Rage) return;
2339
2488
  }
2340
- return try log.fail(ident, .None);
2489
+ return log.fail(.{ ident, .None });
2341
2490
  }
2342
- var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2343
- const stat = unmodifiedStats(battle, player).atk;
2491
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2492
+ const stat = unmodifiedStats(battle, side).atk;
2344
2493
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2345
- try log.boost(ident, reason, n);
2494
+ try log.boost(.{ ident, reason, n });
2495
+ // Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
2496
+ if (showdown and move.effect == .Rage) return;
2346
2497
  },
2347
2498
  .DefenseUp1, .DefenseUp2 => {
2348
2499
  assert(boosts.def >= -6 and boosts.def <= 6);
2349
- if (boosts.def == 6) return try log.fail(ident, .None);
2500
+ if (boosts.def == 6) return log.fail(.{ ident, .None });
2350
2501
  const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
2351
- boosts.def = @intCast(i4, @min(6, @as(i8, boosts.def) + n));
2502
+ boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
2352
2503
  if (stats.def == MAX_STAT_VALUE) {
2353
2504
  boosts.def -= 1;
2354
2505
  if (showdown) {
2355
- try log.boost(ident, .Defense, n);
2356
- try log.boost(ident, .Defense, -1);
2506
+ try log.boost(.{ ident, .Defense, n });
2507
+ try log.boost(.{ ident, .Defense, -1 });
2357
2508
  }
2358
- return try log.fail(ident, .None);
2509
+ return log.fail(.{ ident, .None });
2359
2510
  }
2360
- var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2361
- const stat = unmodifiedStats(battle, player).def;
2511
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2512
+ const stat = unmodifiedStats(battle, side).def;
2362
2513
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2363
- try log.boost(ident, .Defense, n);
2514
+ try log.boost(.{ ident, .Defense, n });
2364
2515
  },
2365
2516
  .SpeedUp2 => {
2366
2517
  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));
2518
+ if (boosts.spe == 6) return log.fail(.{ ident, .None });
2519
+ boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
2369
2520
  if (stats.spe == MAX_STAT_VALUE) {
2370
2521
  boosts.spe -= 1;
2371
2522
  if (showdown) {
2372
- try log.boost(ident, .Speed, 2);
2373
- try log.boost(ident, .Speed, -1);
2523
+ try log.boost(.{ ident, .Speed, 2 });
2524
+ try log.boost(.{ ident, .Speed, -1 });
2374
2525
  }
2375
- return try log.fail(ident, .None);
2526
+ return log.fail(.{ ident, .None });
2376
2527
  }
2377
- var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2378
- const stat = unmodifiedStats(battle, player).spe;
2528
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2529
+ const stat = unmodifiedStats(battle, side).spe;
2379
2530
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2380
- try log.boost(ident, .Speed, 2);
2531
+ try log.boost(.{ ident, .Speed, 2 });
2381
2532
  },
2382
2533
  .SpecialUp1, .SpecialUp2 => {
2383
2534
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2384
- if (boosts.spc == 6) return try log.fail(ident, .None);
2535
+ if (boosts.spc == 6) return log.fail(.{ ident, .None });
2385
2536
  const n: u2 = if (move.effect == .SpecialUp2) 2 else 1;
2386
- boosts.spc = @intCast(i4, @min(6, @as(i8, boosts.spc) + n));
2537
+ boosts.spc = @intCast(@min(6, @as(i8, boosts.spc) + n));
2387
2538
  if (stats.spc == MAX_STAT_VALUE) {
2388
2539
  boosts.spc -= 1;
2389
2540
  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);
2541
+ try log.boost(.{ ident, .SpecialAttack, n });
2542
+ try log.boost(.{ ident, .SpecialAttack, -1 });
2543
+ try log.boost(.{ ident, .SpecialDefense, n });
2544
+ try log.boost(.{ ident, .SpecialDefense, -1 });
2394
2545
  }
2395
- return try log.fail(ident, .None);
2546
+ return log.fail(.{ ident, .None });
2396
2547
  }
2397
- var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2398
- const stat = unmodifiedStats(battle, player).spc;
2548
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2549
+ const stat = unmodifiedStats(battle, side).spc;
2399
2550
  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);
2551
+ try log.boost(.{ ident, .SpecialAttack, n });
2552
+ try log.boost(.{ ident, .SpecialDefense, n });
2402
2553
  },
2403
2554
  .EvasionUp1 => {
2404
2555
  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);
2556
+ if (boosts.evasion == 6) return log.fail(.{ ident, .None });
2557
+ boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
2558
+ try log.boost(.{ ident, .Evasion, 1 });
2408
2559
  },
2409
2560
  else => unreachable,
2410
2561
  }
@@ -2413,106 +2564,105 @@ pub const Effects = struct {
2413
2564
  statusModify(battle.foe(player).stored().status, &battle.foe(player).active.stats);
2414
2565
  }
2415
2566
 
2416
- fn unboost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
2567
+ fn unboost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
2568
+ var log = options.log;
2417
2569
  var foe = battle.foe(player);
2418
2570
  const foe_ident = battle.active(player.foe());
2571
+ const secondary = move.effect.isStatDownChance();
2419
2572
 
2420
- if (foe.active.volatiles.Substitute) {
2421
- return if (!move.effect.isStatDownChance()) log.fail(foe_ident, .None);
2422
- }
2573
+ if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(.{ foe_ident, .None });
2423
2574
 
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
2575
+ if (secondary) {
2576
+ const proc = try Rolls.unboost(battle, player, options);
2577
+ if (!proc or foe.active.volatiles.Invulnerable) return;
2578
+ } else {
2579
+ // checkHit already checks for Invulnerable
2580
+ if (!showdown and !try checkHit(battle, player, move, options)) return;
2432
2581
  }
2433
2582
 
2434
2583
  var stats = &foe.active.stats;
2435
2584
  var boosts = &foe.active.boosts;
2585
+ const fail = showdown or !secondary;
2436
2586
 
2437
2587
  switch (move.effect) {
2438
2588
  .AttackDown1, .AttackDownChance => {
2439
2589
  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));
2590
+ if (boosts.atk == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2591
+ boosts.atk = @intCast(@max(-6, @as(i8, boosts.atk) - 1));
2442
2592
  if (stats.atk == 1) {
2443
2593
  boosts.atk += 1;
2444
2594
  if (showdown) {
2445
- try log.boost(foe_ident, .Attack, -1);
2446
- try log.boost(foe_ident, .Attack, 1);
2595
+ try log.boost(.{ foe_ident, .Attack, -1 });
2596
+ try log.boost(.{ foe_ident, .Attack, 1 });
2447
2597
  }
2448
- return try log.fail(foe_ident, .None);
2598
+ return log.fail(.{ foe_ident, .None });
2449
2599
  }
2450
- var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2451
- const stat = unmodifiedStats(battle, player.foe()).atk;
2600
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2601
+ const stat = unmodifiedStats(battle, foe).atk;
2452
2602
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2453
- try log.boost(foe_ident, .Attack, -1);
2603
+ try log.boost(.{ foe_ident, .Attack, -1 });
2454
2604
  },
2455
2605
  .DefenseDown1, .DefenseDown2, .DefenseDownChance => {
2456
2606
  assert(boosts.def >= -6 and boosts.def <= 6);
2457
- if (boosts.def == -6) return try log.fail(foe_ident, .None);
2607
+ if (boosts.def == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2458
2608
  const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
2459
- boosts.def = @intCast(i4, @max(-6, @as(i8, boosts.def) - n));
2609
+ boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
2460
2610
  if (stats.def == 1) {
2461
2611
  boosts.def += 1;
2462
2612
  if (showdown) {
2463
- try log.boost(foe_ident, .Defense, -@as(i8, n));
2464
- try log.boost(foe_ident, .Defense, 1);
2613
+ try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
2614
+ try log.boost(.{ foe_ident, .Defense, 1 });
2465
2615
  }
2466
- return try log.fail(foe_ident, .None);
2616
+ return log.fail(.{ foe_ident, .None });
2467
2617
  }
2468
- var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2469
- const stat = unmodifiedStats(battle, player.foe()).def;
2618
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2619
+ const stat = unmodifiedStats(battle, foe).def;
2470
2620
  stats.def = @max(1, stat * mod[0] / mod[1]);
2471
- try log.boost(foe_ident, .Defense, -@as(i8, n));
2621
+ try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
2472
2622
  },
2473
2623
  .SpeedDown1, .SpeedDownChance => {
2474
2624
  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));
2625
+ if (boosts.spe == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2626
+ boosts.spe = @intCast(@max(-6, @as(i8, boosts.spe) - 1));
2477
2627
  if (stats.spe == 1) {
2478
2628
  boosts.spe += 1;
2479
2629
  if (showdown) {
2480
- try log.boost(foe_ident, .Speed, -1);
2481
- try log.boost(foe_ident, .Speed, 1);
2630
+ try log.boost(.{ foe_ident, .Speed, -1 });
2631
+ try log.boost(.{ foe_ident, .Speed, 1 });
2482
2632
  }
2483
- return try log.fail(foe_ident, .None);
2633
+ return log.fail(.{ foe_ident, .None });
2484
2634
  }
2485
- var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2486
- const stat = unmodifiedStats(battle, player.foe()).spe;
2635
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2636
+ const stat = unmodifiedStats(battle, foe).spe;
2487
2637
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2488
- try log.boost(foe_ident, .Speed, -1);
2638
+ try log.boost(.{ foe_ident, .Speed, -1 });
2489
2639
  assert(boosts.spe >= -6);
2490
2640
  },
2491
2641
  .SpecialDownChance => {
2492
2642
  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));
2643
+ if (boosts.spc == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2644
+ boosts.spc = @intCast(@max(-6, @as(i8, boosts.spc) - 1));
2495
2645
  if (stats.spc == 1) {
2496
2646
  boosts.spc += 1;
2497
2647
  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);
2648
+ try log.boost(.{ foe_ident, .SpecialAttack, -1 });
2649
+ try log.boost(.{ foe_ident, .SpecialAttack, 1 });
2650
+ try log.boost(.{ foe_ident, .SpecialDefense, -1 });
2651
+ try log.boost(.{ foe_ident, .SpecialDefense, 1 });
2502
2652
  }
2503
- return try log.fail(foe_ident, .None);
2653
+ return log.fail(.{ foe_ident, .None });
2504
2654
  }
2505
- var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2506
- const stat = unmodifiedStats(battle, player.foe()).spc;
2655
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2656
+ const stat = unmodifiedStats(battle, foe).spc;
2507
2657
  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);
2658
+ try log.boost(.{ foe_ident, .SpecialAttack, -1 });
2659
+ try log.boost(.{ foe_ident, .SpecialDefense, -1 });
2510
2660
  },
2511
2661
  .AccuracyDown1 => {
2512
2662
  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);
2663
+ if (boosts.accuracy == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2664
+ boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
2665
+ try log.boost(.{ foe_ident, .Accuracy, -1 });
2516
2666
  },
2517
2667
  else => unreachable,
2518
2668
  }
@@ -2522,8 +2672,7 @@ pub const Effects = struct {
2522
2672
  }
2523
2673
  };
2524
2674
 
2525
- fn unmodifiedStats(battle: anytype, who: Player) *Stats(u16) {
2526
- const side = battle.side(who);
2675
+ fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
2527
2676
  if (!side.active.volatiles.Transform) return &side.stored().stats;
2528
2677
  const id = ID.from(side.active.volatiles.transform);
2529
2678
  return &battle.side(id.player).pokemon[id.id - 1].stats;
@@ -2537,37 +2686,40 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
2537
2686
  }
2538
2687
  }
2539
2688
 
2540
- inline fn isForced(active: ActivePokemon) bool {
2689
+ fn isForced(active: anytype) bool {
2541
2690
  return active.volatiles.Recharging or active.volatiles.Rage or
2542
2691
  active.volatiles.Thrashing or active.volatiles.Charging;
2543
2692
  }
2544
2693
 
2545
- fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
2694
+ fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
2695
+ var log = options.log;
2546
2696
  var side = battle.side(who);
2547
2697
  var volatiles = &side.active.volatiles;
2548
2698
  const ident = battle.active(who);
2549
2699
 
2550
- if (volatiles.disabled_move != 0) {
2551
- volatiles.disabled_move = 0;
2552
- volatiles.disabled_duration = 0;
2553
- try log.end(ident, .DisableSilent);
2700
+ if (volatiles.disable_move != 0) {
2701
+ volatiles.disable_move = 0;
2702
+ volatiles.disable_duration = 0;
2703
+ options.chance.observe(.disable, who, .None);
2704
+ try log.end(.{ ident, .DisableSilent });
2554
2705
  }
2555
2706
  if (volatiles.Confusion) {
2556
2707
  // volatiles.confusion is left unchanged
2557
2708
  volatiles.Confusion = false;
2558
- try log.end(ident, .ConfusionSilent);
2709
+ options.chance.observe(.confusion, who, .None);
2710
+ try log.end(.{ ident, .ConfusionSilent });
2559
2711
  }
2560
2712
  if (volatiles.Mist) {
2561
2713
  volatiles.Mist = false;
2562
- try log.end(ident, .Mist);
2714
+ try log.end(.{ ident, .MistSilent });
2563
2715
  }
2564
2716
  if (volatiles.FocusEnergy) {
2565
2717
  volatiles.FocusEnergy = false;
2566
- try log.end(ident, .FocusEnergy);
2718
+ try log.end(.{ ident, .FocusEnergySilent });
2567
2719
  }
2568
2720
  if (volatiles.LeechSeed) {
2569
2721
  volatiles.LeechSeed = false;
2570
- try log.end(ident, .LeechSeed);
2722
+ try log.end(.{ ident, .LeechSeedSilent });
2571
2723
  }
2572
2724
  if (!showdown and volatiles.Toxic) {
2573
2725
  volatiles.Toxic = false;
@@ -2575,102 +2727,385 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
2575
2727
  }
2576
2728
  if (volatiles.LightScreen) {
2577
2729
  volatiles.LightScreen = false;
2578
- try log.end(ident, .LightScreen);
2730
+ try log.end(.{ ident, .LightScreenSilent });
2579
2731
  }
2580
2732
  if (volatiles.Reflect) {
2581
2733
  volatiles.Reflect = false;
2582
- try log.end(ident, .Reflect);
2734
+ try log.end(.{ ident, .ReflectSilent });
2583
2735
  }
2584
2736
  if (showdown and volatiles.Toxic) {
2585
2737
  volatiles.Toxic = false;
2586
2738
  // Pokémon Showdown erroneously clears the toxic counter
2587
2739
  volatiles.toxic = 0;
2588
- try log.end(ident, .Toxic);
2740
+ try log.end(.{ ident, .ToxicSilent });
2589
2741
  }
2590
2742
  }
2591
2743
 
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;
2744
+ fn decrement(
2745
+ comptime field: chance.Action.Field,
2746
+ player: Player,
2747
+ options: anytype,
2748
+ n: anytype,
2749
+ ) @TypeOf(n) {
2750
+ return if (options.calc.overridden(player, field)) |obs| switch (obs) {
2751
+ .started, .ended => 0,
2752
+ .continuing => if (n > 1) n - 1 else n,
2753
+ else => unreachable,
2754
+ } else n - 1;
2603
2755
  }
2604
2756
 
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);
2757
+ pub const Rolls = struct {
2758
+ fn speedTie(battle: anytype, options: anytype) !bool {
2759
+ const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
2760
+ player == .P1
2761
+ else if (@hasDecl(@TypeOf(battle.rng), "speedTie"))
2762
+ battle.rng.speedTie()
2763
+ else if (showdown)
2764
+ battle.rng.range(u8, 0, 2) == 0
2765
+ else
2766
+ battle.rng.next() < Gen12.percent(50) + 1;
2767
+
2768
+ try options.chance.speedTie(p1);
2769
+ return p1;
2770
+ }
2771
+
2772
+ fn criticalHit(
2773
+ battle: anytype,
2774
+ player: Player,
2775
+ rate: u8,
2776
+ spurious: bool,
2777
+ options: anytype,
2778
+ ) !bool {
2779
+ const crit = if (options.calc.overridden(player, .critical_hit)) |val|
2780
+ val == .true
2781
+ else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
2782
+ battle.rng.criticalHit(player, rate)
2783
+ else if (showdown)
2784
+ battle.rng.chance(u8, rate, 256)
2785
+ else
2786
+ std.math.rotl(u8, battle.rng.next(), 3) < rate;
2787
+
2788
+ if (!spurious) try options.chance.criticalHit(player, crit, rate);
2789
+ return crit;
2609
2790
  }
2610
- }
2611
2791
 
2612
- fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
2613
- var volatiles = &active.volatiles;
2614
- assert(volatiles.Thrashing);
2615
- if (volatiles.attacks > 0) return false;
2792
+ fn damage(battle: anytype, player: Player, options: anytype) !u8 {
2793
+ const roll = if (options.calc.overridden(player, .damage)) |val|
2794
+ val
2795
+ else if (@hasDecl(@TypeOf(battle.rng), "damage"))
2796
+ battle.rng.damage(player)
2797
+ else roll: {
2798
+ if (showdown) break :roll battle.rng.range(u8, 217, 256);
2799
+ while (true) {
2800
+ const r = std.math.rotr(u8, battle.rng.next(), 1);
2801
+ if (r >= 217) break :roll r;
2802
+ }
2803
+ };
2616
2804
 
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);
2805
+ assert(roll >= 217 and roll <= 255);
2806
+ try options.chance.damage(player, roll);
2807
+ return roll;
2808
+ }
2623
2809
 
2624
- return true;
2625
- }
2810
+ const uN = if (pkmn.options.miss) u8 else u9;
2811
+ fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) !bool {
2812
+ const acc: uN = if (pkmn.options.miss) accuracy else @as(uN, accuracy) + 1;
2813
+ const ok = if (options.calc.overridden(player, .hit)) |val|
2814
+ val == .true
2815
+ else if (@hasDecl(@TypeOf(battle.rng), "hit"))
2816
+ battle.rng.hit(player, acc)
2817
+ else if (showdown)
2818
+ battle.rng.chance(uN, acc, 256)
2819
+ else
2820
+ battle.rng.next() < acc;
2626
2821
 
2627
- const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
2822
+ try options.chance.hit(player, ok, acc);
2823
+ return ok;
2824
+ }
2628
2825
 
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
- }
2826
+ fn confused(battle: anytype, player: Player, options: anytype) !bool {
2827
+ const cfz = if (options.calc.overridden(player, .confused)) |val|
2828
+ val == .true
2829
+ else if (@hasDecl(@TypeOf(battle.rng), "confused"))
2830
+ battle.rng.confused(player)
2831
+ else if (showdown)
2832
+ !battle.rng.chance(u8, 128, 256)
2833
+ else
2834
+ battle.rng.next() >= Gen12.percent(50) + 1;
2634
2835
 
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;
2836
+ try options.chance.confused(player, cfz);
2837
+ return cfz;
2838
+ }
2839
+
2840
+ fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2841
+ const par = if (options.calc.overridden(player, .paralyzed)) |val|
2842
+ val == .true
2843
+ else if (@hasDecl(@TypeOf(battle.rng), "paralyzed"))
2844
+ battle.rng.paralyzed(player)
2845
+ else if (showdown)
2846
+ battle.rng.chance(u8, 63, 256)
2847
+ else
2848
+ battle.rng.next() < Gen12.percent(25);
2849
+
2850
+ try options.chance.paralyzed(player, par);
2851
+ return par;
2852
+ }
2853
+
2854
+ fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2855
+ const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2856
+ val == .true
2857
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2858
+ battle.rng.secondaryChance(player, 25)
2859
+ else if (showdown)
2860
+ battle.rng.chance(u8, 25, 256)
2861
+ else
2862
+ battle.rng.next() < Gen12.percent(10);
2863
+
2864
+ try options.chance.secondaryChance(player, proc, 25);
2865
+ return proc;
2866
+ }
2867
+
2868
+ fn secondaryChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2869
+ const rate: u8 = if (low) 26 else 77;
2870
+
2871
+ const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2872
+ val == .true
2873
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2874
+ battle.rng.secondaryChance(player, rate)
2875
+ else if (showdown)
2876
+ battle.rng.chance(u8, rate, 256)
2877
+ else
2878
+ battle.rng.next() < 1 + (if (low) Gen12.percent(10) else Gen12.percent(30));
2879
+
2880
+ try options.chance.secondaryChance(player, proc, rate);
2881
+ return proc;
2882
+ }
2883
+
2884
+ fn poisonChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2885
+ const rate: u8 = if (low) 52 else 103;
2886
+
2887
+ const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2888
+ val == .true
2889
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2890
+ battle.rng.secondaryChance(player, rate)
2891
+ else if (showdown)
2892
+ battle.rng.chance(u8, rate, 256)
2893
+ else
2894
+ battle.rng.next() < 1 + (if (low) Gen12.percent(20) else Gen12.percent(40));
2895
+
2896
+ try options.chance.secondaryChance(player, proc, rate);
2897
+ return proc;
2898
+ }
2899
+
2900
+ fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2901
+ const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2902
+ val == .true
2903
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2904
+ battle.rng.secondaryChance(player, 85)
2905
+ else if (showdown)
2906
+ battle.rng.chance(u8, 85, 256)
2907
+ else
2908
+ battle.rng.next() < Gen12.percent(33) + 1;
2909
+
2910
+ try options.chance.secondaryChance(player, proc, 85);
2911
+ return proc;
2912
+ }
2913
+
2914
+ fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2915
+ const move: Move = if (options.calc.overridden(player, .metronome)) |val|
2916
+ val
2917
+ else if (@hasDecl(@TypeOf(battle.rng), "metronome"))
2918
+ battle.rng.metronome(player)
2919
+ else if (showdown)
2920
+ Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
2921
+ else move: {
2922
+ while (true) {
2923
+ const r = battle.rng.next();
2924
+ if (r == 0 or r == @intFromEnum(Move.Metronome)) continue;
2925
+ if (r >= @intFromEnum(Move.Struggle)) continue;
2926
+ break :move @enumFromInt(r);
2642
2927
  }
2928
+ };
2929
+
2930
+ try options.chance.metronome(player, move);
2931
+ return move;
2932
+ }
2933
+
2934
+ fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2935
+ const power = if (options.calc.overridden(player, .psywave)) |val|
2936
+ val - 1
2937
+ else if (@hasDecl(@TypeOf(battle.rng), "psywave"))
2938
+ battle.rng.psywave(player)
2939
+ else if (showdown)
2940
+ battle.rng.range(u8, 0, max)
2941
+ else power: {
2942
+ while (true) {
2943
+ const r = battle.rng.next();
2944
+ if (r < max) break :power r;
2945
+ }
2946
+ };
2947
+
2948
+ assert(power < max);
2949
+ try options.chance.psywave(player, power, max);
2950
+ return power;
2951
+ }
2952
+
2953
+ fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2954
+ const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2955
+ @intCast(val)
2956
+ else if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
2957
+ battle.rng.sleepDuration(player)
2958
+ else if (showdown)
2959
+ battle.rng.range(u3, 1, 8)
2960
+ else duration: {
2961
+ while (true) {
2962
+ const r = battle.rng.next() & 7;
2963
+ if (r != 0) break :duration @intCast(r);
2964
+ }
2965
+ };
2966
+
2967
+ assert(duration >= 1 and duration <= 7);
2968
+ options.chance.duration(player, duration);
2969
+ return duration;
2970
+ }
2971
+
2972
+ fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2973
+ const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
2974
+ val
2975
+ else 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
+ @intCast(val)
2990
+ else if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
2991
+ battle.rng.confusionDuration(player)
2992
+ else if (showdown)
2993
+ battle.rng.range(u3, 2, 6)
2994
+ else
2995
+ @intCast((battle.rng.next() & 3) + 2);
2996
+
2997
+ assert(duration >= 2 and duration <= 5);
2998
+ options.chance.duration(player, duration);
2999
+ return duration;
3000
+ }
3001
+
3002
+ fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
3003
+ const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
3004
+ @intCast(val)
3005
+ else if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
3006
+ battle.rng.attackingDuration(player)
3007
+ else if (showdown)
3008
+ battle.rng.range(u3, 2, 4)
3009
+ else
3010
+ @intCast((battle.rng.next() & 1) + 2);
3011
+
3012
+ assert(duration >= 2 and duration <= 3);
3013
+ options.chance.duration(player, duration);
3014
+ return duration;
3015
+ }
3016
+
3017
+ const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
3018
+
3019
+ fn distribution(
3020
+ battle: anytype,
3021
+ comptime effect: Move.Effect,
3022
+ player: Player,
3023
+ options: anytype,
3024
+ ) !u3 {
3025
+ const roll = if (effect == .Binding) .duration else .multi_hit;
3026
+ const n: u3 = if (options.calc.overridden(player, roll)) |val|
3027
+ @intCast(val)
3028
+ else if (@hasDecl(@TypeOf(battle.rng), "distribution"))
3029
+ battle.rng.distribution(player)
3030
+ else if (showdown)
3031
+ DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
3032
+ else n: {
3033
+ const r = (battle.rng.next() & 3);
3034
+ break :n @intCast((if (r < 2) r else battle.rng.next() & 3) + 2);
3035
+ };
3036
+
3037
+ assert(n >= 2 and n <= 5);
3038
+ if (effect == .Binding) {
3039
+ options.chance.duration(player, n);
2643
3040
  } 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;
3041
+ try options.chance.multiHit(player, n);
3042
+ }
3043
+ return n;
3044
+ }
3045
+
3046
+ fn moveSlot(
3047
+ battle: anytype,
3048
+ player: Player,
3049
+ moves: []MoveSlot,
3050
+ check_pp: u4,
3051
+ options: anytype,
3052
+ ) !u4 {
3053
+ // TODO: consider throwing error instead of rerolling?
3054
+ const overridden = if (options.calc.overridden(player, .move_slot)) |val|
3055
+ if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
3056
+ val
3057
+ else
3058
+ null
3059
+ else
3060
+ null;
3061
+
3062
+ const slot: u4 = overridden orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
3063
+ battle.rng.moveSlot(player, moves, check_pp)
3064
+ else slot: {
3065
+ if (showdown) {
3066
+ if (check_pp == 0) {
3067
+ var i: usize = moves.len;
3068
+ while (i > 0) {
3069
+ i -= 1;
3070
+ if (moves[i].id != .None) {
3071
+ break :slot battle.rng.range(u4, 0, @as(u4, @intCast(i + 1))) + 1;
3072
+ }
3073
+ }
3074
+ } else {
3075
+ var r = battle.rng.range(u4, 0, check_pp) + 1;
3076
+ var i: usize = 0;
3077
+ while (i < moves.len and r > 0) : (i += 1) {
3078
+ if (moves[i].pp > 0) {
3079
+ r -= 1;
3080
+ if (r == 0) break :slot @intCast(i + 1);
3081
+ }
3082
+ }
2650
3083
  }
2651
3084
  }
2652
- return @intCast(u4, i + 1);
2653
- }
2654
- }
2655
3085
 
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;
3086
+ while (true) {
3087
+ const r: u4 = @intCast(battle.rng.next() & 3);
3088
+ if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) break :slot r + 1;
3089
+ }
3090
+ };
3091
+
3092
+ try options.chance.moveSlot(player, slot, moves, check_pp);
3093
+ return slot;
2659
3094
  }
2660
- }
3095
+ };
2661
3096
 
2662
3097
  test "RNG agreement" {
2663
3098
  if (!showdown) return;
2664
3099
  var expected: [256]u32 = undefined;
2665
3100
  for (0..expected.len) |i| {
2666
- expected[i] = @intCast(u32, i * 0x1000000);
3101
+ expected[i] = @intCast(i * 0x1000000);
2667
3102
  }
2668
3103
 
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 };
3104
+ var spe: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3105
+ var cfz: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3106
+ var par: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3107
+ var brn: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3108
+ var eff: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
2674
3109
 
2675
3110
  for (0..expected.len) |i| {
2676
3111
  try expectEqual(spe.range(u8, 0, 2) == 0, i < Gen12.percent(50) + 1);
@@ -2706,11 +3141,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
2706
3141
  const side = battle.side(player);
2707
3142
  const foe = battle.foe(player);
2708
3143
 
2709
- var active = side.active;
3144
+ const active = &side.active;
2710
3145
  const stored = side.stored();
2711
3146
 
2712
3147
  // 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.
3148
+ // Pokémon Showdown instead produces a list with a single move that must be chosen
2714
3149
  //
2715
3150
  // Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
2716
3151
  // here when not in compatibility mode, but the engine needs to be able to differentiate
@@ -2718,9 +3153,9 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
2718
3153
  // passing due to being forced into using a move. Instead of introducing another option
2719
3154
  // we simply repurpose Move with no move slot, even though pedantically this is not
2720
3155
  // strictly correct as the player would not have been presented the option to move or
2721
- // switch at all.
3156
+ // switch at all
2722
3157
  if (isForced(active)) {
2723
- out[n] = .{ .type = .Move, .data = @boolToInt(showdown) };
3158
+ out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
2724
3159
  n += 1;
2725
3160
  return n;
2726
3161
  }
@@ -2737,7 +3172,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
2737
3172
  // On the cartridge, all of these happen after "FIGHT" (indicating you are not
2738
3173
  // switching) but before you are allowed to select a move. Pokémon Showdown instead
2739
3174
  // either disables all other moves in the case of limited or requires you to select a
2740
- // move normally if sleeping/frozen/bound.
3175
+ // move normally if sleeping/frozen/bound
2741
3176
  if (!showdown and (limited or foe.active.volatiles.Binding or
2742
3177
  Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
2743
3178
  {
@@ -2750,16 +3185,18 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
2750
3185
  // Pokémon Showdown handles Bide and Binding moves by checking if the move in question
2751
3186
  // is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
2752
3187
  // 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.
3188
+ // proc-ed move in their moveset) and disabling all other moves
2754
3189
  if (limited) {
2755
3190
  assert(showdown);
2756
3191
  assert(side.last_selected_move != .None);
2757
3192
  while (slot <= 4) : (slot += 1) {
2758
3193
  const m = active.moves[slot - 1];
2759
3194
  if (m.id == .None) break;
2760
- if (m.id == side.last_selected_move) {
2761
- // Pokémon Showdown displays Struggle if limited to Bide when out of PP
2762
- const s = if (m.pp == 0 and m.id == .Bide) 0 else slot;
3195
+ if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
3196
+ // Pokémon Showdown displays Struggle if limited to Bide but unable to pick
3197
+ const struggle =
3198
+ m.id == .Bide and (m.pp == 0 or active.volatiles.disable_move == slot);
3199
+ const s = if (struggle) 0 else slot;
2763
3200
  out[n] = .{ .type = .Move, .data = s };
2764
3201
  n += 1;
2765
3202
  return n;
@@ -2773,7 +3210,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
2773
3210
  const m = active.moves[slot - 1];
2774
3211
  if (m.id == .None) break;
2775
3212
  if (m.pp == 0) continue;
2776
- if (active.volatiles.disabled_move == slot) continue;
3213
+ if (active.volatiles.disable_move == slot) continue;
2777
3214
  out[n] = .{ .type = .Move, .data = slot };
2778
3215
  n += 1;
2779
3216
  }