@pkmn/engine 0.1.0-dev.2c13d3ec → 0.1.0-dev.2c426670

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