@pkmn/engine 0.1.0-dev.2ad317f2 → 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 (53) hide show
  1. package/LICENSE +1 -3
  2. package/README.md +44 -19
  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 +123 -109
  21. package/package.json +43 -23
  22. package/src/bin/install-pkmn-engine +26 -18
  23. package/src/lib/common/data.zig +12 -24
  24. package/src/lib/common/debug.zig +2 -2
  25. package/src/lib/common/optional.zig +9 -2
  26. package/src/lib/common/options.zig +2 -0
  27. package/src/lib/common/protocol.zig +802 -327
  28. package/src/lib/common/rational.zig +113 -19
  29. package/src/lib/common/rng.zig +26 -17
  30. package/src/lib/gen1/calc.zig +501 -97
  31. package/src/lib/gen1/chance.zig +333 -383
  32. package/src/lib/gen1/data/moves.zig +192 -180
  33. package/src/lib/gen1/data/species.zig +3 -3
  34. package/src/lib/gen1/data/types.zig +7 -7
  35. package/src/lib/gen1/data.zig +50 -62
  36. package/src/lib/gen1/helpers.zig +87 -548
  37. package/src/lib/gen1/mechanics.zig +562 -510
  38. package/src/lib/gen2/calc.zig +143 -0
  39. package/src/lib/gen2/chance.zig +882 -0
  40. package/src/lib/gen2/data/items.zig +12 -18
  41. package/src/lib/gen2/data/moves.zig +663 -569
  42. package/src/lib/gen2/data/species.zig +1 -1
  43. package/src/lib/gen2/data/types.zig +134 -31
  44. package/src/lib/gen2/data.zig +106 -79
  45. package/src/lib/gen2/generated.zig +806 -0
  46. package/src/lib/gen2/helpers.zig +45 -19
  47. package/src/lib/gen2/mechanics.zig +3560 -9
  48. package/src/lib/{binding/node.zig → node.zig} +10 -13
  49. package/src/lib/pkmn.zig +27 -12
  50. package/src/lib/{binding/wasm.zig → wasm.zig} +2 -1
  51. package/src/test/display/pkmn.html.tmpl +5 -5
  52. package/src/lib/gen1/README.md +0 -892
  53. package/src/lib/gen2/README.md +0 -308
@@ -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,15 +162,13 @@ 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);
@@ -210,16 +198,13 @@ fn selectMove(
210
198
  if (choice.data == 0) {
211
199
  const struggle = ok: {
212
200
  for (side.active.moves, 0..) |move, i| {
213
- 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;
214
202
  }
215
203
  break :ok true;
216
204
  };
217
-
218
205
  assert(struggle);
219
- saveMove(battle, player, choice);
220
- } else {
221
- saveMove(battle, player, choice);
222
206
  }
207
+ saveMove(battle, player, choice);
223
208
 
224
209
  return null;
225
210
  }
@@ -232,7 +217,7 @@ fn saveMove(battle: anytype, player: Player, choice: ?Choice) void {
232
217
  if (c.data == 0) {
233
218
  side.last_selected_move = .Struggle;
234
219
  } else {
235
- assert(showdown or side.active.volatiles.disabled_move != c.data);
220
+ assert(showdown or side.active.volatiles.disable_move != c.data);
236
221
  const move = side.active.move(c.data);
237
222
  // You cannot *select* a move with 0 PP (except on Pokémon Showdown where that is
238
223
  // sometimes required...), but a 0 PP move can be used automatically
@@ -270,17 +255,17 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: a
270
255
  active.moves = incoming.moves;
271
256
 
272
257
  statusModify(incoming.status, &active.stats);
258
+ options.chance.switched(player, slot);
273
259
 
274
260
  foe.active.volatiles.Binding = false;
275
261
 
276
- try options.log.switched(battle.active(player), incoming);
277
- options.chance.switched(player, side.order[0], out);
262
+ try options.log.switched(.{ battle.active(player), incoming });
278
263
 
279
264
  if (showdown and incoming.status == Status.TOX) {
280
265
  incoming.status = Status.init(.PSN);
281
266
  // Technically, Pokémon Showdown adds these after *both* Pokémon have switched, but we'd
282
267
  // rather not clutter up turnOrder just for this (incorrect) log message
283
- try options.log.status(battle.active(player), incoming.status, .Silent);
268
+ try options.log.status(.{ battle.active(player), incoming.status, .Silent });
284
269
  }
285
270
  }
286
271
 
@@ -343,12 +328,10 @@ fn doTurn(
343
328
  player_choice: Choice,
344
329
  player_rewrap: bool,
345
330
  player_skip: bool,
346
- player_from: ?Move,
347
331
  foe_player: Player,
348
332
  foe_choice: Choice,
349
333
  foe_rewrap: bool,
350
334
  foe_skip: bool,
351
- foe_from: ?Move,
352
335
  options: anytype,
353
336
  ) !?Result {
354
337
  assert(player_choice.type != .Pass);
@@ -361,19 +344,18 @@ fn doTurn(
361
344
  player_choice,
362
345
  player_rewrap,
363
346
  player_skip,
364
- player_from,
365
347
  &residual,
366
348
  options,
367
349
  )) |r| return r;
368
350
  if (!replace) {
369
351
  if (player_choice.type != .Switch) {
370
- if (try checkFaint(battle, foe_player, options)) |r| return r;
352
+ if (try checkFaint(battle, foe_player, true, options)) |r| return r;
371
353
  }
372
354
  if (residual) try handleResidual(battle, player, options);
373
- if (try checkFaint(battle, player, options)) |r| return r;
355
+ if (try checkFaint(battle, player, false, options)) |r| return r;
374
356
  } else if (foe_choice.type == .Pass) return null;
375
357
 
376
- options.chance.clearPending();
358
+ if (!showdown) options.chance.clearPending();
377
359
 
378
360
  residual = true;
379
361
  replace = battle.side(foe_player).stored().hp == 0;
@@ -384,16 +366,15 @@ fn doTurn(
384
366
  foe_choice,
385
367
  foe_rewrap,
386
368
  foe_skip,
387
- foe_from,
388
369
  &residual,
389
370
  options,
390
371
  )) |r| return r;
391
372
  if (!replace) {
392
373
  if (!calc and foe_choice.type != .Switch) {
393
- if (try checkFaint(battle, player, options)) |r| return r;
374
+ if (try checkFaint(battle, player, true, options)) |r| return r;
394
375
  }
395
376
  if (residual) try handleResidual(battle, foe_player, options);
396
- if (try checkFaint(battle, foe_player, options)) |r| return r;
377
+ if (try checkFaint(battle, foe_player, false, options)) |r| return r;
397
378
  }
398
379
 
399
380
  // Flinch is bugged on Pokémon Showdown because it gets implemented with a duration which causes
@@ -412,7 +393,6 @@ fn executeMove(
412
393
  choice: Choice,
413
394
  rewrap: bool,
414
395
  skip: bool,
415
- from: ?Move,
416
396
  residual: *bool,
417
397
  options: anytype,
418
398
  ) !?Result {
@@ -429,7 +409,7 @@ fn executeMove(
429
409
  if (side.last_selected_move == .SKIP_TURN) {
430
410
  assert(!showdown);
431
411
  if (battle.foe(player).active.volatiles.Binding) {
432
- try options.log.cant(battle.active(player), .Bound);
412
+ try options.log.cant(.{ battle.active(player), .Bound });
433
413
  }
434
414
  return null;
435
415
  }
@@ -447,11 +427,13 @@ fn executeMove(
447
427
  mslot = @intCast(battle.lastMove(player).index);
448
428
  const stored = side.stored();
449
429
  // GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
450
- auto = side.last_selected_move == .HyperBeam or side.active.volatiles.Bide or
451
- 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);
452
434
  // If it wasn't Hyper Beam or the continuation of a move effect then we must have just
453
435
  // thawed, in which case we will desync unless the last_selected_move happened to be at
454
- // 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
455
437
  if (!auto) {
456
438
  // side.active.moves(slot) is safe to check even though the slot in question might not
457
439
  // technically be from this Pokémon because it must be exactly 1 to not desync and
@@ -470,7 +452,7 @@ fn executeMove(
470
452
 
471
453
  var skip_can = false;
472
454
  var skip_pp = false;
473
- switch (try beforeMove(battle, player, skip, from, residual, options)) {
455
+ switch (try beforeMove(battle, player, skip, residual, options)) {
474
456
  .done => return null,
475
457
  .skip_can => skip_can = true,
476
458
  .skip_pp => skip_pp = true,
@@ -478,9 +460,9 @@ fn executeMove(
478
460
  .err => return @as(?Result, Result.Error),
479
461
  }
480
462
 
481
- if (!skip_can and !try canMove(battle, player, mslot, auto, skip_pp, from, residual, options)) {
482
- return null;
483
- }
463
+ const can = skip_can or
464
+ try canMove(battle, player, mslot, auto, skip_pp, .None, residual, options);
465
+ if (!can) return null;
484
466
 
485
467
  return doMove(battle, player, mslot, rewrap, auto, residual, options);
486
468
  }
@@ -491,7 +473,6 @@ fn beforeMove(
491
473
  battle: anytype,
492
474
  player: Player,
493
475
  skip: bool,
494
- from: ?Move,
495
476
  residual: *bool,
496
477
  options: anytype,
497
478
  ) !BeforeMove {
@@ -506,35 +487,39 @@ fn beforeMove(
506
487
  if (Status.is(stored.status, .SLP)) {
507
488
  const before = stored.status;
508
489
  // Even if the EXT bit is set this will still correctly modify the sleep duration
509
- if (options.calc.modify(player, .sleep)) |extend| {
510
- 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,
511
496
  } else {
512
497
  stored.status -= 1;
513
498
  }
514
499
 
515
500
  const duration = Status.duration(stored.status);
516
- if (!Status.is(stored.status, .EXT)) {
517
- try options.chance.sleep(player, duration);
518
- }
501
+ try options.chance.sleep(player, if (Status.is(stored.status, .EXT))
502
+ .None
503
+ else if (duration == 0) .ended else .continuing);
519
504
 
520
505
  if (duration == 0) {
521
- try log.curestatus(ident, before, .Message);
506
+ try log.curestatus(.{ ident, before, .Message });
522
507
  stored.status = 0; // clears EXT if present
523
508
  } else {
524
- try log.cant(ident, .Sleep);
509
+ try log.cant(.{ ident, .Sleep });
525
510
  }
526
511
  side.last_used_move = .None;
527
512
  return .done;
528
513
  }
529
514
 
530
515
  if (Status.is(stored.status, .FRZ)) {
531
- try log.cant(ident, .Freeze);
516
+ try log.cant(.{ ident, .Freeze });
532
517
  side.last_used_move = .None;
533
518
  return .done;
534
519
  }
535
520
 
536
521
  if (skip or foe.active.volatiles.Binding) {
537
- try log.cant(ident, .Bound);
522
+ try log.cant(.{ ident, .Bound });
538
523
  return .done;
539
524
  }
540
525
 
@@ -542,27 +527,27 @@ fn beforeMove(
542
527
  // Pokémon Showdown doesn't clear Flinch until its imaginary "residual" phase, meaning
543
528
  // Pokémon can sometimes flinch multiple times from the same original hit
544
529
  if (!showdown) volatiles.Flinch = false;
545
- try log.cant(ident, .Flinch);
530
+ try log.cant(.{ ident, .Flinch });
546
531
  return .done;
547
532
  }
548
533
 
549
534
  if (volatiles.Recharging) {
550
535
  volatiles.Recharging = false;
551
- try log.cant(ident, .Recharge);
536
+ try log.cant(.{ ident, .Recharge });
552
537
  return .done;
553
538
  }
554
539
 
555
- if (volatiles.disabled_duration > 0) {
556
- if (options.calc.modify(player, .disable)) |extend| {
557
- if (!extend) volatiles.disabled_duration = 0;
558
- } else {
559
- volatiles.disabled_duration -= 1;
560
- }
561
- 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
+ );
562
547
 
563
- if (volatiles.disabled_duration == 0) {
564
- volatiles.disabled_move = 0;
565
- try log.end(ident, .Disable);
548
+ if (volatiles.disable_duration == 0) {
549
+ volatiles.disable_move = 0;
550
+ try log.end(.{ ident, .Disable });
566
551
  }
567
552
  }
568
553
  // Pokémon Showdown's disable condition has a single onBeforeMove handler
@@ -577,19 +562,25 @@ fn beforeMove(
577
562
 
578
563
  if (volatiles.Confusion) {
579
564
  assert(volatiles.confusion > 0);
580
-
581
- if (options.calc.modify(player, .confusion)) |extend| {
582
- 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,
583
573
  } else {
584
574
  volatiles.confusion -= 1;
585
575
  }
586
- try options.chance.confusion(player, volatiles.confusion);
576
+
577
+ try options.chance.confusion(player, if (volatiles.confusion == 0) .ended else .continuing);
587
578
 
588
579
  if (volatiles.confusion == 0) {
589
580
  volatiles.Confusion = false;
590
- try log.end(ident, .Confusion);
581
+ try log.end(.{ ident, .Confusion });
591
582
  } else {
592
- try log.activate(ident, .Confusion);
583
+ try log.activate(.{ ident, .Confusion });
593
584
 
594
585
  if (try Rolls.confused(battle, player, options)) {
595
586
  assert(!volatiles.MultiHit);
@@ -601,7 +592,10 @@ fn beforeMove(
601
592
  volatiles.Charging = false;
602
593
  volatiles.Binding = false;
603
594
  volatiles.Invulnerable = false;
604
- 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);
605
599
  {
606
600
  // This feels (and is) disgusting but the cartridge literally just overwrites
607
601
  // the opponent's defense with the user's defense and resets it after. As a
@@ -609,7 +603,9 @@ fn beforeMove(
609
603
  const def = foe.active.stats.def;
610
604
  foe.active.stats.def = active.stats.def;
611
605
  defer foe.active.stats.def = def;
612
- if (!calcDamage(battle, player, player.foe(), null, false)) return .err;
606
+ if (!calcDamage(battle, player, player.foe(), null, false, options)) {
607
+ return .err;
608
+ }
613
609
  }
614
610
  // Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
615
611
  // on the targeting behavior of the confused Pokémon's selected move which results
@@ -638,9 +634,13 @@ fn beforeMove(
638
634
  volatiles.Thrashing = false;
639
635
  volatiles.Charging = false;
640
636
  volatiles.Binding = false;
641
- 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
+
642
642
  // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
643
- try log.cant(ident, .Paralysis);
643
+ try log.cant(.{ ident, .Paralysis });
644
644
  return .done;
645
645
  }
646
646
 
@@ -656,28 +656,22 @@ fn beforeMove(
656
656
  volatiles.state +%= battle.last_damage;
657
657
  }
658
658
 
659
- assert(volatiles.attacks > 0);
660
-
661
- if (options.calc.modify(player, .attacking)) |extend| {
662
- if (!extend) volatiles.attacks = 0;
663
- } else {
664
- volatiles.attacks -= 1;
665
- }
666
- 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);
667
661
 
668
662
  if (volatiles.attacks != 0) {
669
- try log.activate(ident, .Bide);
663
+ try log.activate(.{ ident, .Bide });
670
664
  return .done;
671
665
  }
672
666
 
673
667
  volatiles.Bide = false;
674
- try log.end(ident, .Bide);
668
+ try log.end(.{ ident, .Bide });
675
669
 
676
670
  battle.last_damage = volatiles.state *% 2;
677
671
  volatiles.state = 0;
678
672
 
679
673
  if (battle.last_damage == 0) {
680
- try log.fail(ident, .None);
674
+ try log.fail(.{ ident, .None });
681
675
  return .done;
682
676
  }
683
677
 
@@ -697,47 +691,38 @@ fn beforeMove(
697
691
  }
698
692
 
699
693
  if (volatiles.Thrashing) {
700
- assert(volatiles.attacks > 0);
701
- assert(from != null);
694
+ try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
702
695
 
703
- if (options.calc.modify(player, .attacking)) |extend| {
704
- if (!extend) volatiles.attacks = 0;
705
- } else {
706
- volatiles.attacks -= 1;
707
- }
708
- 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);
709
698
 
710
- if (!showdown and handleThrashing(battle, active, player, options)) {
711
- try log.start(battle.active(player), .ConfusionSilent);
712
- }
713
- try log.move(ident, side.last_selected_move, battle.active(player.foe()), from);
714
- if (showdown) {
715
- // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
716
- // used in useMove and doesn't have the notion of skipping canMove semantics
717
- side.last_used_move = side.last_selected_move;
718
- if (handleThrashing(battle, active, player, options)) {
719
- try log.start(battle.active(player), .ConfusionSilent);
720
- }
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);
721
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
+
722
718
  return .skip_can;
723
719
  }
724
720
 
725
721
  if (volatiles.Binding) {
726
- assert(volatiles.attacks > 0);
722
+ volatiles.attacks = decrement(.binding, player, options, volatiles.attacks);
723
+ try options.chance.binding(player, if (volatiles.attacks == 0) .ended else .continuing);
727
724
 
728
- if (options.calc.modify(player, .binding)) |extend| {
729
- if (!extend) volatiles.attacks = 0;
730
- } else {
731
- volatiles.attacks -= 1;
732
- }
733
- try options.chance.binding(player, volatiles.attacks);
734
-
735
- try log.move(
736
- battle.active(player),
737
- side.last_selected_move,
738
- battle.active(player.foe()),
739
- side.last_selected_move,
740
- );
725
+ try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
741
726
  if (showdown or battle.last_damage != 0) {
742
727
  const sub = showdown and foe.active.volatiles.Substitute;
743
728
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -757,7 +742,7 @@ fn canMove(
757
742
  mslot: u4,
758
743
  auto: bool,
759
744
  skip_pp: bool,
760
- from: ?Move,
745
+ from: Move,
761
746
  residual: *bool,
762
747
  options: anytype,
763
748
  ) !bool {
@@ -765,33 +750,21 @@ fn canMove(
765
750
  const player_ident = battle.active(player);
766
751
  const move = Move.get(side.last_selected_move);
767
752
 
768
- const foe_last = battle.foe(player).last_used_move;
769
- const special = from != null and (from.? == .Metronome and
770
- (from.? == .MirrorMove and !(foe_last == .None or foe_last == .MirrorMove)));
771
-
772
- var skip = skip_pp;
773
753
  if (side.active.volatiles.Charging) {
774
754
  side.active.volatiles.Charging = false;
775
755
  side.active.volatiles.Invulnerable = false;
776
756
  } else if (move.effect == .Charge) {
777
- try options.log.move(player_ident, side.last_selected_move, .{}, from);
757
+ try options.log.move(.{ player_ident, side.last_selected_move, ID{}, from });
778
758
  setCounterable(battle, player, side, move);
779
759
  try Effects.charge(battle, player, options);
780
760
  return false;
781
761
  }
782
762
 
783
- if (!showdown or !special) side.last_used_move = side.last_selected_move;
784
- if (!skip) decrementPP(side, mslot, auto);
763
+ side.last_used_move = side.last_selected_move;
764
+ if (!skip_pp) decrementPP(side, mslot, auto);
785
765
 
786
766
  const target = if (move.target == .Self) player else player.foe();
787
- // Pokémon Showdown's protocol for Rage and Binding moves should come with a [from], though we
788
- // don't have space to be able to track it in all circumstances and thus usually try to infer it
789
- // from side.last_used_move. However, this gets reset by the opponent switching or fainting,
790
- // meaning sometimes we lose this information. from is still set because it not being null is
791
- // relevant for control flow, but we don't have anything useful to put in the log. This is
792
- // regrettable, but this information is more "nice to have" than required
793
- const f = if (from != null and from.? == .None) null else from;
794
- 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 });
795
768
  setCounterable(battle, player, side, move);
796
769
 
797
770
  if (move.effect.onBegin()) {
@@ -829,12 +802,14 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
829
802
  assert(!volatiles.Rage and !volatiles.Thrashing and !volatiles.MultiHit);
830
803
  if (volatiles.Bide) return;
831
804
 
832
- assert(active.move(mslot).pp > 0 or auto);
833
- 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;
834
808
  if (volatiles.Transform) return;
835
809
 
836
- assert(side.stored().move(mslot).pp > 0 or auto);
837
- 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;
838
813
  assert(active.move(mslot).pp == side.stored().move(mslot).pp);
839
814
  }
840
815
 
@@ -895,7 +870,7 @@ fn doMove(
895
870
  break :miss true;
896
871
  }
897
872
  break :miss (move.target != .Self and
898
- !moveHit(battle, player, move, &immune, &mist, options));
873
+ !try moveHit(battle, player, move, &immune, &mist, options));
899
874
  };
900
875
  assert(!immune or miss or (showdown and move.effect == .Binding));
901
876
 
@@ -924,7 +899,8 @@ fn doMove(
924
899
  // This can overflow after adjustDamage, but will still be sufficient to OHKO
925
900
  battle.last_damage = if (ohko) 65535 else 0;
926
901
  if (showdown) break :blk; // skip adjustDamage / randomizeDamage
927
- } 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);
928
904
  return @as(?Result, Result.Error);
929
905
  }
930
906
  if (battle.last_damage == 0) {
@@ -938,11 +914,12 @@ fn doMove(
938
914
  }
939
915
  }
940
916
 
941
- var zero = battle.last_damage == 0;
942
- miss = if (showdown or skip)
943
- miss
944
- else
945
- (!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
+ }
946
923
 
947
924
  assert(showdown or miss or battle.last_damage > 0 or skip);
948
925
  assert((!showdown and miss) or !(ohko and immune));
@@ -966,16 +943,29 @@ fn doMove(
966
943
  ohko = (!showdown or (!immune and !invulnerable)) and
967
944
  move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
968
945
  if (ohko) {
969
- try log.immune(foe_ident, .OHKO);
946
+ try log.immune(.{ foe_ident, .OHKO });
970
947
  } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
971
- 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
+ }
972
962
  } else if (mist) {
973
- if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
974
- 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 });
975
965
  } else {
976
- if (showdown or !zero) try options.chance.commit(player, .miss);
977
- try log.lastmiss();
978
- 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)});
979
969
  }
980
970
  if (move.effect == .JumpKick) {
981
971
  // Recoil is supposed to be damage/8 but damage will always be 0 here
@@ -988,17 +978,15 @@ fn doMove(
988
978
  try buildRage(battle, player.foe(), options);
989
979
  } else if (showdown and move.effect == .Disable) {
990
980
  try buildRage(battle, player.foe(), options);
991
- } else if (!showdown and immune and move.effect == .Binding) {
992
- try options.chance.commit(player, .binding);
993
981
  }
994
982
  return null;
995
- } else {
983
+ } else if (!showdown) {
996
984
  try options.chance.commit(player, .hit);
997
985
  }
998
986
 
999
987
  // On the cartridge MultiHit doesn't get set up until after damage has been applied for the
1000
988
  // first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
1001
- // it up above before damage calculation).
989
+ // it up above before damage calculation)
1002
990
  if (!showdown and move.effect.isMulti()) {
1003
991
  try Effects.multiHit(battle, player, move, options);
1004
992
  hits = side.active.volatiles.attacks;
@@ -1011,15 +999,15 @@ fn doMove(
1011
999
  var hit: u4 = 0;
1012
1000
  while (hit < hits) {
1013
1001
  if (hit == 0) {
1014
- if (crit) try log.crit(battle.active(player.foe()));
1002
+ if (crit) try log.crit(.{battle.active(player.foe())});
1015
1003
  if (effectiveness > Effectiveness.neutral) {
1016
- try log.supereffective(battle.active(player.foe()));
1004
+ try log.supereffective(.{battle.active(player.foe())});
1017
1005
  } else if (effectiveness < Effectiveness.neutral) {
1018
- try log.resisted(battle.active(player.foe()));
1006
+ try log.resisted(.{battle.active(player.foe())});
1019
1007
  }
1020
1008
  }
1021
1009
  if (!skip) nullified = try applyDamage(battle, player.foe(), player.foe(), .None, options);
1022
- if (hit == 0 and ohko) try log.ohko();
1010
+ if (hit == 0 and ohko) try log.ohko(.{});
1023
1011
  hit += 1;
1024
1012
  if (foe.stored().hp == 0) break;
1025
1013
  if (!late and (!sub or move.effect == .Explode)) {
@@ -1036,7 +1024,7 @@ fn doMove(
1036
1024
  if (showdown and move.effect == .Twineedle and !nullified and foe.stored().hp > 0) {
1037
1025
  try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1038
1026
  }
1039
- try log.hitcount(battle.active(player.foe()), hit);
1027
+ try log.hitcount(.{ battle.active(player.foe()), hit });
1040
1028
  } else if (showdown) {
1041
1029
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
1042
1030
  if (move.effect == .Binding) {
@@ -1046,9 +1034,9 @@ fn doMove(
1046
1034
  assert(foe.stored().hp > 0);
1047
1035
  // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
1048
1036
  if (sub) {
1049
- try log.activate(battle.active(player.foe()), .Substitute);
1037
+ try log.activate(.{ battle.active(player.foe()), .Substitute });
1050
1038
  } else {
1051
- try log.damage(battle.active(player.foe()), foe.stored(), .None);
1039
+ try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
1052
1040
  try buildRage(battle, player.foe(), options);
1053
1041
  }
1054
1042
  return null;
@@ -1057,12 +1045,12 @@ fn doMove(
1057
1045
  }
1058
1046
 
1059
1047
  // Substitute being broken nullifies the move's effect completely so even
1060
- // 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
1061
1049
  if (nullified) return null;
1062
1050
 
1063
1051
  // On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
1064
1052
  // but this is only done to setup the MultiHit looping in the first place. Moving the MultiHit
1065
- // 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
1066
1054
  if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
1067
1055
 
1068
1056
  if (foe.stored().hp == 0) return null;
@@ -1077,7 +1065,7 @@ fn doMove(
1077
1065
  // below). For Twineedle we change the data to that of one with PoisonChance1 given its
1078
1066
  // MultiHit behavior is complete after the loop above, though Pokémon Showdown handles the
1079
1067
  // Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
1080
- // puts the |-status| message before |-hitcount| instead of after.
1068
+ // puts the |-status| message before |-hitcount| instead of after
1081
1069
  if (move.effect == .Twineedle) {
1082
1070
  if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1083
1071
  } else if (move.effect == .Disable) {
@@ -1096,12 +1084,14 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
1096
1084
  const side = battle.side(player);
1097
1085
 
1098
1086
  // Base speed is used for the critical hit calculation, even when Transform-ed
1099
- var rate = @as(u16, Species.chance(side.stored().species));
1087
+ var rate: u16 = Species.chance(side.stored().species);
1100
1088
  // GLITCH: Focus Energy reduces critical hit rate instead of increasing it
1101
1089
  rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
1102
1090
  rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
1103
1091
 
1104
- 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);
1105
1095
  }
1106
1096
 
1107
1097
  fn calcDamage(
@@ -1110,6 +1100,7 @@ fn calcDamage(
1110
1100
  target_player: Player,
1111
1101
  m: ?Move.Data,
1112
1102
  crit: bool,
1103
+ options: anytype,
1113
1104
  ) bool {
1114
1105
  // Confusion (indicated when m == null) just needs a 40 BP physical move
1115
1106
  const cfz = m == null;
@@ -1121,28 +1112,8 @@ fn calcDamage(
1121
1112
 
1122
1113
  const special = move.type.special();
1123
1114
 
1124
- // zig fmt: off
1125
- var atk: u32 =
1126
- if (crit)
1127
- if (special) side.stored().stats.spc
1128
- else side.stored().stats.atk
1129
- else
1130
- if (special) side.active.stats.spc
1131
- else side.active.stats.atk;
1132
- var def: u32 =
1133
- if (crit)
1134
- if (special) target.stored().stats.spc
1135
- else target.stored().stats.def
1136
- else
1137
- // GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
1138
- if (special)
1139
- target.active.stats.spc *
1140
- @as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
1141
- // Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
1142
- else
1143
- target.active.stats.def *
1144
- @as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
1145
- // zig fmt: on
1115
+ var atk = attack(side, crit, special);
1116
+ var def = defense(target, crit, special, cfz);
1146
1117
 
1147
1118
  // Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
1148
1119
  // will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
@@ -1153,9 +1124,9 @@ fn calcDamage(
1153
1124
  def = @max((def / 4) & 255, if (showdown) 1 else 0);
1154
1125
  }
1155
1126
 
1156
- 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);
1157
1128
 
1158
- 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;
1159
1130
 
1160
1131
  if (def == 0) return false;
1161
1132
 
@@ -1167,11 +1138,41 @@ fn calcDamage(
1167
1138
  d = @min(997, d);
1168
1139
  d += 2;
1169
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
+
1170
1151
  battle.last_damage = @intCast(d);
1171
1152
 
1172
1153
  return true;
1173
1154
  }
1174
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
+
1175
1176
  fn adjustDamage(battle: anytype, player: Player) u16 {
1176
1177
  const side = battle.side(player);
1177
1178
  const foe = battle.foe(player);
@@ -1216,7 +1217,6 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1216
1217
  const foe = battle.foe(player);
1217
1218
 
1218
1219
  if (!try checkHit(battle, player, move, options)) return null;
1219
- try options.chance.commit(player, .hit);
1220
1220
 
1221
1221
  battle.last_damage = switch (side.last_selected_move) {
1222
1222
  .SuperFang => @max(foe.stored().hp / 2, 1),
@@ -1246,7 +1246,7 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1246
1246
  const foe = battle.foe(player);
1247
1247
 
1248
1248
  if (battle.last_damage == 0) {
1249
- try options.log.fail(battle.active(player), .None);
1249
+ try options.log.fail(.{ battle.active(player), .None });
1250
1250
  return null;
1251
1251
  }
1252
1252
 
@@ -1264,22 +1264,21 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1264
1264
  foe_last_selected_move.type == .Fighting);
1265
1265
 
1266
1266
  if (!used and !selected) {
1267
- try options.log.fail(battle.active(player), .None);
1267
+ try options.log.fail(.{ battle.active(player), .None });
1268
1268
  return null;
1269
1269
  }
1270
1270
 
1271
1271
  if (!used or !selected) {
1272
1272
  // GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
1273
1273
  if (!showdown) return Result.Error;
1274
- try options.log.fail(battle.active(player), .None);
1274
+ try options.log.fail(.{ battle.active(player), .None });
1275
1275
  return null;
1276
1276
  }
1277
1277
 
1278
1278
  battle.last_damage *|= 2;
1279
1279
 
1280
- // Pokémon Showdown calls checkHit before Counter
1280
+ // Pokémon Showdown calls moveHit before Counter
1281
1281
  if (!showdown and !try checkHit(battle, player, move, options)) return null;
1282
- try options.chance.commit(player, .hit);
1283
1282
 
1284
1283
  const sub = showdown and foe.active.volatiles.Substitute;
1285
1284
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -1307,20 +1306,20 @@ fn applyDamage(
1307
1306
  subbed.active.volatiles.substitute = 0;
1308
1307
  subbed.active.volatiles.Substitute = false;
1309
1308
  // battle.last_damage is not updated with the amount of HP the Substitute had
1310
- try options.log.end(battle.active(sub_player), .Substitute);
1311
- options.calc.capped(target_player);
1309
+ try options.log.end(.{ battle.active(sub_player), .Substitute });
1310
+ options.calc.capped(sub_player);
1312
1311
  return true;
1313
1312
  } else {
1314
1313
  // Safe to truncate since less than subbed.volatiles.substitute which is a u8
1315
1314
  subbed.active.volatiles.substitute -= @intCast(battle.last_damage);
1316
- try options.log.activate(battle.active(sub_player), .Substitute);
1315
+ try options.log.activate(.{ battle.active(sub_player), .Substitute });
1317
1316
  return false;
1318
1317
  }
1319
1318
  }
1320
1319
 
1321
1320
  if (battle.last_damage > target.stored().hp) battle.last_damage = target.stored().hp;
1322
1321
  target.stored().hp -= battle.last_damage;
1323
- try options.log.damage(battle.active(target_player), target.stored(), reason);
1322
+ try options.log.damage(.{ battle.active(target_player), target.stored(), reason });
1324
1323
  return false;
1325
1324
  }
1326
1325
 
@@ -1339,7 +1338,7 @@ fn mirrorMove(
1339
1338
  side.last_selected_move = foe.last_used_move;
1340
1339
 
1341
1340
  if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
1342
- try options.log.fail(battle.active(player), .None);
1341
+ try options.log.fail(.{ battle.active(player), .None });
1343
1342
  return null;
1344
1343
  } else if (!showdown or foe.last_used_move != .Struggle) {
1345
1344
  incrementPP(side, mslot);
@@ -1375,20 +1374,23 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
1375
1374
  var immune = false;
1376
1375
  var mist = false;
1377
1376
 
1378
- 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
+ }
1379
1381
 
1380
1382
  assert(!immune);
1381
1383
  if (mist) {
1382
1384
  assert(!showdown);
1383
1385
  const foe_ident = battle.active(player.foe());
1384
- try options.log.activate(foe_ident, .Mist);
1385
- try options.log.fail(foe_ident, .None);
1386
+ try options.log.activate(.{ foe_ident, .Mist });
1387
+ try options.log.fail(.{ foe_ident, .None });
1386
1388
  } else {
1387
- try options.chance.commit(player, .miss);
1388
- try options.log.lastmiss();
1389
- try options.log.miss(battle.active(player));
1389
+ try options.log.lastmiss(.{});
1390
+ try options.log.miss(.{battle.active(player)});
1390
1391
  }
1391
1392
 
1393
+ if (!showdown) try options.chance.commit(player, .miss);
1392
1394
  return false;
1393
1395
  }
1394
1396
 
@@ -1399,7 +1401,7 @@ fn moveHit(
1399
1401
  immune: *bool,
1400
1402
  mist: *bool,
1401
1403
  options: anytype,
1402
- ) bool {
1404
+ ) !bool {
1403
1405
  var side = battle.side(player);
1404
1406
  const foe = battle.foe(player);
1405
1407
 
@@ -1438,7 +1440,7 @@ fn moveHit(
1438
1440
  const overwritten = overwrite and state > 0;
1439
1441
  assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
1440
1442
 
1441
- var accuracy = if (overwritten) state else @as(u16, Gen12.percent(move.accuracy));
1443
+ var accuracy: u16 = if (overwritten) state else move.accuracy;
1442
1444
  var boost = BOOSTS[@as(u4, @intCast(@as(i8, side.active.boosts.accuracy) + 6))];
1443
1445
  accuracy = accuracy * boost[0] / boost[1];
1444
1446
  boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
@@ -1450,7 +1452,7 @@ fn moveHit(
1450
1452
  if (overwrite) side.active.volatiles.state = accuracy;
1451
1453
 
1452
1454
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1453
- break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
1455
+ break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
1454
1456
  };
1455
1457
 
1456
1458
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1461,13 +1463,17 @@ fn moveHit(
1461
1463
 
1462
1464
  if (!miss) return true;
1463
1465
  if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
1466
+
1464
1467
  side.active.volatiles.Binding = false;
1468
+ options.chance.observe(.binding, player, .None);
1469
+
1465
1470
  return false;
1466
1471
  }
1467
1472
 
1468
1473
  fn checkFaint(
1469
1474
  battle: anytype,
1470
1475
  player: Player,
1476
+ cap: bool,
1471
1477
  options: anytype,
1472
1478
  ) @TypeOf(options.log).Error!?Result {
1473
1479
  const side = battle.side(player);
@@ -1477,33 +1483,32 @@ fn checkFaint(
1477
1483
  const foe_fainted = foe.stored().hp == 0;
1478
1484
 
1479
1485
  const player_out = findFirstAlive(side) == 0;
1480
- const foe_out = findFirstAlive(foe) == 0;
1481
- const tie = player_out and foe_out;
1482
- 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;
1483
1488
 
1484
- if (try faint(battle, player, !(more or foe_fainted), options)) |r| return r;
1485
- 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;
1486
1491
 
1487
1492
  assert(!side.active.volatiles.MultiHit);
1488
1493
  assert(!foe.active.volatiles.MultiHit);
1489
1494
 
1490
- if (tie) {
1491
- try options.log.tie();
1495
+ if (player_out and foe_out) {
1496
+ try options.log.tie(.{});
1492
1497
  return Result.Tie;
1493
1498
  } else if (player_out) {
1494
- try options.log.win(player.foe());
1499
+ try options.log.win(.{player.foe()});
1495
1500
  return if (player == .P1) Result.Lose else Result.Win;
1496
1501
  } else if (foe_out) {
1497
- try options.log.win(player);
1502
+ try options.log.win(.{player});
1498
1503
  return if (player == .P1) Result.Win else Result.Lose;
1499
1504
  }
1500
1505
 
1501
1506
  const foe_choice: Choice.Type = if (foe_fainted) .Switch else .Pass;
1502
- if (player == .P1) return Result{ .p1 = .Switch, .p2 = foe_choice };
1503
- return Result{ .p1 = foe_choice, .p2 = .Switch };
1507
+ if (player == .P1) return .{ .p1 = .Switch, .p2 = foe_choice };
1508
+ return .{ .p1 = foe_choice, .p2 = .Switch };
1504
1509
  }
1505
1510
 
1506
- 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 {
1507
1512
  var side = battle.side(player);
1508
1513
  var foe = battle.foe(player);
1509
1514
  assert(side.stored().hp == 0);
@@ -1536,8 +1541,10 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
1536
1541
  side.stored().stats.spe;
1537
1542
  }
1538
1543
 
1539
- try options.log.faint(battle.active(player), done);
1540
- 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);
1541
1548
  return null;
1542
1549
  }
1543
1550
 
@@ -1548,21 +1555,19 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1548
1555
  var volatiles = &side.active.volatiles;
1549
1556
 
1550
1557
  const brn = Status.is(stored.status, .BRN);
1551
- if (brn or Status.is(stored.status, .PSN)) blk: {
1558
+ if (brn or Status.is(stored.status, .PSN)) {
1552
1559
  var damage = @max(stored.stats.hp / 16, 1);
1553
-
1554
1560
  if (volatiles.Toxic) {
1555
1561
  volatiles.toxic += 1;
1556
1562
  damage *= volatiles.toxic;
1557
1563
  }
1564
+ damage = @min(damage, stored.hp);
1558
1565
 
1559
- const amount = @min(damage, stored.hp);
1560
- if (showdown and amount == 0) break :blk;
1561
-
1562
- stored.hp -= amount;
1563
-
1564
- // Pokémon Showdown uses damageOf here but its not relevant in Generation I
1565
- 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
+ }
1566
1571
  }
1567
1572
 
1568
1573
  if (volatiles.LeechSeed) {
@@ -1586,14 +1591,14 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1586
1591
  const amount = @min(damage, stored.hp);
1587
1592
  stored.hp -= amount;
1588
1593
 
1589
- // As above, Pokémon Showdown uses damageOf but its not relevant
1590
- 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 });
1591
1596
 
1592
1597
  const before = foe_stored.hp;
1593
1598
  // Uncapped damage is added back to the foe
1594
1599
  foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
1595
1600
  // Pokémon Showdown uses the less specific heal here instead of drain... because reasons?
1596
- 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 });
1597
1602
  }
1598
1603
  }
1599
1604
 
@@ -1604,18 +1609,18 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
1604
1609
  battle.turn += 1;
1605
1610
 
1606
1611
  if (showdown and pkmn.options.ebc and checkEBC(battle)) {
1607
- try options.log.tie();
1612
+ try options.log.tie(.{});
1608
1613
  return Result.Tie;
1609
1614
  }
1610
1615
 
1611
1616
  if (showdown and battle.turn >= 1000) {
1612
- try options.log.tie();
1617
+ try options.log.tie(.{});
1613
1618
  return Result.Tie;
1614
1619
  } else if (battle.turn >= 65535) {
1615
1620
  return Result.Error;
1616
1621
  }
1617
1622
 
1618
- try options.log.turn(battle.turn);
1623
+ try options.log.turn(.{battle.turn});
1619
1624
 
1620
1625
  return Result.Default;
1621
1626
  }
@@ -1675,9 +1680,9 @@ fn checkEBC(battle: anytype) bool {
1675
1680
  }
1676
1681
 
1677
1682
  fn disabled(side: *Side, ident: ID, options: anytype) !bool {
1678
- if (side.active.volatiles.disabled_move != 0) {
1683
+ if (side.active.volatiles.disable_move != 0) {
1679
1684
  // A Pokémon that transforms after being disabled may end up with less move slots
1680
- 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;
1681
1686
  // side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
1682
1687
  const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
1683
1688
  m
@@ -1685,32 +1690,21 @@ fn disabled(side: *Side, ident: ID, options: anytype) !bool {
1685
1690
  side.last_selected_move;
1686
1691
  if (m != .None and m == last) {
1687
1692
  side.active.volatiles.Charging = false;
1688
- try options.log.disabled(ident, last);
1693
+ try options.log.cant(.{ ident, .Disable, last });
1689
1694
  return true;
1690
1695
  }
1691
1696
  }
1692
1697
  return false;
1693
1698
  }
1694
1699
 
1695
- inline fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1700
+ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1696
1701
  const side = battle.side(who);
1697
1702
  if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
1698
1703
  try Effects.boost(battle, who, Move.get(.Rage), options);
1699
1704
  }
1700
1705
  }
1701
1706
 
1702
- fn handleThrashing(battle: anytype, active: *ActivePokemon, player: Player, options: anytype) bool {
1703
- var volatiles = &active.volatiles;
1704
- assert(volatiles.Thrashing);
1705
- if (volatiles.attacks > 0) return false;
1706
-
1707
- volatiles.Thrashing = false;
1708
- volatiles.Confusion = true;
1709
- volatiles.confusion = Rolls.confusionDuration(battle, player, true, options);
1710
- return true;
1711
- }
1712
-
1713
- inline fn onBegin(
1707
+ fn onBegin(
1714
1708
  battle: anytype,
1715
1709
  player: Player,
1716
1710
  move: Move.Data,
@@ -1718,7 +1712,6 @@ inline fn onBegin(
1718
1712
  residual: *bool,
1719
1713
  options: anytype,
1720
1714
  ) !void {
1721
- assert(move.effect.onBegin());
1722
1715
  return switch (move.effect) {
1723
1716
  .Confusion => Effects.confusion(battle, player, move, options),
1724
1717
  .Conversion => Effects.conversion(battle, player, options),
@@ -1740,8 +1733,7 @@ inline fn onBegin(
1740
1733
  };
1741
1734
  }
1742
1735
 
1743
- inline fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1744
- assert(move.effect.onEnd());
1736
+ fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1745
1737
  return switch (move.effect) {
1746
1738
  .Bide => Effects.bide(battle, player, options),
1747
1739
  // zig fmt: off
@@ -1756,14 +1748,13 @@ inline fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anyty
1756
1748
  };
1757
1749
  }
1758
1750
 
1759
- inline fn alwaysHappens(
1751
+ fn alwaysHappens(
1760
1752
  battle: anytype,
1761
1753
  player: Player,
1762
1754
  move: Move.Data,
1763
1755
  residual: *bool,
1764
1756
  options: anytype,
1765
1757
  ) !void {
1766
- assert(move.effect.alwaysHappens());
1767
1758
  return switch (move.effect) {
1768
1759
  .DrainHP, .DreamEater => Effects.drainHP(battle, player, options),
1769
1760
  .Explode => Effects.explode(battle, player),
@@ -1775,7 +1766,7 @@ inline fn alwaysHappens(
1775
1766
  };
1776
1767
  }
1777
1768
 
1778
- 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 {
1779
1770
  return switch (move.effect) {
1780
1771
  .BurnChance1, .BurnChance2 => Effects.burnChance(battle, player, move, options),
1781
1772
  .ConfusionChance => Effects.confusion(battle, player, move, options),
@@ -1799,10 +1790,12 @@ pub const Effects = struct {
1799
1790
 
1800
1791
  side.active.volatiles.Bide = true;
1801
1792
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1793
+
1802
1794
  side.active.volatiles.state = 0;
1803
1795
  side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
1796
+ options.chance.observe(.bide, player, .started);
1804
1797
 
1805
- try options.log.start(battle.active(player), .Bide);
1798
+ try options.log.start(.{ battle.active(player), .Bide });
1806
1799
  }
1807
1800
 
1808
1801
  fn burnChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1816,11 +1809,11 @@ pub const Effects = struct {
1816
1809
  // GLITCH: Freeze top move selection desync can occur if thawed Pokémon is slower
1817
1810
  if (Status.is(foe_stored.status, .FRZ)) {
1818
1811
  assert(move.type == .Fire);
1819
- try options.log.curestatus(
1812
+ try options.log.curestatus(.{
1820
1813
  battle.active(player.foe()),
1821
1814
  foe_stored.status,
1822
1815
  .Message,
1823
- );
1816
+ });
1824
1817
  foe_stored.status = 0;
1825
1818
  }
1826
1819
  return;
@@ -1834,7 +1827,7 @@ pub const Effects = struct {
1834
1827
  foe_stored.status = Status.init(.BRN);
1835
1828
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
1836
1829
 
1837
- 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 });
1838
1831
  }
1839
1832
 
1840
1833
  fn charge(battle: anytype, player: Player, options: anytype) !void {
@@ -1844,8 +1837,8 @@ pub const Effects = struct {
1844
1837
  volatiles.Charging = true;
1845
1838
  const move = side.last_selected_move;
1846
1839
  if (move == .Fly or move == .Dig) volatiles.Invulnerable = true;
1847
- try options.log.laststill();
1848
- try options.log.prepare(battle.active(player), move);
1840
+ try options.log.laststill(.{});
1841
+ try options.log.prepare(.{ battle.active(player), move });
1849
1842
  }
1850
1843
 
1851
1844
  fn confusion(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1856,42 +1849,38 @@ pub const Effects = struct {
1856
1849
  if (!try Rolls.confusionChance(battle, player, options)) return;
1857
1850
  } else {
1858
1851
  if (showdown) {
1859
- if (!try checkHit(battle, player, move, options)) {
1860
- return;
1861
- } else if (sub) {
1862
- return options.log.fail(battle.active(player.foe()), .None);
1863
- }
1852
+ if (!try checkHit(battle, player, move, options)) return;
1853
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1864
1854
  } else {
1865
- if (sub) {
1866
- return options.log.fail(battle.active(player.foe()), .None);
1867
- } else if (!try checkHit(battle, player, move, options)) {
1868
- return;
1869
- }
1855
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1856
+ if (!try checkHit(battle, player, move, options)) return;
1870
1857
  }
1871
- try options.chance.commit(player, .hit);
1872
1858
  }
1873
1859
 
1874
1860
  if (foe.active.volatiles.Confusion) return;
1875
1861
  foe.active.volatiles.Confusion = true;
1876
- foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
1877
1862
 
1878
- 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 });
1879
1867
  }
1880
1868
 
1881
1869
  fn conversion(battle: anytype, player: Player, options: anytype) !void {
1882
1870
  const foe = battle.foe(player);
1883
1871
 
1884
1872
  if (foe.active.volatiles.Invulnerable) {
1885
- try options.log.lastmiss();
1886
- return options.log.miss(battle.active(player));
1873
+ try options.log.lastmiss(.{});
1874
+ return options.log.miss(.{battle.active(player)});
1887
1875
  }
1888
1876
 
1889
1877
  battle.side(player).active.types = foe.active.types;
1890
- return options.log.typechange(
1878
+ return options.log.start(.{
1891
1879
  battle.active(player),
1880
+ .TypeChange,
1892
1881
  foe.active.types,
1893
1882
  battle.active(player.foe()),
1894
- );
1883
+ });
1895
1884
  }
1896
1885
 
1897
1886
  fn disable(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
@@ -1900,13 +1889,10 @@ pub const Effects = struct {
1900
1889
  const foe_ident = battle.active(player.foe());
1901
1890
 
1902
1891
  // Pokémon Showdown handles hit/miss earlier in doMove
1903
- if (!showdown) {
1904
- if (!try checkHit(battle, player, move, options)) return null;
1905
- try options.chance.commit(player, .hit);
1906
- }
1892
+ if (!showdown and !try checkHit(battle, player, move, options)) return null;
1907
1893
 
1908
- if (volatiles.disabled_move != 0) {
1909
- try options.log.fail(foe_ident, .None);
1894
+ if (volatiles.disable_move != 0) {
1895
+ try options.log.fail(.{ foe_ident, .None });
1910
1896
  return null;
1911
1897
  }
1912
1898
 
@@ -1922,7 +1908,7 @@ pub const Effects = struct {
1922
1908
  // Technically this is still considered simply a "miss" on the cartridge,
1923
1909
  // but diverging from Pokémon Showdown here would mostly just be pedantic
1924
1910
  if (n == 0) {
1925
- try options.log.fail(foe_ident, .None);
1911
+ try options.log.fail(.{ foe_ident, .None });
1926
1912
  return null;
1927
1913
  } else if (err) {
1928
1914
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
@@ -1930,18 +1916,19 @@ pub const Effects = struct {
1930
1916
  return Result.Error;
1931
1917
  }
1932
1918
 
1933
- volatiles.disabled_move =
1919
+ volatiles.disable_move =
1934
1920
  @intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
1935
- 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);
1936
1923
 
1937
- const id = foe.active.move(volatiles.disabled_move).id;
1938
- 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 });
1939
1926
  return null;
1940
1927
  }
1941
1928
 
1942
1929
  fn drainHP(battle: anytype, player: Player, options: anytype) !void {
1943
- var side = battle.side(player);
1944
- var stored = side.stored();
1930
+ var stored = battle.side(player).stored();
1931
+ const ident = battle.active(player);
1945
1932
 
1946
1933
  if (battle.last_damage == 0) {
1947
1934
  assert(showdown);
@@ -1954,7 +1941,7 @@ pub const Effects = struct {
1954
1941
  if (stored.hp == stored.stats.hp) return;
1955
1942
  stored.hp = @min(stored.stats.hp, stored.hp + drain);
1956
1943
 
1957
- try options.log.drain(battle.active(player), stored, battle.active(player.foe()));
1944
+ try options.log.heal(.{ ident, stored, .Drain, battle.active(player.foe()) });
1958
1945
  }
1959
1946
 
1960
1947
  fn explode(battle: anytype, player: Player) !void {
@@ -1986,7 +1973,7 @@ pub const Effects = struct {
1986
1973
  if (side.active.volatiles.FocusEnergy) return;
1987
1974
  side.active.volatiles.FocusEnergy = true;
1988
1975
 
1989
- try options.log.start(battle.active(player), .FocusEnergy);
1976
+ try options.log.start(.{ battle.active(player), .FocusEnergy });
1990
1977
  }
1991
1978
 
1992
1979
  fn freezeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2008,7 +1995,7 @@ pub const Effects = struct {
2008
1995
  foe_stored.status = Status.init(.FRZ);
2009
1996
  // GLITCH: Hyper Beam recharging status is not cleared
2010
1997
 
2011
- try options.log.status(foe_ident, foe_stored.status, .None);
1998
+ try options.log.status(.{ foe_ident, foe_stored.status, .None });
2012
1999
  }
2013
2000
 
2014
2001
  fn haze(battle: anytype, player: Player, options: anytype) !void {
@@ -2016,7 +2003,7 @@ pub const Effects = struct {
2016
2003
  var side = battle.side(player);
2017
2004
  var foe = battle.foe(player);
2018
2005
 
2019
- var side_stored = side.stored();
2006
+ const side_stored = side.stored();
2020
2007
  var foe_stored = foe.stored();
2021
2008
 
2022
2009
  const player_ident = battle.active(player);
@@ -2028,8 +2015,8 @@ pub const Effects = struct {
2028
2015
  side.active.stats = side_stored.stats;
2029
2016
  foe.active.stats = foe_stored.stats;
2030
2017
 
2031
- try log.activate(player_ident, .Haze);
2032
- try log.clearallboost();
2018
+ try log.activate(.{ player_ident, .Haze });
2019
+ try log.clearallboost(.{});
2033
2020
 
2034
2021
  // Pokémon Showdown clears P1 then P2 instead of status -> side -> foe
2035
2022
  if (showdown) {
@@ -2037,24 +2024,26 @@ pub const Effects = struct {
2037
2024
  const p: Player = @enumFromInt(i);
2038
2025
  // Pokémon Showdown incorrectly does not prevent sleep/freeze from moving
2039
2026
  if (p != player and Status.any(s.stored().status)) {
2040
- try log.curestatus(foe_ident, foe_stored.status, .Silent);
2027
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2041
2028
  s.stored().status = 0;
2029
+ options.chance.observe(.sleep, p, .None);
2042
2030
  } else if (showdown and s.stored().status == Status.TOX) {
2043
2031
  s.stored().status = Status.init(.PSN);
2044
- try log.status(battle.active(p), s.stored().status, .None);
2032
+ try log.status(.{ battle.active(p), s.stored().status, .None });
2045
2033
  }
2046
- try clearVolatiles(battle, p, p != player, options);
2034
+ try clearVolatiles(battle, p, options);
2047
2035
  }
2048
2036
  } else {
2049
2037
  if (Status.any(foe_stored.status)) {
2050
2038
  if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
2051
2039
  foe.last_selected_move = .SKIP_TURN;
2040
+ options.chance.observe(.sleep, player.foe(), .None);
2052
2041
  }
2053
- try log.curestatus(foe_ident, foe_stored.status, .Silent);
2042
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2054
2043
  foe_stored.status = 0;
2055
2044
  }
2056
- try clearVolatiles(battle, player, false, options);
2057
- try clearVolatiles(battle, player.foe(), true, options);
2045
+ try clearVolatiles(battle, player, options);
2046
+ try clearVolatiles(battle, player.foe(), options);
2058
2047
  }
2059
2048
  }
2060
2049
 
@@ -2066,7 +2055,7 @@ pub const Effects = struct {
2066
2055
  // GLITCH: HP recovery move failure glitches
2067
2056
  const delta = stored.stats.hp - stored.hp;
2068
2057
  if (delta == 0 or (delta & 255 == 255 and stored.hp % 256 != 0)) {
2069
- return try options.log.fail(ident, .None);
2058
+ return options.log.fail(.{ ident, .None });
2070
2059
  }
2071
2060
 
2072
2061
  const rest = side.last_selected_move == .Rest;
@@ -2074,17 +2063,17 @@ pub const Effects = struct {
2074
2063
  // Adding the sleep status runs the sleep condition handler to roll duration
2075
2064
  if (showdown) battle.rng.advance(1);
2076
2065
  stored.status = Status.slf(2);
2077
- try options.log.statusFrom(ident, stored.status, Move.Rest);
2066
+ try options.log.status(.{ ident, stored.status, .From, Move.Rest });
2078
2067
  stored.hp = stored.stats.hp;
2079
2068
  } else {
2080
2069
  stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
2081
2070
  }
2082
- 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 });
2083
2072
  }
2084
2073
 
2085
2074
  fn hyperBeam(battle: anytype, player: Player, options: anytype) !void {
2086
2075
  battle.side(player).active.volatiles.Recharging = true;
2087
- try options.log.mustrecharge(battle.active(player));
2076
+ try options.log.mustrecharge(.{battle.active(player)});
2088
2077
  }
2089
2078
 
2090
2079
  fn leechSeed(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2093,33 +2082,31 @@ pub const Effects = struct {
2093
2082
  if (showdown) {
2094
2083
  // Invulnerability trumps type immunity on Pokémon Showdown
2095
2084
  if (!foe.active.volatiles.Invulnerable and foe.active.types.includes(.Grass)) {
2096
- return options.log.immune(battle.active(player.foe()), .None);
2085
+ return options.log.immune(.{ battle.active(player.foe()), .None });
2097
2086
  }
2098
2087
  if (!try checkHit(battle, player, move, options)) return;
2099
2088
  if (foe.active.volatiles.LeechSeed) return;
2100
2089
  } else {
2101
2090
  if (!try checkHit(battle, player, move, options)) return;
2102
2091
  if (foe.active.types.includes(.Grass) or foe.active.volatiles.LeechSeed) {
2103
- try options.log.lastmiss();
2104
- return options.log.miss(battle.active(player));
2092
+ try options.log.lastmiss(.{});
2093
+ return options.log.miss(.{battle.active(player)});
2105
2094
  }
2106
2095
  }
2107
2096
 
2108
- try options.chance.commit(player, .hit);
2109
2097
  foe.active.volatiles.LeechSeed = true;
2110
2098
 
2111
- try options.log.start(battle.active(player.foe()), .LeechSeed);
2099
+ try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
2112
2100
  }
2113
2101
 
2114
2102
  fn lightScreen(battle: anytype, player: Player, options: anytype) !void {
2115
2103
  var side = battle.side(player);
2104
+ const ident = battle.active(player);
2116
2105
 
2117
- if (side.active.volatiles.LightScreen) {
2118
- return options.log.fail(battle.active(player), .None);
2119
- }
2106
+ if (side.active.volatiles.LightScreen) return options.log.fail(.{ ident, .None });
2120
2107
  side.active.volatiles.LightScreen = true;
2121
2108
 
2122
- try options.log.start(battle.active(player), .LightScreen);
2109
+ try options.log.start(.{ ident, .LightScreen });
2123
2110
  }
2124
2111
 
2125
2112
  fn mimic(battle: anytype, player: Player, move: Move.Data, mslot: u8, options: anytype) !void {
@@ -2147,17 +2134,15 @@ pub const Effects = struct {
2147
2134
  if (!has_mimic) {
2148
2135
  // Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
2149
2136
  if (!try checkHit(battle, player, move, options)) return;
2150
- try options.chance.commit(player, .hit);
2151
- return try options.log.fail(battle.active(player.foe()), .None);
2137
+ return options.log.fail(.{ battle.active(player.foe()), .None });
2152
2138
  }
2153
2139
  }
2154
2140
  if (!try checkHit(battle, player, move, options)) return;
2155
- try options.chance.commit(player, .hit);
2156
2141
 
2157
2142
  const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
2158
2143
  side.active.move(oslot).id = foe.active.move(rslot).id;
2159
2144
 
2160
- 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 });
2161
2146
  }
2162
2147
 
2163
2148
  fn mist(battle: anytype, player: Player, options: anytype) !void {
@@ -2166,7 +2151,7 @@ pub const Effects = struct {
2166
2151
  if (side.active.volatiles.Mist) return;
2167
2152
  side.active.volatiles.Mist = true;
2168
2153
 
2169
- try options.log.start(battle.active(player), .Mist);
2154
+ try options.log.start(.{ battle.active(player), .Mist });
2170
2155
  }
2171
2156
 
2172
2157
  fn multiHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2192,25 +2177,25 @@ pub const Effects = struct {
2192
2177
 
2193
2178
  if (showdown) {
2194
2179
  // Invulnerability trumps type immunity on Pokémon Showdown
2195
- if (immune and !foe.active.volatiles.Invulnerable) return log.immune(foe_ident, .None);
2196
- 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;
2197
2183
  }
2198
2184
  if (Status.any(foe_stored.status)) {
2199
- return log.fail(
2185
+ return log.fail(.{
2200
2186
  foe_ident,
2201
- if (Status.is(foe_stored.status, .PAR)) .Paralysis else .None,
2202
- );
2187
+ if (Status.is(foe_stored.status, .PAR)) Fail.Paralysis else Fail.None,
2188
+ });
2203
2189
  }
2204
2190
  if (!showdown) {
2205
- if (immune) return log.immune(foe_ident, .None);
2191
+ if (immune) return log.immune(.{ foe_ident, .None });
2206
2192
  if (!try checkHit(battle, player, move, options)) return;
2207
2193
  }
2208
- try options.chance.commit(player, .hit);
2209
2194
 
2210
2195
  foe_stored.status = Status.init(.PAR);
2211
2196
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2212
2197
 
2213
- try log.status(foe_ident, foe_stored.status, .None);
2198
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2214
2199
  }
2215
2200
 
2216
2201
  fn paralyzeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2232,12 +2217,12 @@ pub const Effects = struct {
2232
2217
  foe_stored.status = Status.init(.PAR);
2233
2218
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2234
2219
 
2235
- 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 });
2236
2221
  }
2237
2222
 
2238
2223
  fn payDay(battle: anytype, player: Player, options: anytype) !void {
2239
2224
  if (!showdown or !battle.foe(player).active.volatiles.Substitute) {
2240
- try options.log.fieldactivate();
2225
+ try options.log.fieldactivate(.{});
2241
2226
  }
2242
2227
  }
2243
2228
 
@@ -2252,28 +2237,27 @@ pub const Effects = struct {
2252
2237
  return;
2253
2238
  } else if (foe.active.volatiles.Substitute) {
2254
2239
  if (move.effect != .Poison) return;
2255
- return log.fail(foe_ident, .None);
2240
+ return log.fail(.{ foe_ident, .None });
2256
2241
  } else if (Status.any(foe_stored.status)) {
2257
2242
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2258
2243
  // Pokémon Showdown considers Toxic to be a status even in Generation I and so
2259
2244
  // will not include a fail reason for Toxic vs. Poison or vice-versa...
2260
- 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))
2261
2246
  if (!showdown)
2262
- .Poison
2247
+ Fail.Poison
2263
2248
  else if (toxic == (foe_stored.status == Status.TOX))
2264
- if (toxic) .Toxic else .Poison
2249
+ if (toxic) Fail.Toxic else Fail.Poison
2265
2250
  else
2266
- .None
2251
+ Fail.None
2267
2252
  else
2268
- .None);
2253
+ Fail.None });
2269
2254
  } else if (foe.active.types.includes(.Poison)) {
2270
2255
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2271
- return log.immune(foe_ident, .None);
2256
+ return log.immune(.{ foe_ident, .None });
2272
2257
  }
2273
2258
 
2274
2259
  if (move.effect == .Poison) {
2275
2260
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2276
- try options.chance.commit(player, .hit);
2277
2261
  } else {
2278
2262
  if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
2279
2263
  return;
@@ -2287,7 +2271,7 @@ pub const Effects = struct {
2287
2271
  foe.active.volatiles.toxic = 0;
2288
2272
  }
2289
2273
 
2290
- try log.status(foe_ident, foe_stored.status, .None);
2274
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2291
2275
  }
2292
2276
 
2293
2277
  fn rage(battle: anytype, player: Player) !void {
@@ -2307,22 +2291,23 @@ pub const Effects = struct {
2307
2291
  @as(u8, if (side.last_selected_move == .Struggle) 2 else 4), 1));
2308
2292
  stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
2309
2293
 
2310
- try options.log.damageOf(
2294
+ try options.log.damage(.{
2311
2295
  battle.active(player),
2312
2296
  stored,
2313
2297
  .RecoilOf,
2314
2298
  battle.active(player.foe()),
2315
- );
2299
+ });
2316
2300
  if (showdown and stored.hp == 0) residual.* = false;
2317
2301
  }
2318
2302
 
2319
2303
  fn reflect(battle: anytype, player: Player, options: anytype) !void {
2320
2304
  var side = battle.side(player);
2305
+ const ident = battle.active(player);
2321
2306
 
2322
- 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 });
2323
2308
  side.active.volatiles.Reflect = true;
2324
2309
 
2325
- try options.log.start(battle.active(player), .Reflect);
2310
+ try options.log.start(.{ ident, .Reflect });
2326
2311
  }
2327
2312
 
2328
2313
  fn sleep(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2336,14 +2321,13 @@ pub const Effects = struct {
2336
2321
  // the Sleep Clause Mod might activate, causing us to not actually bypass
2337
2322
  } else {
2338
2323
  if (Status.any(foe_stored.status)) {
2339
- return options.log.fail(
2324
+ return options.log.fail(.{
2340
2325
  foe_ident,
2341
- if (Status.is(foe_stored.status, .SLP)) .Sleep else .None,
2342
- );
2326
+ if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
2327
+ });
2343
2328
  }
2344
- // 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
2345
2330
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2346
- try options.chance.commit(player, .hit);
2347
2331
  }
2348
2332
 
2349
2333
  // Sleep Clause Mod
@@ -2355,19 +2339,20 @@ pub const Effects = struct {
2355
2339
  foe.active.volatiles.Recharging = false;
2356
2340
 
2357
2341
  foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2342
+ options.chance.observe(.sleep, player.foe(), .started);
2343
+
2358
2344
  const last = battle.side(player).last_selected_move;
2359
- try options.log.statusFrom(foe_ident, foe_stored.status, last);
2345
+ try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
2360
2346
  }
2361
2347
 
2362
2348
  fn splash(battle: anytype, player: Player, options: anytype) !void {
2363
- try options.log.activate(battle.active(player), .Splash);
2349
+ try options.log.activate(.{ battle.active(player), .Splash });
2364
2350
  }
2365
2351
 
2366
2352
  fn substitute(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
2367
2353
  var side = battle.side(player);
2368
2354
  if (side.active.volatiles.Substitute) {
2369
- try options.log.fail(battle.active(player), .Substitute);
2370
- return;
2355
+ return options.log.fail(.{ battle.active(player), .Substitute });
2371
2356
  }
2372
2357
 
2373
2358
  assert(side.stored().stats.hp <= 1023);
@@ -2379,28 +2364,30 @@ pub const Effects = struct {
2379
2364
  // avoid having to convert to floating point)
2380
2365
  const required_hp = if (showdown) @divFloor(side.stored().stats.hp - 1, 4) + 1 else hp;
2381
2366
  if (side.stored().hp < required_hp) {
2382
- try options.log.fail(battle.active(player), .Weak);
2383
- return;
2367
+ return options.log.fail(.{ battle.active(player), .Weak });
2384
2368
  }
2385
2369
 
2386
2370
  // GLITCH: can leave the user with 0 HP (faints later) because didn't check '<=' above
2387
2371
  side.stored().hp -= hp;
2388
2372
  side.active.volatiles.substitute = hp + 1;
2389
2373
  side.active.volatiles.Substitute = true;
2390
- try options.log.start(battle.active(player), .Substitute);
2374
+ try options.log.start(.{ battle.active(player), .Substitute });
2391
2375
  if (hp > 0) {
2392
- try options.log.damage(battle.active(player), side.stored(), .None);
2376
+ try options.log.damage(.{ battle.active(player), side.stored(), .None });
2393
2377
  if (showdown and side.stored().hp == 0) residual.* = false;
2394
2378
  }
2395
2379
  }
2396
2380
 
2397
2381
  fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
2398
- if (!showdown or battle.side(player).last_selected_move == .Teleport) return;
2399
-
2400
2382
  // Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
2401
- if (try checkHit(battle, player, move, options)) try options.chance.commit(player, .hit);
2402
-
2403
- 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
+ }
2404
2391
  }
2405
2392
 
2406
2393
  fn thrashing(battle: anytype, player: Player, options: anytype) void {
@@ -2410,6 +2397,8 @@ pub const Effects = struct {
2410
2397
 
2411
2398
  volatiles.Thrashing = true;
2412
2399
  volatiles.attacks = Rolls.attackingDuration(battle, player, options);
2400
+
2401
+ options.chance.observe(.thrashing, player, .started);
2413
2402
  }
2414
2403
 
2415
2404
  fn transform(battle: anytype, player: Player, options: anytype) !void {
@@ -2438,7 +2427,7 @@ pub const Effects = struct {
2438
2427
  side.active.moves[i].pp = if (m.id != .None) 5 else 0;
2439
2428
  }
2440
2429
 
2441
- try options.log.transform(battle.active(player), foe_ident);
2430
+ try options.log.transform(.{ battle.active(player), foe_ident });
2442
2431
  }
2443
2432
 
2444
2433
  fn binding(battle: anytype, player: Player, rewrap: bool, options: anytype) !void {
@@ -2455,6 +2444,7 @@ pub const Effects = struct {
2455
2444
 
2456
2445
  side.active.volatiles.attacks =
2457
2446
  try Rolls.distribution(battle, .Binding, player, options) - 1;
2447
+ options.chance.observe(.binding, player, .started);
2458
2448
  }
2459
2449
 
2460
2450
  fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2468,87 +2458,87 @@ pub const Effects = struct {
2468
2458
  switch (move.effect) {
2469
2459
  .AttackUp1, .AttackUp2, .Rage => {
2470
2460
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2471
- if (boosts.atk == 6) return try log.fail(ident, .None);
2461
+ if (boosts.atk == 6) return log.fail(.{ ident, .None });
2472
2462
  const n: u2 = if (move.effect == .AttackUp2) 2 else 1;
2473
2463
  boosts.atk = @as(i4, @intCast(@min(6, @as(i8, boosts.atk) + n)));
2474
- const reason = if (move.effect == .Rage) Boost.Rage else Boost.Attack;
2464
+ const reason: Boost = if (move.effect == .Rage) .Rage else .Attack;
2475
2465
  if (stats.atk == MAX_STAT_VALUE) {
2476
2466
  boosts.atk -= 1;
2477
2467
  if (showdown) {
2478
- try log.boost(ident, reason, n);
2479
- try log.boost(ident, Boost.Attack, -1);
2468
+ try log.boost(.{ ident, reason, n });
2469
+ try log.boost(.{ ident, .Attack, -1 });
2480
2470
  if (move.effect == .Rage) return;
2481
2471
  }
2482
- return try log.fail(ident, .None);
2472
+ return log.fail(.{ ident, .None });
2483
2473
  }
2484
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2474
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2485
2475
  const stat = unmodifiedStats(battle, side).atk;
2486
2476
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2487
- try log.boost(ident, reason, n);
2477
+ try log.boost(.{ ident, reason, n });
2488
2478
  // Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
2489
2479
  if (showdown and move.effect == .Rage) return;
2490
2480
  },
2491
2481
  .DefenseUp1, .DefenseUp2 => {
2492
2482
  assert(boosts.def >= -6 and boosts.def <= 6);
2493
- if (boosts.def == 6) return try log.fail(ident, .None);
2483
+ if (boosts.def == 6) return log.fail(.{ ident, .None });
2494
2484
  const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
2495
2485
  boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
2496
2486
  if (stats.def == MAX_STAT_VALUE) {
2497
2487
  boosts.def -= 1;
2498
2488
  if (showdown) {
2499
- try log.boost(ident, .Defense, n);
2500
- try log.boost(ident, .Defense, -1);
2489
+ try log.boost(.{ ident, .Defense, n });
2490
+ try log.boost(.{ ident, .Defense, -1 });
2501
2491
  }
2502
- return try log.fail(ident, .None);
2492
+ return log.fail(.{ ident, .None });
2503
2493
  }
2504
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2494
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2505
2495
  const stat = unmodifiedStats(battle, side).def;
2506
2496
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2507
- try log.boost(ident, .Defense, n);
2497
+ try log.boost(.{ ident, .Defense, n });
2508
2498
  },
2509
2499
  .SpeedUp2 => {
2510
2500
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2511
- if (boosts.spe == 6) return try log.fail(ident, .None);
2501
+ if (boosts.spe == 6) return log.fail(.{ ident, .None });
2512
2502
  boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
2513
2503
  if (stats.spe == MAX_STAT_VALUE) {
2514
2504
  boosts.spe -= 1;
2515
2505
  if (showdown) {
2516
- try log.boost(ident, .Speed, 2);
2517
- try log.boost(ident, .Speed, -1);
2506
+ try log.boost(.{ ident, .Speed, 2 });
2507
+ try log.boost(.{ ident, .Speed, -1 });
2518
2508
  }
2519
- return try log.fail(ident, .None);
2509
+ return log.fail(.{ ident, .None });
2520
2510
  }
2521
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2511
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2522
2512
  const stat = unmodifiedStats(battle, side).spe;
2523
2513
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2524
- try log.boost(ident, .Speed, 2);
2514
+ try log.boost(.{ ident, .Speed, 2 });
2525
2515
  },
2526
2516
  .SpecialUp1, .SpecialUp2 => {
2527
2517
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2528
- if (boosts.spc == 6) return try log.fail(ident, .None);
2518
+ if (boosts.spc == 6) return log.fail(.{ ident, .None });
2529
2519
  const n: u2 = if (move.effect == .SpecialUp2) 2 else 1;
2530
2520
  boosts.spc = @intCast(@min(6, @as(i8, boosts.spc) + n));
2531
2521
  if (stats.spc == MAX_STAT_VALUE) {
2532
2522
  boosts.spc -= 1;
2533
2523
  if (showdown) {
2534
- try log.boost(ident, .SpecialAttack, n);
2535
- try log.boost(ident, .SpecialAttack, -1);
2536
- try log.boost(ident, .SpecialDefense, n);
2537
- 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 });
2538
2528
  }
2539
- return try log.fail(ident, .None);
2529
+ return log.fail(.{ ident, .None });
2540
2530
  }
2541
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2531
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2542
2532
  const stat = unmodifiedStats(battle, side).spc;
2543
2533
  stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2544
- try log.boost(ident, .SpecialAttack, n);
2545
- try log.boost(ident, .SpecialDefense, n);
2534
+ try log.boost(.{ ident, .SpecialAttack, n });
2535
+ try log.boost(.{ ident, .SpecialDefense, n });
2546
2536
  },
2547
2537
  .EvasionUp1 => {
2548
2538
  assert(boosts.evasion >= -6 and boosts.evasion <= 6);
2549
- if (boosts.evasion == 6) return try log.fail(ident, .None);
2539
+ if (boosts.evasion == 6) return log.fail(.{ ident, .None });
2550
2540
  boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
2551
- try log.boost(ident, .Evasion, 1);
2541
+ try log.boost(.{ ident, .Evasion, 1 });
2552
2542
  },
2553
2543
  else => unreachable,
2554
2544
  }
@@ -2563,7 +2553,7 @@ pub const Effects = struct {
2563
2553
  const foe_ident = battle.active(player.foe());
2564
2554
  const secondary = move.effect.isStatDownChance();
2565
2555
 
2566
- 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 });
2567
2557
 
2568
2558
  if (secondary) {
2569
2559
  const proc = try Rolls.unboost(battle, player, options);
@@ -2571,7 +2561,6 @@ pub const Effects = struct {
2571
2561
  } else {
2572
2562
  // checkHit already checks for Invulnerable
2573
2563
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2574
- try options.chance.commit(player, .hit);
2575
2564
  }
2576
2565
 
2577
2566
  var stats = &foe.active.stats;
@@ -2581,82 +2570,82 @@ pub const Effects = struct {
2581
2570
  switch (move.effect) {
2582
2571
  .AttackDown1, .AttackDownChance => {
2583
2572
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2584
- 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 });
2585
2574
  boosts.atk = @intCast(@max(-6, @as(i8, boosts.atk) - 1));
2586
2575
  if (stats.atk == 1) {
2587
2576
  boosts.atk += 1;
2588
2577
  if (showdown) {
2589
- try log.boost(foe_ident, .Attack, -1);
2590
- try log.boost(foe_ident, .Attack, 1);
2578
+ try log.boost(.{ foe_ident, .Attack, -1 });
2579
+ try log.boost(.{ foe_ident, .Attack, 1 });
2591
2580
  }
2592
- return try log.fail(foe_ident, .None);
2581
+ return log.fail(.{ foe_ident, .None });
2593
2582
  }
2594
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2583
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2595
2584
  const stat = unmodifiedStats(battle, foe).atk;
2596
2585
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2597
- try log.boost(foe_ident, .Attack, -1);
2586
+ try log.boost(.{ foe_ident, .Attack, -1 });
2598
2587
  },
2599
2588
  .DefenseDown1, .DefenseDown2, .DefenseDownChance => {
2600
2589
  assert(boosts.def >= -6 and boosts.def <= 6);
2601
- 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 });
2602
2591
  const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
2603
2592
  boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
2604
2593
  if (stats.def == 1) {
2605
2594
  boosts.def += 1;
2606
2595
  if (showdown) {
2607
- try log.boost(foe_ident, .Defense, -@as(i8, n));
2608
- 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 });
2609
2598
  }
2610
- return try log.fail(foe_ident, .None);
2599
+ return log.fail(.{ foe_ident, .None });
2611
2600
  }
2612
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2601
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2613
2602
  const stat = unmodifiedStats(battle, foe).def;
2614
2603
  stats.def = @max(1, stat * mod[0] / mod[1]);
2615
- try log.boost(foe_ident, .Defense, -@as(i8, n));
2604
+ try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
2616
2605
  },
2617
2606
  .SpeedDown1, .SpeedDownChance => {
2618
2607
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2619
- 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 });
2620
2609
  boosts.spe = @intCast(@max(-6, @as(i8, boosts.spe) - 1));
2621
2610
  if (stats.spe == 1) {
2622
2611
  boosts.spe += 1;
2623
2612
  if (showdown) {
2624
- try log.boost(foe_ident, .Speed, -1);
2625
- try log.boost(foe_ident, .Speed, 1);
2613
+ try log.boost(.{ foe_ident, .Speed, -1 });
2614
+ try log.boost(.{ foe_ident, .Speed, 1 });
2626
2615
  }
2627
- return try log.fail(foe_ident, .None);
2616
+ return log.fail(.{ foe_ident, .None });
2628
2617
  }
2629
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2618
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2630
2619
  const stat = unmodifiedStats(battle, foe).spe;
2631
2620
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2632
- try log.boost(foe_ident, .Speed, -1);
2621
+ try log.boost(.{ foe_ident, .Speed, -1 });
2633
2622
  assert(boosts.spe >= -6);
2634
2623
  },
2635
2624
  .SpecialDownChance => {
2636
2625
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2637
- 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 });
2638
2627
  boosts.spc = @intCast(@max(-6, @as(i8, boosts.spc) - 1));
2639
2628
  if (stats.spc == 1) {
2640
2629
  boosts.spc += 1;
2641
2630
  if (showdown) {
2642
- try log.boost(foe_ident, .SpecialAttack, -1);
2643
- try log.boost(foe_ident, .SpecialAttack, 1);
2644
- try log.boost(foe_ident, .SpecialDefense, -1);
2645
- 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 });
2646
2635
  }
2647
- return try log.fail(foe_ident, .None);
2636
+ return log.fail(.{ foe_ident, .None });
2648
2637
  }
2649
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2638
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2650
2639
  const stat = unmodifiedStats(battle, foe).spc;
2651
2640
  stats.spc = @max(1, stat * mod[0] / mod[1]);
2652
- try log.boost(foe_ident, .SpecialAttack, -1);
2653
- try log.boost(foe_ident, .SpecialDefense, -1);
2641
+ try log.boost(.{ foe_ident, .SpecialAttack, -1 });
2642
+ try log.boost(.{ foe_ident, .SpecialDefense, -1 });
2654
2643
  },
2655
2644
  .AccuracyDown1 => {
2656
2645
  assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
2657
- 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 });
2658
2647
  boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
2659
- try log.boost(foe_ident, .Accuracy, -1);
2648
+ try log.boost(.{ foe_ident, .Accuracy, -1 });
2660
2649
  },
2661
2650
  else => unreachable,
2662
2651
  }
@@ -2680,40 +2669,40 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
2680
2669
  }
2681
2670
  }
2682
2671
 
2683
- inline fn isForced(active: anytype) bool {
2672
+ fn isForced(active: anytype) bool {
2684
2673
  return active.volatiles.Recharging or active.volatiles.Rage or
2685
2674
  active.volatiles.Thrashing or active.volatiles.Charging;
2686
2675
  }
2687
2676
 
2688
- fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !void {
2677
+ fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
2689
2678
  var log = options.log;
2690
2679
  var side = battle.side(who);
2691
2680
  var volatiles = &side.active.volatiles;
2692
2681
  const ident = battle.active(who);
2693
2682
 
2694
- options.chance.clearDurations(who, clear);
2695
-
2696
- if (volatiles.disabled_move != 0) {
2697
- volatiles.disabled_move = 0;
2698
- volatiles.disabled_duration = 0;
2699
- 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 });
2700
2688
  }
2701
2689
  if (volatiles.Confusion) {
2702
2690
  // volatiles.confusion is left unchanged
2703
2691
  volatiles.Confusion = false;
2704
- try log.end(ident, .ConfusionSilent);
2692
+ options.chance.observe(.confusion, who, .None);
2693
+ try log.end(.{ ident, .ConfusionSilent });
2705
2694
  }
2706
2695
  if (volatiles.Mist) {
2707
2696
  volatiles.Mist = false;
2708
- try log.end(ident, .Mist);
2697
+ try log.end(.{ ident, .MistSilent });
2709
2698
  }
2710
2699
  if (volatiles.FocusEnergy) {
2711
2700
  volatiles.FocusEnergy = false;
2712
- try log.end(ident, .FocusEnergy);
2701
+ try log.end(.{ ident, .FocusEnergySilent });
2713
2702
  }
2714
2703
  if (volatiles.LeechSeed) {
2715
2704
  volatiles.LeechSeed = false;
2716
- try log.end(ident, .LeechSeed);
2705
+ try log.end(.{ ident, .LeechSeedSilent });
2717
2706
  }
2718
2707
  if (!showdown and volatiles.Toxic) {
2719
2708
  volatiles.Toxic = false;
@@ -2721,24 +2710,39 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2721
2710
  }
2722
2711
  if (volatiles.LightScreen) {
2723
2712
  volatiles.LightScreen = false;
2724
- try log.end(ident, .LightScreen);
2713
+ try log.end(.{ ident, .LightScreenSilent });
2725
2714
  }
2726
2715
  if (volatiles.Reflect) {
2727
2716
  volatiles.Reflect = false;
2728
- try log.end(ident, .Reflect);
2717
+ try log.end(.{ ident, .ReflectSilent });
2729
2718
  }
2730
2719
  if (showdown and volatiles.Toxic) {
2731
2720
  volatiles.Toxic = false;
2732
2721
  // Pokémon Showdown erroneously clears the toxic counter
2733
2722
  volatiles.toxic = 0;
2734
- try log.end(ident, .Toxic);
2723
+ try log.end(.{ ident, .ToxicSilent });
2735
2724
  }
2736
2725
  }
2737
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
+
2738
2740
  pub const Rolls = struct {
2739
- inline fn speedTie(battle: anytype, options: anytype) !bool {
2741
+ fn speedTie(battle: anytype, options: anytype) !bool {
2740
2742
  const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
2741
2743
  player == .P1
2744
+ else if (@hasDecl(@TypeOf(battle.rng), "speedTie"))
2745
+ battle.rng.speedTie()
2742
2746
  else if (showdown)
2743
2747
  battle.rng.range(u8, 0, 2) == 0
2744
2748
  else
@@ -2748,21 +2752,31 @@ pub const Rolls = struct {
2748
2752
  return p1;
2749
2753
  }
2750
2754
 
2751
- 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 {
2752
2762
  const crit = if (options.calc.overridden(player, .critical_hit)) |val|
2753
2763
  val == .true
2764
+ else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
2765
+ battle.rng.criticalHit(player, rate)
2754
2766
  else if (showdown)
2755
2767
  battle.rng.chance(u8, rate, 256)
2756
2768
  else
2757
2769
  std.math.rotl(u8, battle.rng.next(), 3) < rate;
2758
2770
 
2759
- try options.chance.criticalHit(player, crit, rate);
2771
+ if (!spurious) try options.chance.criticalHit(player, crit, rate);
2760
2772
  return crit;
2761
2773
  }
2762
2774
 
2763
- inline fn damage(battle: anytype, player: anytype, options: anytype) !u8 {
2775
+ fn damage(battle: anytype, player: Player, options: anytype) !u8 {
2764
2776
  const roll = if (options.calc.overridden(player, .damage)) |val|
2765
2777
  val
2778
+ else if (@hasDecl(@TypeOf(battle.rng), "damage"))
2779
+ battle.rng.damage(player)
2766
2780
  else roll: {
2767
2781
  if (showdown) break :roll battle.rng.range(u8, 217, 256);
2768
2782
  while (true) {
@@ -2771,25 +2785,32 @@ pub const Rolls = struct {
2771
2785
  }
2772
2786
  };
2773
2787
 
2788
+ assert(roll >= 217 and roll <= 255);
2774
2789
  try options.chance.damage(player, roll);
2775
2790
  return roll;
2776
2791
  }
2777
2792
 
2778
- 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;
2779
2796
  const ok = if (options.calc.overridden(player, .hit)) |val|
2780
2797
  val == .true
2798
+ else if (@hasDecl(@TypeOf(battle.rng), "hit"))
2799
+ battle.rng.hit(player, acc)
2781
2800
  else if (showdown)
2782
- battle.rng.chance(u8, accuracy, 256)
2801
+ battle.rng.chance(uN, acc, 256)
2783
2802
  else
2784
- battle.rng.next() < accuracy;
2803
+ battle.rng.next() < acc;
2785
2804
 
2786
- options.chance.hit(ok, accuracy);
2805
+ try options.chance.hit(player, ok, acc);
2787
2806
  return ok;
2788
2807
  }
2789
2808
 
2790
- inline fn confused(battle: anytype, player: Player, options: anytype) !bool {
2809
+ fn confused(battle: anytype, player: Player, options: anytype) !bool {
2791
2810
  const cfz = if (options.calc.overridden(player, .confused)) |val|
2792
2811
  val == .true
2812
+ else if (@hasDecl(@TypeOf(battle.rng), "confused"))
2813
+ battle.rng.confused(player)
2793
2814
  else if (showdown)
2794
2815
  !battle.rng.chance(u8, 128, 256)
2795
2816
  else
@@ -2799,9 +2820,11 @@ pub const Rolls = struct {
2799
2820
  return cfz;
2800
2821
  }
2801
2822
 
2802
- inline fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2823
+ fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2803
2824
  const par = if (options.calc.overridden(player, .paralyzed)) |val|
2804
2825
  val == .true
2826
+ else if (@hasDecl(@TypeOf(battle.rng), "paralyzed"))
2827
+ battle.rng.paralyzed(player)
2805
2828
  else if (showdown)
2806
2829
  battle.rng.chance(u8, 63, 256)
2807
2830
  else
@@ -2811,9 +2834,11 @@ pub const Rolls = struct {
2811
2834
  return par;
2812
2835
  }
2813
2836
 
2814
- inline fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2837
+ fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2815
2838
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2816
2839
  val == .true
2840
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2841
+ battle.rng.secondaryChance(player, 25)
2817
2842
  else if (showdown)
2818
2843
  battle.rng.chance(u8, 25, 256)
2819
2844
  else
@@ -2823,11 +2848,13 @@ pub const Rolls = struct {
2823
2848
  return proc;
2824
2849
  }
2825
2850
 
2826
- inline fn secondaryChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2851
+ fn secondaryChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2827
2852
  const rate: u8 = if (low) 26 else 77;
2828
2853
 
2829
2854
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2830
2855
  val == .true
2856
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2857
+ battle.rng.secondaryChance(player, rate)
2831
2858
  else if (showdown)
2832
2859
  battle.rng.chance(u8, rate, 256)
2833
2860
  else
@@ -2837,11 +2864,13 @@ pub const Rolls = struct {
2837
2864
  return proc;
2838
2865
  }
2839
2866
 
2840
- inline fn poisonChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2867
+ fn poisonChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2841
2868
  const rate: u8 = if (low) 52 else 103;
2842
2869
 
2843
2870
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2844
2871
  val == .true
2872
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2873
+ battle.rng.secondaryChance(player, rate)
2845
2874
  else if (showdown)
2846
2875
  battle.rng.chance(u8, rate, 256)
2847
2876
  else
@@ -2851,9 +2880,11 @@ pub const Rolls = struct {
2851
2880
  return proc;
2852
2881
  }
2853
2882
 
2854
- inline fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2883
+ fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2855
2884
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2856
2885
  val == .true
2886
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2887
+ battle.rng.secondaryChance(player, 85)
2857
2888
  else if (showdown)
2858
2889
  battle.rng.chance(u8, 85, 256)
2859
2890
  else
@@ -2863,14 +2894,14 @@ pub const Rolls = struct {
2863
2894
  return proc;
2864
2895
  }
2865
2896
 
2866
- inline fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2897
+ fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2867
2898
  const move: Move = if (options.calc.overridden(player, .metronome)) |val|
2868
2899
  val
2869
- else if (showdown) move: {
2870
- const r = battle.rng.range(u8, 0, @intFromEnum(Move.Struggle) - 2);
2871
- const mod = @as(u2, (if (r < @intFromEnum(Move.Metronome) - 1) 1 else 2));
2872
- break :move @enumFromInt(r + mod);
2873
- } 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: {
2874
2905
  while (true) {
2875
2906
  const r = battle.rng.next();
2876
2907
  if (r == 0 or r == @intFromEnum(Move.Metronome)) continue;
@@ -2883,9 +2914,11 @@ pub const Rolls = struct {
2883
2914
  return move;
2884
2915
  }
2885
2916
 
2886
- inline fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2917
+ fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2887
2918
  const power = if (options.calc.overridden(player, .psywave)) |val|
2888
2919
  val - 1
2920
+ else if (@hasDecl(@TypeOf(battle.rng), "psywave"))
2921
+ battle.rng.psywave(player)
2889
2922
  else if (showdown)
2890
2923
  battle.rng.range(u8, 0, max)
2891
2924
  else power: {
@@ -2895,15 +2928,18 @@ pub const Rolls = struct {
2895
2928
  }
2896
2929
  };
2897
2930
 
2931
+ assert(power < max);
2898
2932
  try options.chance.psywave(player, power, max);
2899
2933
  return power;
2900
2934
  }
2901
2935
 
2902
- inline fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2936
+ fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2903
2937
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2904
2938
  @intCast(val)
2939
+ else if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
2940
+ battle.rng.sleepDuration(player)
2905
2941
  else if (showdown)
2906
- @intCast(battle.rng.range(u8, 1, 8))
2942
+ battle.rng.range(u3, 1, 8)
2907
2943
  else duration: {
2908
2944
  while (true) {
2909
2945
  const r = battle.rng.next() & 7;
@@ -2911,47 +2947,59 @@ pub const Rolls = struct {
2911
2947
  }
2912
2948
  };
2913
2949
 
2914
- options.chance.duration(.sleep, player, player.foe(), duration);
2950
+ assert(duration >= 1 and duration <= 7);
2951
+ options.chance.duration(player, duration);
2915
2952
  return duration;
2916
2953
  }
2917
2954
 
2918
- inline fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2955
+ fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2919
2956
  const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
2920
2957
  val
2958
+ else if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
2959
+ battle.rng.disableDuration(player)
2921
2960
  else if (showdown)
2922
- @intCast(battle.rng.range(u8, 1, 9))
2961
+ battle.rng.range(u4, 1, 9)
2923
2962
  else
2924
2963
  @intCast((battle.rng.next() & 7) + 1);
2925
2964
 
2926
- options.chance.duration(.disable, player, player.foe(), duration);
2965
+ assert(duration >= 1 and duration <= 8);
2966
+ options.chance.duration(player, duration);
2927
2967
  return duration;
2928
2968
  }
2929
2969
 
2930
- inline fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
2970
+ fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
2931
2971
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2932
2972
  @intCast(val)
2973
+ else if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
2974
+ battle.rng.confusionDuration(player)
2933
2975
  else if (showdown)
2934
- @intCast(battle.rng.range(u8, 2, 6))
2976
+ battle.rng.range(u3, 2, 6)
2935
2977
  else
2936
2978
  @intCast((battle.rng.next() & 3) + 2);
2937
2979
 
2938
- 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);
2939
2982
  return duration;
2940
2983
  }
2941
2984
 
2942
- inline fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2985
+ fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2943
2986
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2944
2987
  @intCast(val)
2988
+ else if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
2989
+ battle.rng.attackingDuration(player)
2945
2990
  else if (showdown)
2946
- @intCast(battle.rng.range(u4, 2, 4))
2991
+ battle.rng.range(u3, 2, 4)
2947
2992
  else
2948
2993
  @intCast((battle.rng.next() & 1) + 2);
2949
2994
 
2950
- options.chance.duration(.attacking, player, player, duration);
2995
+ assert(duration >= 2 and duration <= 3);
2996
+ options.chance.duration(player, duration);
2951
2997
  return duration;
2952
2998
  }
2953
2999
 
2954
- inline fn distribution(
3000
+ const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
3001
+
3002
+ fn distribution(
2955
3003
  battle: anytype,
2956
3004
  comptime effect: Move.Effect,
2957
3005
  player: Player,
@@ -2960,15 +3008,18 @@ pub const Rolls = struct {
2960
3008
  const roll = if (effect == .Binding) .duration else .multi_hit;
2961
3009
  const n: u3 = if (options.calc.overridden(player, roll)) |val|
2962
3010
  @intCast(val)
3011
+ else if (@hasDecl(@TypeOf(battle.rng), "distribution"))
3012
+ battle.rng.distribution(player)
2963
3013
  else if (showdown)
2964
- DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)]
3014
+ DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
2965
3015
  else n: {
2966
3016
  const r = (battle.rng.next() & 3);
2967
3017
  break :n @intCast((if (r < 2) r else battle.rng.next() & 3) + 2);
2968
3018
  };
2969
3019
 
3020
+ assert(n >= 2 and n <= 5);
2970
3021
  if (effect == .Binding) {
2971
- options.chance.duration(.binding, player, player, n);
3022
+ options.chance.duration(player, n);
2972
3023
  } else {
2973
3024
  try options.chance.multiHit(player, n);
2974
3025
  }
@@ -2991,7 +3042,9 @@ pub const Rolls = struct {
2991
3042
  else
2992
3043
  null;
2993
3044
 
2994
- 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: {
2995
3048
  if (showdown) {
2996
3049
  if (check_pp == 0) {
2997
3050
  var i: usize = moves.len;
@@ -3007,10 +3060,9 @@ pub const Rolls = struct {
3007
3060
  while (i < moves.len and r > 0) : (i += 1) {
3008
3061
  if (moves[i].pp > 0) {
3009
3062
  r -= 1;
3010
- if (r == 0) break;
3063
+ if (r == 0) break :slot @intCast(i + 1);
3011
3064
  }
3012
3065
  }
3013
- break :slot @intCast(i + 1);
3014
3066
  }
3015
3067
  }
3016
3068
 
@@ -3032,11 +3084,11 @@ test "RNG agreement" {
3032
3084
  expected[i] = @intCast(i * 0x1000000);
3033
3085
  }
3034
3086
 
3035
- var spe = rng.FixedRNG(1, expected.len){ .rolls = expected };
3036
- var cfz = rng.FixedRNG(1, expected.len){ .rolls = expected };
3037
- var par = rng.FixedRNG(1, expected.len){ .rolls = expected };
3038
- var brn = rng.FixedRNG(1, expected.len){ .rolls = expected };
3039
- 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 };
3040
3092
 
3041
3093
  for (0..expected.len) |i| {
3042
3094
  try expectEqual(spe.range(u8, 0, 2) == 0, i < Gen12.percent(50) + 1);
@@ -3072,11 +3124,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3072
3124
  const side = battle.side(player);
3073
3125
  const foe = battle.foe(player);
3074
3126
 
3075
- var active = &side.active;
3127
+ const active = &side.active;
3076
3128
  const stored = side.stored();
3077
3129
 
3078
3130
  // While players are not given any input options on the cartridge in these cases,
3079
- // 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
3080
3132
  //
3081
3133
  // Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
3082
3134
  // here when not in compatibility mode, but the engine needs to be able to differentiate
@@ -3084,7 +3136,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3084
3136
  // passing due to being forced into using a move. Instead of introducing another option
3085
3137
  // we simply repurpose Move with no move slot, even though pedantically this is not
3086
3138
  // strictly correct as the player would not have been presented the option to move or
3087
- // switch at all.
3139
+ // switch at all
3088
3140
  if (isForced(active)) {
3089
3141
  out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
3090
3142
  n += 1;
@@ -3103,7 +3155,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3103
3155
  // On the cartridge, all of these happen after "FIGHT" (indicating you are not
3104
3156
  // switching) but before you are allowed to select a move. Pokémon Showdown instead
3105
3157
  // either disables all other moves in the case of limited or requires you to select a
3106
- // move normally if sleeping/frozen/bound.
3158
+ // move normally if sleeping/frozen/bound
3107
3159
  if (!showdown and (limited or foe.active.volatiles.Binding or
3108
3160
  Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
3109
3161
  {
@@ -3116,7 +3168,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3116
3168
  // Pokémon Showdown handles Bide and Binding moves by checking if the move in question
3117
3169
  // is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
3118
3170
  // Move will not result in forcing the subsequent use unless the user also had the
3119
- // proc-ed move in their moveset) and disabling all other moves.
3171
+ // proc-ed move in their moveset) and disabling all other moves
3120
3172
  if (limited) {
3121
3173
  assert(showdown);
3122
3174
  assert(side.last_selected_move != .None);
@@ -3126,7 +3178,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3126
3178
  if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
3127
3179
  // Pokémon Showdown displays Struggle if limited to Bide but unable to pick
3128
3180
  const struggle =
3129
- 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);
3130
3182
  const s = if (struggle) 0 else slot;
3131
3183
  out[n] = .{ .type = .Move, .data = s };
3132
3184
  n += 1;
@@ -3141,7 +3193,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3141
3193
  const m = active.moves[slot - 1];
3142
3194
  if (m.id == .None) break;
3143
3195
  if (m.pp == 0) continue;
3144
- if (active.volatiles.disabled_move == slot) continue;
3196
+ if (active.volatiles.disable_move == slot) continue;
3145
3197
  out[n] = .{ .type = .Move, .data = slot };
3146
3198
  n += 1;
3147
3199
  }