@pkmn/engine 0.1.0-dev.7fe22fc0 → 0.1.0-dev.7feed58c

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