@pkmn/engine 0.1.0-dev.c5fe6d07 → 0.1.0-dev.cbdbb719

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/LICENSE +1 -3
  2. package/README.md +46 -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 +3 -2
  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 +232 -99
  21. package/package.json +46 -23
  22. package/src/bin/install-pkmn-engine +25 -17
  23. package/src/lib/common/array.zig +77 -0
  24. package/src/lib/common/data.zig +12 -24
  25. package/src/lib/common/debug.zig +2 -2
  26. package/src/lib/common/js.zig +4 -2
  27. package/src/lib/common/optional.zig +21 -10
  28. package/src/lib/common/options.zig +2 -0
  29. package/src/lib/common/protocol.zig +807 -327
  30. package/src/lib/common/rational.zig +125 -31
  31. package/src/lib/common/rng.zig +22 -13
  32. package/src/lib/common/util.zig +50 -1
  33. package/src/lib/gen1/calc.zig +514 -135
  34. package/src/lib/gen1/chance.zig +407 -284
  35. package/src/lib/gen1/data/moves.zig +192 -180
  36. package/src/lib/gen1/data/species.zig +3 -3
  37. package/src/lib/gen1/data/types.zig +7 -7
  38. package/src/lib/gen1/data.zig +46 -57
  39. package/src/lib/gen1/helpers.zig +95 -550
  40. package/src/lib/gen1/mechanics.zig +571 -516
  41. package/src/lib/gen2/calc.zig +161 -0
  42. package/src/lib/gen2/chance.zig +907 -0
  43. package/src/lib/gen2/data/items.zig +12 -18
  44. package/src/lib/gen2/data/moves.zig +663 -569
  45. package/src/lib/gen2/data/species.zig +1 -1
  46. package/src/lib/gen2/data/types.zig +134 -31
  47. package/src/lib/gen2/data.zig +107 -79
  48. package/src/lib/gen2/generated.zig +806 -0
  49. package/src/lib/gen2/helpers.zig +49 -21
  50. package/src/lib/gen2/mechanics.zig +3563 -9
  51. package/src/lib/{binding/node.zig → node.zig} +10 -13
  52. package/src/lib/pkmn.zig +32 -15
  53. package/src/lib/{binding/wasm.zig → wasm.zig} +2 -1
  54. package/src/test/display/pkmn.html.tmpl +5 -5
  55. package/src/lib/gen1/README.md +0 -892
  56. 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 {
@@ -505,36 +486,41 @@ fn beforeMove(
505
486
 
506
487
  if (Status.is(stored.status, .SLP)) {
507
488
  const before = stored.status;
489
+ const slf = Status.is(stored.status, .EXT);
508
490
  // 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;
491
+ if (options.calc.overridden(player, .sleep)) |obs| switch (obs) {
492
+ .started, .ended => stored.status = 0,
493
+ .continuing => if (Status.duration(stored.status) > 1) {
494
+ stored.status -= 1;
495
+ },
496
+ else => unreachable,
511
497
  } else {
512
498
  stored.status -= 1;
513
499
  }
514
500
 
515
501
  const duration = Status.duration(stored.status);
516
- if (!Status.is(stored.status, .EXT)) {
517
- try options.chance.sleep(player, duration);
518
- }
502
+ try options.chance.sleep(player, if (slf)
503
+ .None
504
+ else if (duration == 0) .ended else .continuing);
519
505
 
520
506
  if (duration == 0) {
521
- try log.curestatus(ident, before, .Message);
507
+ try log.curestatus(.{ ident, before, .Message });
522
508
  stored.status = 0; // clears EXT if present
523
509
  } else {
524
- try log.cant(ident, .Sleep);
510
+ try log.cant(.{ ident, .Sleep });
525
511
  }
526
512
  side.last_used_move = .None;
527
513
  return .done;
528
514
  }
529
515
 
530
516
  if (Status.is(stored.status, .FRZ)) {
531
- try log.cant(ident, .Freeze);
517
+ try log.cant(.{ ident, .Freeze });
532
518
  side.last_used_move = .None;
533
519
  return .done;
534
520
  }
535
521
 
536
522
  if (skip or foe.active.volatiles.Binding) {
537
- try log.cant(ident, .Bound);
523
+ try log.cant(.{ ident, .Bound });
538
524
  return .done;
539
525
  }
540
526
 
@@ -542,27 +528,27 @@ fn beforeMove(
542
528
  // Pokémon Showdown doesn't clear Flinch until its imaginary "residual" phase, meaning
543
529
  // Pokémon can sometimes flinch multiple times from the same original hit
544
530
  if (!showdown) volatiles.Flinch = false;
545
- try log.cant(ident, .Flinch);
531
+ try log.cant(.{ ident, .Flinch });
546
532
  return .done;
547
533
  }
548
534
 
549
535
  if (volatiles.Recharging) {
550
536
  volatiles.Recharging = false;
551
- try log.cant(ident, .Recharge);
537
+ try log.cant(.{ ident, .Recharge });
552
538
  return .done;
553
539
  }
554
540
 
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);
541
+ if (volatiles.disable_duration > 0) {
542
+ volatiles.disable_duration =
543
+ decrement(.disable, player, options, volatiles.disable_duration);
544
+ try options.chance.disable(
545
+ player,
546
+ if (volatiles.disable_duration == 0) .ended else .continuing,
547
+ );
562
548
 
563
- if (volatiles.disabled_duration == 0) {
564
- volatiles.disabled_move = 0;
565
- try log.end(ident, .Disable);
549
+ if (volatiles.disable_duration == 0) {
550
+ volatiles.disable_move = 0;
551
+ try log.end(.{ ident, .Disable });
566
552
  }
567
553
  }
568
554
  // Pokémon Showdown's disable condition has a single onBeforeMove handler
@@ -577,19 +563,25 @@ fn beforeMove(
577
563
 
578
564
  if (volatiles.Confusion) {
579
565
  assert(volatiles.confusion > 0);
580
-
581
- if (options.calc.modify(player, .confusion)) |extend| {
582
- if (!extend) volatiles.confusion = 0;
566
+ if (options.calc.overridden(player, .confusion)) |obs| switch (obs) {
567
+ .started => volatiles.confusion -= 1,
568
+ .continuing => if (volatiles.confusion > 1) {
569
+ volatiles.confusion -= 1;
570
+ },
571
+ .ended => volatiles.confusion = 0,
572
+ .overwritten => {},
573
+ else => unreachable,
583
574
  } else {
584
575
  volatiles.confusion -= 1;
585
576
  }
586
- try options.chance.confusion(player, volatiles.confusion);
577
+
578
+ try options.chance.confusion(player, if (volatiles.confusion == 0) .ended else .continuing);
587
579
 
588
580
  if (volatiles.confusion == 0) {
589
581
  volatiles.Confusion = false;
590
- try log.end(ident, .Confusion);
582
+ try log.end(.{ ident, .Confusion });
591
583
  } else {
592
- try log.activate(ident, .Confusion);
584
+ try log.activate(.{ ident, .Confusion });
593
585
 
594
586
  if (try Rolls.confused(battle, player, options)) {
595
587
  assert(!volatiles.MultiHit);
@@ -601,7 +593,9 @@ fn beforeMove(
601
593
  volatiles.Charging = false;
602
594
  volatiles.Binding = false;
603
595
  volatiles.Invulnerable = false;
604
- options.chance.clearDurations(player, null);
596
+
597
+ options.chance.observe(.attacking, 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,12 @@ 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(.attacking, player, .None);
639
+ options.chance.observe(.binding, player, .None);
640
+
642
641
  // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
643
- try log.cant(ident, .Paralysis);
642
+ try log.cant(.{ ident, .Paralysis });
644
643
  return .done;
645
644
  }
646
645
 
@@ -656,28 +655,22 @@ fn beforeMove(
656
655
  volatiles.state +%= battle.last_damage;
657
656
  }
658
657
 
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);
658
+ volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
659
+ try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
667
660
 
668
661
  if (volatiles.attacks != 0) {
669
- try log.activate(ident, .Bide);
662
+ try log.activate(.{ ident, .Bide });
670
663
  return .done;
671
664
  }
672
665
 
673
666
  volatiles.Bide = false;
674
- try log.end(ident, .Bide);
667
+ try log.end(.{ ident, .Bide });
675
668
 
676
669
  battle.last_damage = volatiles.state *% 2;
677
670
  volatiles.state = 0;
678
671
 
679
672
  if (battle.last_damage == 0) {
680
- try log.fail(ident, .None);
673
+ try log.fail(.{ ident, .None });
681
674
  return .done;
682
675
  }
683
676
 
@@ -697,47 +690,38 @@ fn beforeMove(
697
690
  }
698
691
 
699
692
  if (volatiles.Thrashing) {
700
- assert(volatiles.attacks > 0);
701
- assert(from != null);
693
+ try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
702
694
 
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);
695
+ volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
696
+ try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
709
697
 
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
- }
698
+ if (volatiles.attacks == 0) {
699
+ const overwritten = volatiles.Confusion;
700
+
701
+ volatiles.Thrashing = false;
702
+ volatiles.Confusion = true;
703
+ volatiles.confusion = Rolls.confusionDuration(battle, player, options);
704
+
705
+ // On Pokémon Showdown this could leak information to the opponent that thrashing has
706
+ // ended (the engine's protocol is always from the omniscient perspective and clients
707
+ // can be expected to recognize this pattern and pass the information only to the
708
+ // correct player)
709
+ try log.start(.{ battle.active(player), .ConfusionSilent });
710
+ options.chance.observe(.confusion, player, if (overwritten) .overwritten else .started);
721
711
  }
712
+
713
+ // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
714
+ // used in useMove and doesn't have the notion of skipping canMove semantics
715
+ if (showdown) side.last_used_move = side.last_selected_move;
716
+
722
717
  return .skip_can;
723
718
  }
724
719
 
725
720
  if (volatiles.Binding) {
726
- assert(volatiles.attacks > 0);
727
-
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);
721
+ volatiles.attacks = decrement(.binding, player, options, volatiles.attacks);
722
+ try options.chance.binding(player, if (volatiles.attacks == 0) .ended else .continuing);
734
723
 
735
- try log.move(
736
- battle.active(player),
737
- side.last_selected_move,
738
- battle.active(player.foe()),
739
- side.last_selected_move,
740
- );
724
+ try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
741
725
  if (showdown or battle.last_damage != 0) {
742
726
  const sub = showdown and foe.active.volatiles.Substitute;
743
727
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -757,7 +741,7 @@ fn canMove(
757
741
  mslot: u4,
758
742
  auto: bool,
759
743
  skip_pp: bool,
760
- from: ?Move,
744
+ from: Move,
761
745
  residual: *bool,
762
746
  options: anytype,
763
747
  ) !bool {
@@ -765,33 +749,21 @@ fn canMove(
765
749
  const player_ident = battle.active(player);
766
750
  const move = Move.get(side.last_selected_move);
767
751
 
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
752
  if (side.active.volatiles.Charging) {
774
753
  side.active.volatiles.Charging = false;
775
754
  side.active.volatiles.Invulnerable = false;
776
755
  } else if (move.effect == .Charge) {
777
- try options.log.move(player_ident, side.last_selected_move, .{}, from);
756
+ try options.log.move(.{ player_ident, side.last_selected_move, ID{}, from });
778
757
  setCounterable(battle, player, side, move);
779
758
  try Effects.charge(battle, player, options);
780
759
  return false;
781
760
  }
782
761
 
783
- if (!showdown or !special) side.last_used_move = side.last_selected_move;
784
- if (!skip) decrementPP(side, mslot, auto);
762
+ side.last_used_move = side.last_selected_move;
763
+ if (!skip_pp) decrementPP(side, mslot, auto);
785
764
 
786
765
  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);
766
+ try options.log.move(.{ player_ident, side.last_selected_move, battle.active(target), from });
795
767
  setCounterable(battle, player, side, move);
796
768
 
797
769
  if (move.effect.onBegin()) {
@@ -829,16 +801,18 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
829
801
  assert(!volatiles.Rage and !volatiles.Thrashing and !volatiles.MultiHit);
830
802
  if (volatiles.Bide) return;
831
803
 
832
- assert(active.move(mslot).pp > 0 or auto);
833
- active.move(mslot).pp = @as(u6, @intCast(active.move(mslot).pp)) -% 1;
804
+ var move_slot = active.move(mslot);
805
+ assert(move_slot.pp > 0 or auto);
806
+ move_slot.pp = @as(u6, @intCast(move_slot.pp)) -% 1;
834
807
  if (volatiles.Transform) return;
835
808
 
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;
809
+ move_slot = side.stored().move(mslot);
810
+ assert(move_slot.pp > 0 or auto);
811
+ move_slot.pp = @as(u6, @intCast(move_slot.pp)) -% 1;
838
812
  assert(active.move(mslot).pp == side.stored().move(mslot).pp);
839
813
  }
840
814
 
841
- fn incrementPP(side: *Side, mslot: u4) void {
815
+ fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
842
816
  var active = &side.active;
843
817
  const volatiles = &active.volatiles;
844
818
 
@@ -846,8 +820,11 @@ fn incrementPP(side: *Side, mslot: u4) void {
846
820
  // GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
847
821
  if (showdown and volatiles.Transform) return;
848
822
 
823
+ const n = options.calc.overridden(player, .pp) orelse 1;
824
+ if (volatiles.Transform and !options.chance.pp(player, n)) return;
825
+
849
826
  assert(mslot > 0 and mslot <= 4);
850
- side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% 1;
827
+ side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% n;
851
828
  }
852
829
 
853
830
  // Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
@@ -895,13 +872,13 @@ fn doMove(
895
872
  break :miss true;
896
873
  }
897
874
  break :miss (move.target != .Self and
898
- !moveHit(battle, player, move, &immune, &mist, options));
875
+ !try moveHit(battle, player, move, &immune, &mist, options));
899
876
  };
900
877
  assert(!immune or miss or (showdown and move.effect == .Binding));
901
878
 
902
879
  var late = showdown and move.effect != .Explode;
903
880
  const skip = status or immune;
904
- if ((!showdown or (!skip or counter)) and !miss) blk: {
881
+ if ((!showdown or (!skip or counter)) and !miss) damage: {
905
882
  if (showdown and move.effect.isMulti()) {
906
883
  try Effects.multiHit(battle, player, move, options);
907
884
  hits = side.active.volatiles.attacks;
@@ -923,8 +900,9 @@ fn doMove(
923
900
  ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
924
901
  // This can overflow after adjustDamage, but will still be sufficient to OHKO
925
902
  battle.last_damage = if (ohko) 65535 else 0;
926
- if (showdown) break :blk; // skip adjustDamage / randomizeDamage
927
- } else if (!calcDamage(battle, player, player.foe(), move, crit)) {
903
+ if (showdown) break :damage; // skip adjustDamage / randomizeDamage
904
+ } else if (!calcDamage(battle, player, player.foe(), move, crit, options)) {
905
+ if (!showdown) try options.chance.commit(player, .err);
928
906
  return @as(?Result, Result.Error);
929
907
  }
930
908
  if (battle.last_damage == 0) {
@@ -938,11 +916,12 @@ fn doMove(
938
916
  }
939
917
  }
940
918
 
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);
919
+ var missed: bool = undefined;
920
+ const zero = battle.last_damage == 0;
921
+ if (!showdown and !skip) {
922
+ missed = !try moveHit(battle, player, move, &immune, &mist, options);
923
+ miss = missed or zero;
924
+ }
946
925
 
947
926
  assert(showdown or miss or battle.last_damage > 0 or skip);
948
927
  assert((!showdown and miss) or !(ohko and immune));
@@ -966,16 +945,37 @@ fn doMove(
966
945
  ohko = (!showdown or (!immune and !invulnerable)) and
967
946
  move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
968
947
  if (ohko) {
969
- try log.immune(foe_ident, .OHKO);
948
+ try log.immune(.{ foe_ident, .OHKO });
970
949
  } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
971
- try log.immune(foe_ident, .None);
950
+ try log.immune(.{ foe_ident, .None });
951
+ if (!showdown) {
952
+ if (move.effect == .Binding) {
953
+ // Rather obnoxiously, the cartridge sets the Binding volatile in canMove and
954
+ // then simply clears it later on if the move happens to miss in moveHit.
955
+ // However, moveHit can return true and we'll still end up in the outer miss
956
+ // handling block because attacking into an immune Pokemon does zero damage
957
+ // (considered missing)
958
+ assert(zero);
959
+ // Thus we still need to actually save the hit result (but not the crit roll).
960
+ // Using .miss for this commit would accomplish what we want, but is somewhat
961
+ // misleading as its entirely possible the move actually did hit (and the target
962
+ // is now trapped) so we use the .binding value instead which is effectively
963
+ // identical, just doesn't claim to know about the actual results of the hit
964
+ // roll)
965
+ try options.chance.commit(player, .binding);
966
+ } else {
967
+ // The .glitch option allows for conditionally committing if we are in a
968
+ // division-by-zero freeze alternative timeline scenario
969
+ try options.chance.commit(player, .glitch);
970
+ }
971
+ }
972
972
  } else if (mist) {
973
- if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
974
- try log.fail(foe_ident, .None);
973
+ if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
974
+ try log.fail(.{ foe_ident, .None });
975
975
  } else {
976
- if (showdown or !zero) try options.chance.commit(player, .miss);
977
- try log.lastmiss();
978
- try log.miss(battle.active(player));
976
+ if (!showdown) try options.chance.commit(player, if (zero and !missed) .hit else .miss);
977
+ try log.lastmiss(.{});
978
+ try log.miss(.{battle.active(player)});
979
979
  }
980
980
  if (move.effect == .JumpKick) {
981
981
  // Recoil is supposed to be damage/8 but damage will always be 0 here
@@ -988,17 +988,15 @@ fn doMove(
988
988
  try buildRage(battle, player.foe(), options);
989
989
  } else if (showdown and move.effect == .Disable) {
990
990
  try buildRage(battle, player.foe(), options);
991
- } else if (!showdown and immune and move.effect == .Binding) {
992
- try options.chance.commit(player, .binding);
993
991
  }
994
992
  return null;
995
- } else {
993
+ } else if (!showdown) {
996
994
  try options.chance.commit(player, .hit);
997
995
  }
998
996
 
999
997
  // On the cartridge MultiHit doesn't get set up until after damage has been applied for the
1000
998
  // first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
1001
- // it up above before damage calculation).
999
+ // it up above before damage calculation)
1002
1000
  if (!showdown and move.effect.isMulti()) {
1003
1001
  try Effects.multiHit(battle, player, move, options);
1004
1002
  hits = side.active.volatiles.attacks;
@@ -1011,15 +1009,15 @@ fn doMove(
1011
1009
  var hit: u4 = 0;
1012
1010
  while (hit < hits) {
1013
1011
  if (hit == 0) {
1014
- if (crit) try log.crit(battle.active(player.foe()));
1012
+ if (crit) try log.crit(.{battle.active(player.foe())});
1015
1013
  if (effectiveness > Effectiveness.neutral) {
1016
- try log.supereffective(battle.active(player.foe()));
1014
+ try log.supereffective(.{battle.active(player.foe())});
1017
1015
  } else if (effectiveness < Effectiveness.neutral) {
1018
- try log.resisted(battle.active(player.foe()));
1016
+ try log.resisted(.{battle.active(player.foe())});
1019
1017
  }
1020
1018
  }
1021
1019
  if (!skip) nullified = try applyDamage(battle, player.foe(), player.foe(), .None, options);
1022
- if (hit == 0 and ohko) try log.ohko();
1020
+ if (hit == 0 and ohko) try log.ohko(.{});
1023
1021
  hit += 1;
1024
1022
  if (foe.stored().hp == 0) break;
1025
1023
  if (!late and (!sub or move.effect == .Explode)) {
@@ -1036,7 +1034,7 @@ fn doMove(
1036
1034
  if (showdown and move.effect == .Twineedle and !nullified and foe.stored().hp > 0) {
1037
1035
  try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1038
1036
  }
1039
- try log.hitcount(battle.active(player.foe()), hit);
1037
+ try log.hitcount(.{ battle.active(player.foe()), hit });
1040
1038
  } else if (showdown) {
1041
1039
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
1042
1040
  if (move.effect == .Binding) {
@@ -1046,9 +1044,9 @@ fn doMove(
1046
1044
  assert(foe.stored().hp > 0);
1047
1045
  // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
1048
1046
  if (sub) {
1049
- try log.activate(battle.active(player.foe()), .Substitute);
1047
+ try log.activate(.{ battle.active(player.foe()), .Substitute });
1050
1048
  } else {
1051
- try log.damage(battle.active(player.foe()), foe.stored(), .None);
1049
+ try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
1052
1050
  try buildRage(battle, player.foe(), options);
1053
1051
  }
1054
1052
  return null;
@@ -1057,12 +1055,12 @@ fn doMove(
1057
1055
  }
1058
1056
 
1059
1057
  // 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.
1058
+ // if an effect was intended to "always happen" it will still get skipped
1061
1059
  if (nullified) return null;
1062
1060
 
1063
1061
  // On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
1064
1062
  // 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.
1063
+ // setup before the loop means we can avoid having to waste time doing no-op handler searches
1066
1064
  if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
1067
1065
 
1068
1066
  if (foe.stored().hp == 0) return null;
@@ -1077,7 +1075,7 @@ fn doMove(
1077
1075
  // below). For Twineedle we change the data to that of one with PoisonChance1 given its
1078
1076
  // MultiHit behavior is complete after the loop above, though Pokémon Showdown handles the
1079
1077
  // Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
1080
- // puts the |-status| message before |-hitcount| instead of after.
1078
+ // puts the |-status| message before |-hitcount| instead of after
1081
1079
  if (move.effect == .Twineedle) {
1082
1080
  if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1083
1081
  } else if (move.effect == .Disable) {
@@ -1096,12 +1094,14 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
1096
1094
  const side = battle.side(player);
1097
1095
 
1098
1096
  // Base speed is used for the critical hit calculation, even when Transform-ed
1099
- var rate = @as(u16, Species.chance(side.stored().species));
1097
+ var rate: u16 = Species.chance(side.stored().species);
1100
1098
  // GLITCH: Focus Energy reduces critical hit rate instead of increasing it
1101
1099
  rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
1102
1100
  rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
1103
1101
 
1104
- return Rolls.criticalHit(battle, player, @intCast(rate), options);
1102
+ const spurious =
1103
+ !showdown and ((move.bp == 0 and move.effect != .OHKO) or move.target == .Depends);
1104
+ return Rolls.criticalHit(battle, player, @intCast(rate), spurious, options);
1105
1105
  }
1106
1106
 
1107
1107
  fn calcDamage(
@@ -1110,6 +1110,7 @@ fn calcDamage(
1110
1110
  target_player: Player,
1111
1111
  m: ?Move.Data,
1112
1112
  crit: bool,
1113
+ options: anytype,
1113
1114
  ) bool {
1114
1115
  // Confusion (indicated when m == null) just needs a 40 BP physical move
1115
1116
  const cfz = m == null;
@@ -1121,28 +1122,8 @@ fn calcDamage(
1121
1122
 
1122
1123
  const special = move.type.special();
1123
1124
 
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
1125
+ var atk = attack(side, crit, special);
1126
+ var def = defense(target, crit, special, cfz);
1146
1127
 
1147
1128
  // Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
1148
1129
  // will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
@@ -1153,9 +1134,9 @@ fn calcDamage(
1153
1134
  def = @max((def / 4) & 255, if (showdown) 1 else 0);
1154
1135
  }
1155
1136
 
1156
- const lvl = @as(u32, side.stored().level * @as(u2, if (crit) 2 else 1));
1137
+ const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
1157
1138
 
1158
- def = @as(u32, if (move.effect == .Explode) @max(def / 2, 1) else def);
1139
+ def = if (move.effect == .Explode) @max(def / 2, 1) else def;
1159
1140
 
1160
1141
  if (def == 0) return false;
1161
1142
 
@@ -1167,11 +1148,41 @@ fn calcDamage(
1167
1148
  d = @min(997, d);
1168
1149
  d += 2;
1169
1150
 
1151
+ // Whether or not a critical hit occurred can change whether a division-by-zero freeze is
1152
+ // encountered so we need to make sure that in the event of a miss we commit the status of the
1153
+ // crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
1154
+ // that results in counter intuitive probabilities in the common case
1155
+ if (pkmn.options.chance and !showdown) {
1156
+ atk = attack(side, !crit, special);
1157
+ def = defense(target, !crit, special, false);
1158
+ if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
1159
+ }
1160
+
1170
1161
  battle.last_damage = @intCast(d);
1171
1162
 
1172
1163
  return true;
1173
1164
  }
1174
1165
 
1166
+ fn attack(side: *Side, crit: bool, special: bool) u32 {
1167
+ return if (crit)
1168
+ if (special) side.stored().stats.spc else side.stored().stats.atk
1169
+ else if (special) side.active.stats.spc else side.active.stats.atk;
1170
+ }
1171
+
1172
+ fn defense(target: *Side, crit: bool, special: bool, cfz: bool) u32 {
1173
+ return if (crit)
1174
+ if (special) target.stored().stats.spc else target.stored().stats.def
1175
+ else
1176
+ // GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
1177
+ if (special)
1178
+ target.active.stats.spc *
1179
+ @as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
1180
+ else
1181
+ // Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
1182
+ target.active.stats.def *
1183
+ @as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
1184
+ }
1185
+
1175
1186
  fn adjustDamage(battle: anytype, player: Player) u16 {
1176
1187
  const side = battle.side(player);
1177
1188
  const foe = battle.foe(player);
@@ -1216,7 +1227,6 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1216
1227
  const foe = battle.foe(player);
1217
1228
 
1218
1229
  if (!try checkHit(battle, player, move, options)) return null;
1219
- try options.chance.commit(player, .hit);
1220
1230
 
1221
1231
  battle.last_damage = switch (side.last_selected_move) {
1222
1232
  .SuperFang => @max(foe.stored().hp / 2, 1),
@@ -1246,7 +1256,7 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1246
1256
  const foe = battle.foe(player);
1247
1257
 
1248
1258
  if (battle.last_damage == 0) {
1249
- try options.log.fail(battle.active(player), .None);
1259
+ try options.log.fail(.{ battle.active(player), .None });
1250
1260
  return null;
1251
1261
  }
1252
1262
 
@@ -1264,22 +1274,21 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1264
1274
  foe_last_selected_move.type == .Fighting);
1265
1275
 
1266
1276
  if (!used and !selected) {
1267
- try options.log.fail(battle.active(player), .None);
1277
+ try options.log.fail(.{ battle.active(player), .None });
1268
1278
  return null;
1269
1279
  }
1270
1280
 
1271
1281
  if (!used or !selected) {
1272
1282
  // GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
1273
1283
  if (!showdown) return Result.Error;
1274
- try options.log.fail(battle.active(player), .None);
1284
+ try options.log.fail(.{ battle.active(player), .None });
1275
1285
  return null;
1276
1286
  }
1277
1287
 
1278
1288
  battle.last_damage *|= 2;
1279
1289
 
1280
- // Pokémon Showdown calls checkHit before Counter
1290
+ // Pokémon Showdown calls moveHit before Counter
1281
1291
  if (!showdown and !try checkHit(battle, player, move, options)) return null;
1282
- try options.chance.commit(player, .hit);
1283
1292
 
1284
1293
  const sub = showdown and foe.active.volatiles.Substitute;
1285
1294
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -1307,20 +1316,20 @@ fn applyDamage(
1307
1316
  subbed.active.volatiles.substitute = 0;
1308
1317
  subbed.active.volatiles.Substitute = false;
1309
1318
  // 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);
1319
+ try options.log.end(.{ battle.active(sub_player), .Substitute });
1320
+ options.calc.capped(sub_player);
1312
1321
  return true;
1313
1322
  } else {
1314
1323
  // Safe to truncate since less than subbed.volatiles.substitute which is a u8
1315
1324
  subbed.active.volatiles.substitute -= @intCast(battle.last_damage);
1316
- try options.log.activate(battle.active(sub_player), .Substitute);
1325
+ try options.log.activate(.{ battle.active(sub_player), .Substitute });
1317
1326
  return false;
1318
1327
  }
1319
1328
  }
1320
1329
 
1321
1330
  if (battle.last_damage > target.stored().hp) battle.last_damage = target.stored().hp;
1322
1331
  target.stored().hp -= battle.last_damage;
1323
- try options.log.damage(battle.active(target_player), target.stored(), reason);
1332
+ try options.log.damage(.{ battle.active(target_player), target.stored(), reason });
1324
1333
  return false;
1325
1334
  }
1326
1335
 
@@ -1339,10 +1348,10 @@ fn mirrorMove(
1339
1348
  side.last_selected_move = foe.last_used_move;
1340
1349
 
1341
1350
  if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
1342
- try options.log.fail(battle.active(player), .None);
1351
+ try options.log.fail(.{ battle.active(player), .None });
1343
1352
  return null;
1344
1353
  } else if (!showdown or foe.last_used_move != .Struggle) {
1345
- incrementPP(side, mslot);
1354
+ incrementPP(side, player, mslot, options);
1346
1355
  }
1347
1356
 
1348
1357
  if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
@@ -1363,7 +1372,7 @@ fn metronome(
1363
1372
  var side = battle.side(player);
1364
1373
 
1365
1374
  side.last_selected_move = try Rolls.metronome(battle, player, options);
1366
- incrementPP(side, mslot);
1375
+ incrementPP(side, player, mslot, options);
1367
1376
 
1368
1377
  if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
1369
1378
  return null;
@@ -1375,20 +1384,23 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
1375
1384
  var immune = false;
1376
1385
  var mist = false;
1377
1386
 
1378
- if (moveHit(battle, player, move, &immune, &mist, options)) return true;
1387
+ if (try moveHit(battle, player, move, &immune, &mist, options)) {
1388
+ if (!showdown) try options.chance.commit(player, .hit);
1389
+ return true;
1390
+ }
1379
1391
 
1380
1392
  assert(!immune);
1381
1393
  if (mist) {
1382
1394
  assert(!showdown);
1383
1395
  const foe_ident = battle.active(player.foe());
1384
- try options.log.activate(foe_ident, .Mist);
1385
- try options.log.fail(foe_ident, .None);
1396
+ try options.log.activate(.{ foe_ident, .Mist });
1397
+ try options.log.fail(.{ foe_ident, .None });
1386
1398
  } else {
1387
- try options.chance.commit(player, .miss);
1388
- try options.log.lastmiss();
1389
- try options.log.miss(battle.active(player));
1399
+ try options.log.lastmiss(.{});
1400
+ try options.log.miss(.{battle.active(player)});
1390
1401
  }
1391
1402
 
1403
+ if (!showdown) try options.chance.commit(player, .miss);
1392
1404
  return false;
1393
1405
  }
1394
1406
 
@@ -1399,7 +1411,7 @@ fn moveHit(
1399
1411
  immune: *bool,
1400
1412
  mist: *bool,
1401
1413
  options: anytype,
1402
- ) bool {
1414
+ ) !bool {
1403
1415
  var side = battle.side(player);
1404
1416
  const foe = battle.foe(player);
1405
1417
 
@@ -1438,7 +1450,7 @@ fn moveHit(
1438
1450
  const overwritten = overwrite and state > 0;
1439
1451
  assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
1440
1452
 
1441
- var accuracy = if (overwritten) state else @as(u16, Gen12.percent(move.accuracy));
1453
+ var accuracy: u16 = if (overwritten) state else move.accuracy;
1442
1454
  var boost = BOOSTS[@as(u4, @intCast(@as(i8, side.active.boosts.accuracy) + 6))];
1443
1455
  accuracy = accuracy * boost[0] / boost[1];
1444
1456
  boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
@@ -1450,7 +1462,7 @@ fn moveHit(
1450
1462
  if (overwrite) side.active.volatiles.state = accuracy;
1451
1463
 
1452
1464
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1453
- break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
1465
+ break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
1454
1466
  };
1455
1467
 
1456
1468
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1461,13 +1473,17 @@ fn moveHit(
1461
1473
 
1462
1474
  if (!miss) return true;
1463
1475
  if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
1476
+
1464
1477
  side.active.volatiles.Binding = false;
1478
+ options.chance.observe(.binding, player, .None);
1479
+
1465
1480
  return false;
1466
1481
  }
1467
1482
 
1468
1483
  fn checkFaint(
1469
1484
  battle: anytype,
1470
1485
  player: Player,
1486
+ cap: bool,
1471
1487
  options: anytype,
1472
1488
  ) @TypeOf(options.log).Error!?Result {
1473
1489
  const side = battle.side(player);
@@ -1477,33 +1493,32 @@ fn checkFaint(
1477
1493
  const foe_fainted = foe.stored().hp == 0;
1478
1494
 
1479
1495
  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;
1496
+ const foe_out = foe_fainted and findFirstAlive(foe) == 0;
1497
+ const more = player_out or foe_out;
1483
1498
 
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;
1499
+ if (try faint(battle, player, !(more or foe_fainted), cap, options)) |r| return r;
1500
+ if (foe_fainted) if (try faint(battle, player.foe(), !more, false, options)) |r| return r;
1486
1501
 
1487
1502
  assert(!side.active.volatiles.MultiHit);
1488
1503
  assert(!foe.active.volatiles.MultiHit);
1489
1504
 
1490
- if (tie) {
1491
- try options.log.tie();
1505
+ if (player_out and foe_out) {
1506
+ try options.log.tie(.{});
1492
1507
  return Result.Tie;
1493
1508
  } else if (player_out) {
1494
- try options.log.win(player.foe());
1509
+ try options.log.win(.{player.foe()});
1495
1510
  return if (player == .P1) Result.Lose else Result.Win;
1496
1511
  } else if (foe_out) {
1497
- try options.log.win(player);
1512
+ try options.log.win(.{player});
1498
1513
  return if (player == .P1) Result.Win else Result.Lose;
1499
1514
  }
1500
1515
 
1501
1516
  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 };
1517
+ if (player == .P1) return .{ .p1 = .Switch, .p2 = foe_choice };
1518
+ return .{ .p1 = foe_choice, .p2 = .Switch };
1504
1519
  }
1505
1520
 
1506
- fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
1521
+ fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytype) !?Result {
1507
1522
  var side = battle.side(player);
1508
1523
  var foe = battle.foe(player);
1509
1524
  assert(side.stored().hp == 0);
@@ -1536,8 +1551,10 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
1536
1551
  side.stored().stats.spe;
1537
1552
  }
1538
1553
 
1539
- try options.log.faint(battle.active(player), done);
1540
- options.calc.capped(player);
1554
+ try options.log.faint(.{ battle.active(player), done });
1555
+ // We can only safely set capped if the faint occured as the direct result of an opponent's
1556
+ // move (as opposed to due to residual damage, recoil, Explosion, etc)
1557
+ if (cap) options.calc.capped(player);
1541
1558
  return null;
1542
1559
  }
1543
1560
 
@@ -1548,21 +1565,19 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1548
1565
  var volatiles = &side.active.volatiles;
1549
1566
 
1550
1567
  const brn = Status.is(stored.status, .BRN);
1551
- if (brn or Status.is(stored.status, .PSN)) blk: {
1568
+ if (brn or Status.is(stored.status, .PSN)) {
1552
1569
  var damage = @max(stored.stats.hp / 16, 1);
1553
-
1554
1570
  if (volatiles.Toxic) {
1555
1571
  volatiles.toxic += 1;
1556
1572
  damage *= volatiles.toxic;
1557
1573
  }
1574
+ damage = @min(damage, stored.hp);
1558
1575
 
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);
1576
+ if (!showdown or damage > 0) {
1577
+ stored.hp -= damage;
1578
+ // Pokémon Showdown uses damage [of] here but its not relevant in Generation I
1579
+ try options.log.damage(.{ ident, stored, if (brn) Damage.Burn else Damage.Poison });
1580
+ }
1566
1581
  }
1567
1582
 
1568
1583
  if (volatiles.LeechSeed) {
@@ -1586,14 +1601,14 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1586
1601
  const amount = @min(damage, stored.hp);
1587
1602
  stored.hp -= amount;
1588
1603
 
1589
- // As above, Pokémon Showdown uses damageOf but its not relevant
1590
- if (amount > 0) try options.log.damage(ident, stored, .LeechSeed);
1604
+ // As above, Pokémon Showdown uses damage [of] but its not relevant
1605
+ if (amount > 0) try options.log.damage(.{ ident, stored, .LeechSeed });
1591
1606
 
1592
1607
  const before = foe_stored.hp;
1593
1608
  // Uncapped damage is added back to the foe
1594
1609
  foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
1595
1610
  // 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);
1611
+ if (foe_stored.hp > before) try options.log.heal(.{ foe_ident, foe_stored, .Silent });
1597
1612
  }
1598
1613
  }
1599
1614
 
@@ -1604,18 +1619,18 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
1604
1619
  battle.turn += 1;
1605
1620
 
1606
1621
  if (showdown and pkmn.options.ebc and checkEBC(battle)) {
1607
- try options.log.tie();
1622
+ try options.log.tie(.{});
1608
1623
  return Result.Tie;
1609
1624
  }
1610
1625
 
1611
1626
  if (showdown and battle.turn >= 1000) {
1612
- try options.log.tie();
1627
+ try options.log.tie(.{});
1613
1628
  return Result.Tie;
1614
1629
  } else if (battle.turn >= 65535) {
1615
1630
  return Result.Error;
1616
1631
  }
1617
1632
 
1618
- try options.log.turn(battle.turn);
1633
+ try options.log.turn(.{battle.turn});
1619
1634
 
1620
1635
  return Result.Default;
1621
1636
  }
@@ -1675,9 +1690,9 @@ fn checkEBC(battle: anytype) bool {
1675
1690
  }
1676
1691
 
1677
1692
  fn disabled(side: *Side, ident: ID, options: anytype) !bool {
1678
- if (side.active.volatiles.disabled_move != 0) {
1693
+ if (side.active.volatiles.disable_move != 0) {
1679
1694
  // 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;
1695
+ const m = side.active.moves[side.active.volatiles.disable_move - 1].id;
1681
1696
  // side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
1682
1697
  const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
1683
1698
  m
@@ -1685,32 +1700,21 @@ fn disabled(side: *Side, ident: ID, options: anytype) !bool {
1685
1700
  side.last_selected_move;
1686
1701
  if (m != .None and m == last) {
1687
1702
  side.active.volatiles.Charging = false;
1688
- try options.log.disabled(ident, last);
1703
+ try options.log.cant(.{ ident, .Disable, last });
1689
1704
  return true;
1690
1705
  }
1691
1706
  }
1692
1707
  return false;
1693
1708
  }
1694
1709
 
1695
- inline fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1710
+ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1696
1711
  const side = battle.side(who);
1697
1712
  if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
1698
1713
  try Effects.boost(battle, who, Move.get(.Rage), options);
1699
1714
  }
1700
1715
  }
1701
1716
 
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(
1717
+ fn onBegin(
1714
1718
  battle: anytype,
1715
1719
  player: Player,
1716
1720
  move: Move.Data,
@@ -1718,7 +1722,6 @@ inline fn onBegin(
1718
1722
  residual: *bool,
1719
1723
  options: anytype,
1720
1724
  ) !void {
1721
- assert(move.effect.onBegin());
1722
1725
  return switch (move.effect) {
1723
1726
  .Confusion => Effects.confusion(battle, player, move, options),
1724
1727
  .Conversion => Effects.conversion(battle, player, options),
@@ -1740,8 +1743,7 @@ inline fn onBegin(
1740
1743
  };
1741
1744
  }
1742
1745
 
1743
- inline fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1744
- assert(move.effect.onEnd());
1746
+ fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1745
1747
  return switch (move.effect) {
1746
1748
  .Bide => Effects.bide(battle, player, options),
1747
1749
  // zig fmt: off
@@ -1756,14 +1758,13 @@ inline fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anyty
1756
1758
  };
1757
1759
  }
1758
1760
 
1759
- inline fn alwaysHappens(
1761
+ fn alwaysHappens(
1760
1762
  battle: anytype,
1761
1763
  player: Player,
1762
1764
  move: Move.Data,
1763
1765
  residual: *bool,
1764
1766
  options: anytype,
1765
1767
  ) !void {
1766
- assert(move.effect.alwaysHappens());
1767
1768
  return switch (move.effect) {
1768
1769
  .DrainHP, .DreamEater => Effects.drainHP(battle, player, options),
1769
1770
  .Explode => Effects.explode(battle, player),
@@ -1775,7 +1776,7 @@ inline fn alwaysHappens(
1775
1776
  };
1776
1777
  }
1777
1778
 
1778
- inline fn moveEffect(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1779
+ fn moveEffect(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1779
1780
  return switch (move.effect) {
1780
1781
  .BurnChance1, .BurnChance2 => Effects.burnChance(battle, player, move, options),
1781
1782
  .ConfusionChance => Effects.confusion(battle, player, move, options),
@@ -1799,10 +1800,12 @@ pub const Effects = struct {
1799
1800
 
1800
1801
  side.active.volatiles.Bide = true;
1801
1802
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1803
+
1802
1804
  side.active.volatiles.state = 0;
1803
1805
  side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
1806
+ options.chance.observe(.attacking, player, .started);
1804
1807
 
1805
- try options.log.start(battle.active(player), .Bide);
1808
+ try options.log.start(.{ battle.active(player), .Bide });
1806
1809
  }
1807
1810
 
1808
1811
  fn burnChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1816,11 +1819,11 @@ pub const Effects = struct {
1816
1819
  // GLITCH: Freeze top move selection desync can occur if thawed Pokémon is slower
1817
1820
  if (Status.is(foe_stored.status, .FRZ)) {
1818
1821
  assert(move.type == .Fire);
1819
- try options.log.curestatus(
1822
+ try options.log.curestatus(.{
1820
1823
  battle.active(player.foe()),
1821
1824
  foe_stored.status,
1822
1825
  .Message,
1823
- );
1826
+ });
1824
1827
  foe_stored.status = 0;
1825
1828
  }
1826
1829
  return;
@@ -1834,7 +1837,7 @@ pub const Effects = struct {
1834
1837
  foe_stored.status = Status.init(.BRN);
1835
1838
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
1836
1839
 
1837
- try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
1840
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
1838
1841
  }
1839
1842
 
1840
1843
  fn charge(battle: anytype, player: Player, options: anytype) !void {
@@ -1844,8 +1847,8 @@ pub const Effects = struct {
1844
1847
  volatiles.Charging = true;
1845
1848
  const move = side.last_selected_move;
1846
1849
  if (move == .Fly or move == .Dig) volatiles.Invulnerable = true;
1847
- try options.log.laststill();
1848
- try options.log.prepare(battle.active(player), move);
1850
+ try options.log.laststill(.{});
1851
+ try options.log.prepare(.{ battle.active(player), move });
1849
1852
  }
1850
1853
 
1851
1854
  fn confusion(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1856,42 +1859,38 @@ pub const Effects = struct {
1856
1859
  if (!try Rolls.confusionChance(battle, player, options)) return;
1857
1860
  } else {
1858
1861
  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
- }
1862
+ if (!try checkHit(battle, player, move, options)) return;
1863
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1864
1864
  } 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
- }
1865
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1866
+ if (!try checkHit(battle, player, move, options)) return;
1870
1867
  }
1871
- try options.chance.commit(player, .hit);
1872
1868
  }
1873
1869
 
1874
1870
  if (foe.active.volatiles.Confusion) return;
1875
1871
  foe.active.volatiles.Confusion = true;
1876
- foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
1877
1872
 
1878
- try options.log.start(battle.active(player.foe()), .Confusion);
1873
+ foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
1874
+ options.chance.observe(.confusion, player.foe(), .started);
1875
+
1876
+ try options.log.start(.{ battle.active(player.foe()), .Confusion });
1879
1877
  }
1880
1878
 
1881
1879
  fn conversion(battle: anytype, player: Player, options: anytype) !void {
1882
1880
  const foe = battle.foe(player);
1883
1881
 
1884
1882
  if (foe.active.volatiles.Invulnerable) {
1885
- try options.log.lastmiss();
1886
- return options.log.miss(battle.active(player));
1883
+ try options.log.lastmiss(.{});
1884
+ return options.log.miss(.{battle.active(player)});
1887
1885
  }
1888
1886
 
1889
1887
  battle.side(player).active.types = foe.active.types;
1890
- return options.log.typechange(
1888
+ return options.log.start(.{
1891
1889
  battle.active(player),
1890
+ .TypeChange,
1892
1891
  foe.active.types,
1893
1892
  battle.active(player.foe()),
1894
- );
1893
+ });
1895
1894
  }
1896
1895
 
1897
1896
  fn disable(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
@@ -1900,13 +1899,10 @@ pub const Effects = struct {
1900
1899
  const foe_ident = battle.active(player.foe());
1901
1900
 
1902
1901
  // 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
- }
1902
+ if (!showdown and !try checkHit(battle, player, move, options)) return null;
1907
1903
 
1908
- if (volatiles.disabled_move != 0) {
1909
- try options.log.fail(foe_ident, .None);
1904
+ if (volatiles.disable_move != 0) {
1905
+ try options.log.fail(.{ foe_ident, .None });
1910
1906
  return null;
1911
1907
  }
1912
1908
 
@@ -1922,7 +1918,7 @@ pub const Effects = struct {
1922
1918
  // Technically this is still considered simply a "miss" on the cartridge,
1923
1919
  // but diverging from Pokémon Showdown here would mostly just be pedantic
1924
1920
  if (n == 0) {
1925
- try options.log.fail(foe_ident, .None);
1921
+ try options.log.fail(.{ foe_ident, .None });
1926
1922
  return null;
1927
1923
  } else if (err) {
1928
1924
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
@@ -1930,18 +1926,19 @@ pub const Effects = struct {
1930
1926
  return Result.Error;
1931
1927
  }
1932
1928
 
1933
- volatiles.disabled_move =
1929
+ volatiles.disable_move =
1934
1930
  @intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
1935
- volatiles.disabled_duration = Rolls.disableDuration(battle, player, options);
1931
+ volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
1932
+ options.chance.observe(.disable, player.foe(), .started);
1936
1933
 
1937
- const id = foe.active.move(volatiles.disabled_move).id;
1938
- try options.log.startEffect(foe_ident, .Disable, id);
1934
+ const id = foe.active.move(volatiles.disable_move).id;
1935
+ try options.log.start(.{ foe_ident, .Disable, id });
1939
1936
  return null;
1940
1937
  }
1941
1938
 
1942
1939
  fn drainHP(battle: anytype, player: Player, options: anytype) !void {
1943
- var side = battle.side(player);
1944
- var stored = side.stored();
1940
+ var stored = battle.side(player).stored();
1941
+ const ident = battle.active(player);
1945
1942
 
1946
1943
  if (battle.last_damage == 0) {
1947
1944
  assert(showdown);
@@ -1954,7 +1951,7 @@ pub const Effects = struct {
1954
1951
  if (stored.hp == stored.stats.hp) return;
1955
1952
  stored.hp = @min(stored.stats.hp, stored.hp + drain);
1956
1953
 
1957
- try options.log.drain(battle.active(player), stored, battle.active(player.foe()));
1954
+ try options.log.heal(.{ ident, stored, .Drain, battle.active(player.foe()) });
1958
1955
  }
1959
1956
 
1960
1957
  fn explode(battle: anytype, player: Player) !void {
@@ -1986,7 +1983,7 @@ pub const Effects = struct {
1986
1983
  if (side.active.volatiles.FocusEnergy) return;
1987
1984
  side.active.volatiles.FocusEnergy = true;
1988
1985
 
1989
- try options.log.start(battle.active(player), .FocusEnergy);
1986
+ try options.log.start(.{ battle.active(player), .FocusEnergy });
1990
1987
  }
1991
1988
 
1992
1989
  fn freezeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2008,7 +2005,7 @@ pub const Effects = struct {
2008
2005
  foe_stored.status = Status.init(.FRZ);
2009
2006
  // GLITCH: Hyper Beam recharging status is not cleared
2010
2007
 
2011
- try options.log.status(foe_ident, foe_stored.status, .None);
2008
+ try options.log.status(.{ foe_ident, foe_stored.status, .None });
2012
2009
  }
2013
2010
 
2014
2011
  fn haze(battle: anytype, player: Player, options: anytype) !void {
@@ -2016,7 +2013,7 @@ pub const Effects = struct {
2016
2013
  var side = battle.side(player);
2017
2014
  var foe = battle.foe(player);
2018
2015
 
2019
- var side_stored = side.stored();
2016
+ const side_stored = side.stored();
2020
2017
  var foe_stored = foe.stored();
2021
2018
 
2022
2019
  const player_ident = battle.active(player);
@@ -2028,8 +2025,8 @@ pub const Effects = struct {
2028
2025
  side.active.stats = side_stored.stats;
2029
2026
  foe.active.stats = foe_stored.stats;
2030
2027
 
2031
- try log.activate(player_ident, .Haze);
2032
- try log.clearallboost();
2028
+ try log.activate(.{ player_ident, .Haze });
2029
+ try log.clearallboost(.{});
2033
2030
 
2034
2031
  // Pokémon Showdown clears P1 then P2 instead of status -> side -> foe
2035
2032
  if (showdown) {
@@ -2037,24 +2034,26 @@ pub const Effects = struct {
2037
2034
  const p: Player = @enumFromInt(i);
2038
2035
  // Pokémon Showdown incorrectly does not prevent sleep/freeze from moving
2039
2036
  if (p != player and Status.any(s.stored().status)) {
2040
- try log.curestatus(foe_ident, foe_stored.status, .Silent);
2037
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2041
2038
  s.stored().status = 0;
2039
+ options.chance.observe(.sleep, p, .None);
2042
2040
  } else if (showdown and s.stored().status == Status.TOX) {
2043
2041
  s.stored().status = Status.init(.PSN);
2044
- try log.status(battle.active(p), s.stored().status, .None);
2042
+ try log.status(.{ battle.active(p), s.stored().status, .None });
2045
2043
  }
2046
- try clearVolatiles(battle, p, p != player, options);
2044
+ try clearVolatiles(battle, p, options);
2047
2045
  }
2048
2046
  } else {
2049
2047
  if (Status.any(foe_stored.status)) {
2050
2048
  if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
2051
2049
  foe.last_selected_move = .SKIP_TURN;
2050
+ options.chance.observe(.sleep, player.foe(), .None);
2052
2051
  }
2053
- try log.curestatus(foe_ident, foe_stored.status, .Silent);
2052
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2054
2053
  foe_stored.status = 0;
2055
2054
  }
2056
- try clearVolatiles(battle, player, false, options);
2057
- try clearVolatiles(battle, player.foe(), true, options);
2055
+ try clearVolatiles(battle, player, options);
2056
+ try clearVolatiles(battle, player.foe(), options);
2058
2057
  }
2059
2058
  }
2060
2059
 
@@ -2066,7 +2065,7 @@ pub const Effects = struct {
2066
2065
  // GLITCH: HP recovery move failure glitches
2067
2066
  const delta = stored.stats.hp - stored.hp;
2068
2067
  if (delta == 0 or (delta & 255 == 255 and stored.hp % 256 != 0)) {
2069
- return try options.log.fail(ident, .None);
2068
+ return options.log.fail(.{ ident, .None });
2070
2069
  }
2071
2070
 
2072
2071
  const rest = side.last_selected_move == .Rest;
@@ -2074,17 +2073,17 @@ pub const Effects = struct {
2074
2073
  // Adding the sleep status runs the sleep condition handler to roll duration
2075
2074
  if (showdown) battle.rng.advance(1);
2076
2075
  stored.status = Status.slf(2);
2077
- try options.log.statusFrom(ident, stored.status, Move.Rest);
2076
+ try options.log.status(.{ ident, stored.status, .From, Move.Rest });
2078
2077
  stored.hp = stored.stats.hp;
2079
2078
  } else {
2080
2079
  stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
2081
2080
  }
2082
- try options.log.heal(ident, stored, if (rest) Heal.Silent else Heal.None);
2081
+ try options.log.heal(.{ ident, stored, if (rest) Heal.Silent else Heal.None });
2083
2082
  }
2084
2083
 
2085
2084
  fn hyperBeam(battle: anytype, player: Player, options: anytype) !void {
2086
2085
  battle.side(player).active.volatiles.Recharging = true;
2087
- try options.log.mustrecharge(battle.active(player));
2086
+ try options.log.mustrecharge(.{battle.active(player)});
2088
2087
  }
2089
2088
 
2090
2089
  fn leechSeed(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2093,33 +2092,31 @@ pub const Effects = struct {
2093
2092
  if (showdown) {
2094
2093
  // Invulnerability trumps type immunity on Pokémon Showdown
2095
2094
  if (!foe.active.volatiles.Invulnerable and foe.active.types.includes(.Grass)) {
2096
- return options.log.immune(battle.active(player.foe()), .None);
2095
+ return options.log.immune(.{ battle.active(player.foe()), .None });
2097
2096
  }
2098
2097
  if (!try checkHit(battle, player, move, options)) return;
2099
2098
  if (foe.active.volatiles.LeechSeed) return;
2100
2099
  } else {
2101
2100
  if (!try checkHit(battle, player, move, options)) return;
2102
2101
  if (foe.active.types.includes(.Grass) or foe.active.volatiles.LeechSeed) {
2103
- try options.log.lastmiss();
2104
- return options.log.miss(battle.active(player));
2102
+ try options.log.lastmiss(.{});
2103
+ return options.log.miss(.{battle.active(player)});
2105
2104
  }
2106
2105
  }
2107
2106
 
2108
- try options.chance.commit(player, .hit);
2109
2107
  foe.active.volatiles.LeechSeed = true;
2110
2108
 
2111
- try options.log.start(battle.active(player.foe()), .LeechSeed);
2109
+ try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
2112
2110
  }
2113
2111
 
2114
2112
  fn lightScreen(battle: anytype, player: Player, options: anytype) !void {
2115
2113
  var side = battle.side(player);
2114
+ const ident = battle.active(player);
2116
2115
 
2117
- if (side.active.volatiles.LightScreen) {
2118
- return options.log.fail(battle.active(player), .None);
2119
- }
2116
+ if (side.active.volatiles.LightScreen) return options.log.fail(.{ ident, .None });
2120
2117
  side.active.volatiles.LightScreen = true;
2121
2118
 
2122
- try options.log.start(battle.active(player), .LightScreen);
2119
+ try options.log.start(.{ ident, .LightScreen });
2123
2120
  }
2124
2121
 
2125
2122
  fn mimic(battle: anytype, player: Player, move: Move.Data, mslot: u8, options: anytype) !void {
@@ -2147,17 +2144,15 @@ pub const Effects = struct {
2147
2144
  if (!has_mimic) {
2148
2145
  // Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
2149
2146
  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);
2147
+ return options.log.fail(.{ battle.active(player.foe()), .None });
2152
2148
  }
2153
2149
  }
2154
2150
  if (!try checkHit(battle, player, move, options)) return;
2155
- try options.chance.commit(player, .hit);
2156
2151
 
2157
2152
  const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
2158
2153
  side.active.move(oslot).id = foe.active.move(rslot).id;
2159
2154
 
2160
- try options.log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
2155
+ try options.log.start(.{ battle.active(player), .Mimic, side.active.move(oslot).id });
2161
2156
  }
2162
2157
 
2163
2158
  fn mist(battle: anytype, player: Player, options: anytype) !void {
@@ -2166,7 +2161,7 @@ pub const Effects = struct {
2166
2161
  if (side.active.volatiles.Mist) return;
2167
2162
  side.active.volatiles.Mist = true;
2168
2163
 
2169
- try options.log.start(battle.active(player), .Mist);
2164
+ try options.log.start(.{ battle.active(player), .Mist });
2170
2165
  }
2171
2166
 
2172
2167
  fn multiHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2192,25 +2187,25 @@ pub const Effects = struct {
2192
2187
 
2193
2188
  if (showdown) {
2194
2189
  // 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;
2190
+ if (immune and !foe.active.volatiles.Invulnerable) {
2191
+ return log.immune(.{ foe_ident, .None });
2192
+ } else if (!try checkHit(battle, player, move, options)) return;
2197
2193
  }
2198
2194
  if (Status.any(foe_stored.status)) {
2199
- return log.fail(
2195
+ return log.fail(.{
2200
2196
  foe_ident,
2201
- if (Status.is(foe_stored.status, .PAR)) .Paralysis else .None,
2202
- );
2197
+ if (Status.is(foe_stored.status, .PAR)) Fail.Paralysis else Fail.None,
2198
+ });
2203
2199
  }
2204
2200
  if (!showdown) {
2205
- if (immune) return log.immune(foe_ident, .None);
2201
+ if (immune) return log.immune(.{ foe_ident, .None });
2206
2202
  if (!try checkHit(battle, player, move, options)) return;
2207
2203
  }
2208
- try options.chance.commit(player, .hit);
2209
2204
 
2210
2205
  foe_stored.status = Status.init(.PAR);
2211
2206
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2212
2207
 
2213
- try log.status(foe_ident, foe_stored.status, .None);
2208
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2214
2209
  }
2215
2210
 
2216
2211
  fn paralyzeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2232,12 +2227,12 @@ pub const Effects = struct {
2232
2227
  foe_stored.status = Status.init(.PAR);
2233
2228
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2234
2229
 
2235
- try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
2230
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
2236
2231
  }
2237
2232
 
2238
2233
  fn payDay(battle: anytype, player: Player, options: anytype) !void {
2239
2234
  if (!showdown or !battle.foe(player).active.volatiles.Substitute) {
2240
- try options.log.fieldactivate();
2235
+ try options.log.fieldactivate(.{});
2241
2236
  }
2242
2237
  }
2243
2238
 
@@ -2252,28 +2247,27 @@ pub const Effects = struct {
2252
2247
  return;
2253
2248
  } else if (foe.active.volatiles.Substitute) {
2254
2249
  if (move.effect != .Poison) return;
2255
- return log.fail(foe_ident, .None);
2250
+ return log.fail(.{ foe_ident, .None });
2256
2251
  } else if (Status.any(foe_stored.status)) {
2257
2252
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2258
2253
  // Pokémon Showdown considers Toxic to be a status even in Generation I and so
2259
2254
  // 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))
2255
+ return log.fail(.{ foe_ident, if (Status.is(foe_stored.status, .PSN))
2261
2256
  if (!showdown)
2262
- .Poison
2257
+ Fail.Poison
2263
2258
  else if (toxic == (foe_stored.status == Status.TOX))
2264
- if (toxic) .Toxic else .Poison
2259
+ if (toxic) Fail.Toxic else Fail.Poison
2265
2260
  else
2266
- .None
2261
+ Fail.None
2267
2262
  else
2268
- .None);
2263
+ Fail.None });
2269
2264
  } else if (foe.active.types.includes(.Poison)) {
2270
2265
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2271
- return log.immune(foe_ident, .None);
2266
+ return log.immune(.{ foe_ident, .None });
2272
2267
  }
2273
2268
 
2274
2269
  if (move.effect == .Poison) {
2275
2270
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2276
- try options.chance.commit(player, .hit);
2277
2271
  } else {
2278
2272
  if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
2279
2273
  return;
@@ -2287,7 +2281,7 @@ pub const Effects = struct {
2287
2281
  foe.active.volatiles.toxic = 0;
2288
2282
  }
2289
2283
 
2290
- try log.status(foe_ident, foe_stored.status, .None);
2284
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2291
2285
  }
2292
2286
 
2293
2287
  fn rage(battle: anytype, player: Player) !void {
@@ -2307,22 +2301,23 @@ pub const Effects = struct {
2307
2301
  @as(u8, if (side.last_selected_move == .Struggle) 2 else 4), 1));
2308
2302
  stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
2309
2303
 
2310
- try options.log.damageOf(
2304
+ try options.log.damage(.{
2311
2305
  battle.active(player),
2312
2306
  stored,
2313
2307
  .RecoilOf,
2314
2308
  battle.active(player.foe()),
2315
- );
2309
+ });
2316
2310
  if (showdown and stored.hp == 0) residual.* = false;
2317
2311
  }
2318
2312
 
2319
2313
  fn reflect(battle: anytype, player: Player, options: anytype) !void {
2320
2314
  var side = battle.side(player);
2315
+ const ident = battle.active(player);
2321
2316
 
2322
- if (side.active.volatiles.Reflect) return options.log.fail(battle.active(player), .None);
2317
+ if (side.active.volatiles.Reflect) return options.log.fail(.{ ident, .None });
2323
2318
  side.active.volatiles.Reflect = true;
2324
2319
 
2325
- try options.log.start(battle.active(player), .Reflect);
2320
+ try options.log.start(.{ ident, .Reflect });
2326
2321
  }
2327
2322
 
2328
2323
  fn sleep(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2336,14 +2331,13 @@ pub const Effects = struct {
2336
2331
  // the Sleep Clause Mod might activate, causing us to not actually bypass
2337
2332
  } else {
2338
2333
  if (Status.any(foe_stored.status)) {
2339
- return options.log.fail(
2334
+ return options.log.fail(.{
2340
2335
  foe_ident,
2341
- if (Status.is(foe_stored.status, .SLP)) .Sleep else .None,
2342
- );
2336
+ if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
2337
+ });
2343
2338
  }
2344
- // If checkHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2339
+ // If moveHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2345
2340
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2346
- try options.chance.commit(player, .hit);
2347
2341
  }
2348
2342
 
2349
2343
  // Sleep Clause Mod
@@ -2355,19 +2349,20 @@ pub const Effects = struct {
2355
2349
  foe.active.volatiles.Recharging = false;
2356
2350
 
2357
2351
  foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2352
+ options.chance.observe(.sleep, player.foe(), .started);
2353
+
2358
2354
  const last = battle.side(player).last_selected_move;
2359
- try options.log.statusFrom(foe_ident, foe_stored.status, last);
2355
+ try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
2360
2356
  }
2361
2357
 
2362
2358
  fn splash(battle: anytype, player: Player, options: anytype) !void {
2363
- try options.log.activate(battle.active(player), .Splash);
2359
+ try options.log.activate(.{ battle.active(player), .Splash });
2364
2360
  }
2365
2361
 
2366
2362
  fn substitute(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
2367
2363
  var side = battle.side(player);
2368
2364
  if (side.active.volatiles.Substitute) {
2369
- try options.log.fail(battle.active(player), .Substitute);
2370
- return;
2365
+ return options.log.fail(.{ battle.active(player), .Substitute });
2371
2366
  }
2372
2367
 
2373
2368
  assert(side.stored().stats.hp <= 1023);
@@ -2379,28 +2374,30 @@ pub const Effects = struct {
2379
2374
  // avoid having to convert to floating point)
2380
2375
  const required_hp = if (showdown) @divFloor(side.stored().stats.hp - 1, 4) + 1 else hp;
2381
2376
  if (side.stored().hp < required_hp) {
2382
- try options.log.fail(battle.active(player), .Weak);
2383
- return;
2377
+ return options.log.fail(.{ battle.active(player), .Weak });
2384
2378
  }
2385
2379
 
2386
2380
  // GLITCH: can leave the user with 0 HP (faints later) because didn't check '<=' above
2387
2381
  side.stored().hp -= hp;
2388
2382
  side.active.volatiles.substitute = hp + 1;
2389
2383
  side.active.volatiles.Substitute = true;
2390
- try options.log.start(battle.active(player), .Substitute);
2384
+ try options.log.start(.{ battle.active(player), .Substitute });
2391
2385
  if (hp > 0) {
2392
- try options.log.damage(battle.active(player), side.stored(), .None);
2386
+ try options.log.damage(.{ battle.active(player), side.stored(), .None });
2393
2387
  if (showdown and side.stored().hp == 0) residual.* = false;
2394
2388
  }
2395
2389
  }
2396
2390
 
2397
2391
  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
2392
  // 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;
2393
+ if (showdown) {
2394
+ if (battle.side(player).last_selected_move == .Teleport) return;
2395
+ _ = try checkHit(battle, player, move, options);
2396
+ battle.last_damage = 0;
2397
+ } else {
2398
+ try options.log.fail(.{ battle.active(player), .None });
2399
+ try options.log.laststill(.{});
2400
+ }
2404
2401
  }
2405
2402
 
2406
2403
  fn thrashing(battle: anytype, player: Player, options: anytype) void {
@@ -2410,6 +2407,8 @@ pub const Effects = struct {
2410
2407
 
2411
2408
  volatiles.Thrashing = true;
2412
2409
  volatiles.attacks = Rolls.attackingDuration(battle, player, options);
2410
+
2411
+ options.chance.observe(.attacking, player, .started);
2413
2412
  }
2414
2413
 
2415
2414
  fn transform(battle: anytype, player: Player, options: anytype) !void {
@@ -2438,7 +2437,7 @@ pub const Effects = struct {
2438
2437
  side.active.moves[i].pp = if (m.id != .None) 5 else 0;
2439
2438
  }
2440
2439
 
2441
- try options.log.transform(battle.active(player), foe_ident);
2440
+ try options.log.transform(.{ battle.active(player), foe_ident });
2442
2441
  }
2443
2442
 
2444
2443
  fn binding(battle: anytype, player: Player, rewrap: bool, options: anytype) !void {
@@ -2455,6 +2454,7 @@ pub const Effects = struct {
2455
2454
 
2456
2455
  side.active.volatiles.attacks =
2457
2456
  try Rolls.distribution(battle, .Binding, player, options) - 1;
2457
+ options.chance.observe(.binding, player, .started);
2458
2458
  }
2459
2459
 
2460
2460
  fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2468,87 +2468,87 @@ pub const Effects = struct {
2468
2468
  switch (move.effect) {
2469
2469
  .AttackUp1, .AttackUp2, .Rage => {
2470
2470
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2471
- if (boosts.atk == 6) return try log.fail(ident, .None);
2471
+ if (boosts.atk == 6) return log.fail(.{ ident, .None });
2472
2472
  const n: u2 = if (move.effect == .AttackUp2) 2 else 1;
2473
2473
  boosts.atk = @as(i4, @intCast(@min(6, @as(i8, boosts.atk) + n)));
2474
- const reason = if (move.effect == .Rage) Boost.Rage else Boost.Attack;
2474
+ const reason: Boost = if (move.effect == .Rage) .Rage else .Attack;
2475
2475
  if (stats.atk == MAX_STAT_VALUE) {
2476
2476
  boosts.atk -= 1;
2477
2477
  if (showdown) {
2478
- try log.boost(ident, reason, n);
2479
- try log.boost(ident, Boost.Attack, -1);
2478
+ try log.boost(.{ ident, reason, n });
2479
+ try log.boost(.{ ident, .Attack, -1 });
2480
2480
  if (move.effect == .Rage) return;
2481
2481
  }
2482
- return try log.fail(ident, .None);
2482
+ return log.fail(.{ ident, .None });
2483
2483
  }
2484
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2484
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2485
2485
  const stat = unmodifiedStats(battle, side).atk;
2486
2486
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2487
- try log.boost(ident, reason, n);
2487
+ try log.boost(.{ ident, reason, n });
2488
2488
  // Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
2489
2489
  if (showdown and move.effect == .Rage) return;
2490
2490
  },
2491
2491
  .DefenseUp1, .DefenseUp2 => {
2492
2492
  assert(boosts.def >= -6 and boosts.def <= 6);
2493
- if (boosts.def == 6) return try log.fail(ident, .None);
2493
+ if (boosts.def == 6) return log.fail(.{ ident, .None });
2494
2494
  const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
2495
2495
  boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
2496
2496
  if (stats.def == MAX_STAT_VALUE) {
2497
2497
  boosts.def -= 1;
2498
2498
  if (showdown) {
2499
- try log.boost(ident, .Defense, n);
2500
- try log.boost(ident, .Defense, -1);
2499
+ try log.boost(.{ ident, .Defense, n });
2500
+ try log.boost(.{ ident, .Defense, -1 });
2501
2501
  }
2502
- return try log.fail(ident, .None);
2502
+ return log.fail(.{ ident, .None });
2503
2503
  }
2504
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2504
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2505
2505
  const stat = unmodifiedStats(battle, side).def;
2506
2506
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2507
- try log.boost(ident, .Defense, n);
2507
+ try log.boost(.{ ident, .Defense, n });
2508
2508
  },
2509
2509
  .SpeedUp2 => {
2510
2510
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2511
- if (boosts.spe == 6) return try log.fail(ident, .None);
2511
+ if (boosts.spe == 6) return log.fail(.{ ident, .None });
2512
2512
  boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
2513
2513
  if (stats.spe == MAX_STAT_VALUE) {
2514
2514
  boosts.spe -= 1;
2515
2515
  if (showdown) {
2516
- try log.boost(ident, .Speed, 2);
2517
- try log.boost(ident, .Speed, -1);
2516
+ try log.boost(.{ ident, .Speed, 2 });
2517
+ try log.boost(.{ ident, .Speed, -1 });
2518
2518
  }
2519
- return try log.fail(ident, .None);
2519
+ return log.fail(.{ ident, .None });
2520
2520
  }
2521
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2521
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2522
2522
  const stat = unmodifiedStats(battle, side).spe;
2523
2523
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2524
- try log.boost(ident, .Speed, 2);
2524
+ try log.boost(.{ ident, .Speed, 2 });
2525
2525
  },
2526
2526
  .SpecialUp1, .SpecialUp2 => {
2527
2527
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2528
- if (boosts.spc == 6) return try log.fail(ident, .None);
2528
+ if (boosts.spc == 6) return log.fail(.{ ident, .None });
2529
2529
  const n: u2 = if (move.effect == .SpecialUp2) 2 else 1;
2530
2530
  boosts.spc = @intCast(@min(6, @as(i8, boosts.spc) + n));
2531
2531
  if (stats.spc == MAX_STAT_VALUE) {
2532
2532
  boosts.spc -= 1;
2533
2533
  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);
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 });
2538
2538
  }
2539
- return try log.fail(ident, .None);
2539
+ return log.fail(.{ ident, .None });
2540
2540
  }
2541
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2541
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2542
2542
  const stat = unmodifiedStats(battle, side).spc;
2543
2543
  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);
2544
+ try log.boost(.{ ident, .SpecialAttack, n });
2545
+ try log.boost(.{ ident, .SpecialDefense, n });
2546
2546
  },
2547
2547
  .EvasionUp1 => {
2548
2548
  assert(boosts.evasion >= -6 and boosts.evasion <= 6);
2549
- if (boosts.evasion == 6) return try log.fail(ident, .None);
2549
+ if (boosts.evasion == 6) return log.fail(.{ ident, .None });
2550
2550
  boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
2551
- try log.boost(ident, .Evasion, 1);
2551
+ try log.boost(.{ ident, .Evasion, 1 });
2552
2552
  },
2553
2553
  else => unreachable,
2554
2554
  }
@@ -2563,7 +2563,7 @@ pub const Effects = struct {
2563
2563
  const foe_ident = battle.active(player.foe());
2564
2564
  const secondary = move.effect.isStatDownChance();
2565
2565
 
2566
- if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(foe_ident, .None);
2566
+ if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(.{ foe_ident, .None });
2567
2567
 
2568
2568
  if (secondary) {
2569
2569
  const proc = try Rolls.unboost(battle, player, options);
@@ -2571,7 +2571,6 @@ pub const Effects = struct {
2571
2571
  } else {
2572
2572
  // checkHit already checks for Invulnerable
2573
2573
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2574
- try options.chance.commit(player, .hit);
2575
2574
  }
2576
2575
 
2577
2576
  var stats = &foe.active.stats;
@@ -2581,82 +2580,82 @@ pub const Effects = struct {
2581
2580
  switch (move.effect) {
2582
2581
  .AttackDown1, .AttackDownChance => {
2583
2582
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2584
- if (boosts.atk == -6) return if (fail) try log.fail(foe_ident, .None);
2583
+ if (boosts.atk == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2585
2584
  boosts.atk = @intCast(@max(-6, @as(i8, boosts.atk) - 1));
2586
2585
  if (stats.atk == 1) {
2587
2586
  boosts.atk += 1;
2588
2587
  if (showdown) {
2589
- try log.boost(foe_ident, .Attack, -1);
2590
- try log.boost(foe_ident, .Attack, 1);
2588
+ try log.boost(.{ foe_ident, .Attack, -1 });
2589
+ try log.boost(.{ foe_ident, .Attack, 1 });
2591
2590
  }
2592
- return try log.fail(foe_ident, .None);
2591
+ return log.fail(.{ foe_ident, .None });
2593
2592
  }
2594
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2593
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2595
2594
  const stat = unmodifiedStats(battle, foe).atk;
2596
2595
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2597
- try log.boost(foe_ident, .Attack, -1);
2596
+ try log.boost(.{ foe_ident, .Attack, -1 });
2598
2597
  },
2599
2598
  .DefenseDown1, .DefenseDown2, .DefenseDownChance => {
2600
2599
  assert(boosts.def >= -6 and boosts.def <= 6);
2601
- if (boosts.def == -6) return if (fail) try log.fail(foe_ident, .None);
2600
+ if (boosts.def == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2602
2601
  const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
2603
2602
  boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
2604
2603
  if (stats.def == 1) {
2605
2604
  boosts.def += 1;
2606
2605
  if (showdown) {
2607
- try log.boost(foe_ident, .Defense, -@as(i8, n));
2608
- try log.boost(foe_ident, .Defense, 1);
2606
+ try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
2607
+ try log.boost(.{ foe_ident, .Defense, 1 });
2609
2608
  }
2610
- return try log.fail(foe_ident, .None);
2609
+ return log.fail(.{ foe_ident, .None });
2611
2610
  }
2612
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2611
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2613
2612
  const stat = unmodifiedStats(battle, foe).def;
2614
2613
  stats.def = @max(1, stat * mod[0] / mod[1]);
2615
- try log.boost(foe_ident, .Defense, -@as(i8, n));
2614
+ try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
2616
2615
  },
2617
2616
  .SpeedDown1, .SpeedDownChance => {
2618
2617
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2619
- if (boosts.spe == -6) return if (fail) try log.fail(foe_ident, .None);
2618
+ if (boosts.spe == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2620
2619
  boosts.spe = @intCast(@max(-6, @as(i8, boosts.spe) - 1));
2621
2620
  if (stats.spe == 1) {
2622
2621
  boosts.spe += 1;
2623
2622
  if (showdown) {
2624
- try log.boost(foe_ident, .Speed, -1);
2625
- try log.boost(foe_ident, .Speed, 1);
2623
+ try log.boost(.{ foe_ident, .Speed, -1 });
2624
+ try log.boost(.{ foe_ident, .Speed, 1 });
2626
2625
  }
2627
- return try log.fail(foe_ident, .None);
2626
+ return log.fail(.{ foe_ident, .None });
2628
2627
  }
2629
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2628
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2630
2629
  const stat = unmodifiedStats(battle, foe).spe;
2631
2630
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2632
- try log.boost(foe_ident, .Speed, -1);
2631
+ try log.boost(.{ foe_ident, .Speed, -1 });
2633
2632
  assert(boosts.spe >= -6);
2634
2633
  },
2635
2634
  .SpecialDownChance => {
2636
2635
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2637
- if (boosts.spc == -6) return if (fail) try log.fail(foe_ident, .None);
2636
+ if (boosts.spc == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2638
2637
  boosts.spc = @intCast(@max(-6, @as(i8, boosts.spc) - 1));
2639
2638
  if (stats.spc == 1) {
2640
2639
  boosts.spc += 1;
2641
2640
  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);
2641
+ try log.boost(.{ foe_ident, .SpecialAttack, -1 });
2642
+ try log.boost(.{ foe_ident, .SpecialAttack, 1 });
2643
+ try log.boost(.{ foe_ident, .SpecialDefense, -1 });
2644
+ try log.boost(.{ foe_ident, .SpecialDefense, 1 });
2646
2645
  }
2647
- return try log.fail(foe_ident, .None);
2646
+ return log.fail(.{ foe_ident, .None });
2648
2647
  }
2649
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2648
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2650
2649
  const stat = unmodifiedStats(battle, foe).spc;
2651
2650
  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);
2651
+ try log.boost(.{ foe_ident, .SpecialAttack, -1 });
2652
+ try log.boost(.{ foe_ident, .SpecialDefense, -1 });
2654
2653
  },
2655
2654
  .AccuracyDown1 => {
2656
2655
  assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
2657
- if (boosts.accuracy == -6) return if (fail) try log.fail(foe_ident, .None);
2656
+ if (boosts.accuracy == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2658
2657
  boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
2659
- try log.boost(foe_ident, .Accuracy, -1);
2658
+ try log.boost(.{ foe_ident, .Accuracy, -1 });
2660
2659
  },
2661
2660
  else => unreachable,
2662
2661
  }
@@ -2680,40 +2679,40 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
2680
2679
  }
2681
2680
  }
2682
2681
 
2683
- inline fn isForced(active: anytype) bool {
2682
+ fn isForced(active: anytype) bool {
2684
2683
  return active.volatiles.Recharging or active.volatiles.Rage or
2685
2684
  active.volatiles.Thrashing or active.volatiles.Charging;
2686
2685
  }
2687
2686
 
2688
- fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !void {
2687
+ fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
2689
2688
  var log = options.log;
2690
2689
  var side = battle.side(who);
2691
2690
  var volatiles = &side.active.volatiles;
2692
2691
  const ident = battle.active(who);
2693
2692
 
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);
2693
+ if (volatiles.disable_move != 0) {
2694
+ volatiles.disable_move = 0;
2695
+ volatiles.disable_duration = 0;
2696
+ options.chance.observe(.disable, who, .None);
2697
+ try log.end(.{ ident, .DisableSilent });
2700
2698
  }
2701
2699
  if (volatiles.Confusion) {
2702
2700
  // volatiles.confusion is left unchanged
2703
2701
  volatiles.Confusion = false;
2704
- try log.end(ident, .ConfusionSilent);
2702
+ options.chance.observe(.confusion, who, .None);
2703
+ try log.end(.{ ident, .ConfusionSilent });
2705
2704
  }
2706
2705
  if (volatiles.Mist) {
2707
2706
  volatiles.Mist = false;
2708
- try log.end(ident, .Mist);
2707
+ try log.end(.{ ident, .MistSilent });
2709
2708
  }
2710
2709
  if (volatiles.FocusEnergy) {
2711
2710
  volatiles.FocusEnergy = false;
2712
- try log.end(ident, .FocusEnergy);
2711
+ try log.end(.{ ident, .FocusEnergySilent });
2713
2712
  }
2714
2713
  if (volatiles.LeechSeed) {
2715
2714
  volatiles.LeechSeed = false;
2716
- try log.end(ident, .LeechSeed);
2715
+ try log.end(.{ ident, .LeechSeedSilent });
2717
2716
  }
2718
2717
  if (!showdown and volatiles.Toxic) {
2719
2718
  volatiles.Toxic = false;
@@ -2721,24 +2720,39 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2721
2720
  }
2722
2721
  if (volatiles.LightScreen) {
2723
2722
  volatiles.LightScreen = false;
2724
- try log.end(ident, .LightScreen);
2723
+ try log.end(.{ ident, .LightScreenSilent });
2725
2724
  }
2726
2725
  if (volatiles.Reflect) {
2727
2726
  volatiles.Reflect = false;
2728
- try log.end(ident, .Reflect);
2727
+ try log.end(.{ ident, .ReflectSilent });
2729
2728
  }
2730
2729
  if (showdown and volatiles.Toxic) {
2731
2730
  volatiles.Toxic = false;
2732
2731
  // Pokémon Showdown erroneously clears the toxic counter
2733
2732
  volatiles.toxic = 0;
2734
- try log.end(ident, .Toxic);
2733
+ try log.end(.{ ident, .ToxicSilent });
2735
2734
  }
2736
2735
  }
2737
2736
 
2737
+ fn decrement(
2738
+ comptime field: chance.Action.Field,
2739
+ player: Player,
2740
+ options: anytype,
2741
+ n: anytype,
2742
+ ) @TypeOf(n) {
2743
+ return if (options.calc.overridden(player, field)) |obs| switch (obs) {
2744
+ .started, .ended => 0,
2745
+ .continuing => if (n > 1) n - 1 else n,
2746
+ else => unreachable,
2747
+ } else n - 1;
2748
+ }
2749
+
2738
2750
  pub const Rolls = struct {
2739
- inline fn speedTie(battle: anytype, options: anytype) !bool {
2751
+ fn speedTie(battle: anytype, options: anytype) !bool {
2740
2752
  const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
2741
2753
  player == .P1
2754
+ else if (@hasDecl(@TypeOf(battle.rng), "speedTie"))
2755
+ battle.rng.speedTie()
2742
2756
  else if (showdown)
2743
2757
  battle.rng.range(u8, 0, 2) == 0
2744
2758
  else
@@ -2748,21 +2762,31 @@ pub const Rolls = struct {
2748
2762
  return p1;
2749
2763
  }
2750
2764
 
2751
- inline fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2765
+ fn criticalHit(
2766
+ battle: anytype,
2767
+ player: Player,
2768
+ rate: u8,
2769
+ spurious: bool,
2770
+ options: anytype,
2771
+ ) !bool {
2752
2772
  const crit = if (options.calc.overridden(player, .critical_hit)) |val|
2753
2773
  val == .true
2774
+ else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
2775
+ battle.rng.criticalHit(player, rate)
2754
2776
  else if (showdown)
2755
2777
  battle.rng.chance(u8, rate, 256)
2756
2778
  else
2757
2779
  std.math.rotl(u8, battle.rng.next(), 3) < rate;
2758
2780
 
2759
- try options.chance.criticalHit(player, crit, rate);
2781
+ if (!spurious) try options.chance.criticalHit(player, crit, rate);
2760
2782
  return crit;
2761
2783
  }
2762
2784
 
2763
- inline fn damage(battle: anytype, player: anytype, options: anytype) !u8 {
2785
+ fn damage(battle: anytype, player: Player, options: anytype) !u8 {
2764
2786
  const roll = if (options.calc.overridden(player, .damage)) |val|
2765
2787
  val
2788
+ else if (@hasDecl(@TypeOf(battle.rng), "damage"))
2789
+ battle.rng.damage(player)
2766
2790
  else roll: {
2767
2791
  if (showdown) break :roll battle.rng.range(u8, 217, 256);
2768
2792
  while (true) {
@@ -2776,21 +2800,27 @@ pub const Rolls = struct {
2776
2800
  return roll;
2777
2801
  }
2778
2802
 
2779
- inline fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) bool {
2803
+ const uN = if (pkmn.options.miss) u8 else u9;
2804
+ fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) !bool {
2805
+ const acc: uN = if (pkmn.options.miss) accuracy else @as(uN, accuracy) + 1;
2780
2806
  const ok = if (options.calc.overridden(player, .hit)) |val|
2781
2807
  val == .true
2808
+ else if (@hasDecl(@TypeOf(battle.rng), "hit"))
2809
+ battle.rng.hit(player, acc)
2782
2810
  else if (showdown)
2783
- battle.rng.chance(u8, accuracy, 256)
2811
+ battle.rng.chance(uN, acc, 256)
2784
2812
  else
2785
- battle.rng.next() < accuracy;
2813
+ battle.rng.next() < acc;
2786
2814
 
2787
- options.chance.hit(ok, accuracy);
2815
+ try options.chance.hit(player, ok, acc);
2788
2816
  return ok;
2789
2817
  }
2790
2818
 
2791
- inline fn confused(battle: anytype, player: Player, options: anytype) !bool {
2819
+ fn confused(battle: anytype, player: Player, options: anytype) !bool {
2792
2820
  const cfz = if (options.calc.overridden(player, .confused)) |val|
2793
2821
  val == .true
2822
+ else if (@hasDecl(@TypeOf(battle.rng), "confused"))
2823
+ battle.rng.confused(player)
2794
2824
  else if (showdown)
2795
2825
  !battle.rng.chance(u8, 128, 256)
2796
2826
  else
@@ -2800,9 +2830,11 @@ pub const Rolls = struct {
2800
2830
  return cfz;
2801
2831
  }
2802
2832
 
2803
- inline fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2833
+ fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2804
2834
  const par = if (options.calc.overridden(player, .paralyzed)) |val|
2805
2835
  val == .true
2836
+ else if (@hasDecl(@TypeOf(battle.rng), "paralyzed"))
2837
+ battle.rng.paralyzed(player)
2806
2838
  else if (showdown)
2807
2839
  battle.rng.chance(u8, 63, 256)
2808
2840
  else
@@ -2812,9 +2844,11 @@ pub const Rolls = struct {
2812
2844
  return par;
2813
2845
  }
2814
2846
 
2815
- inline fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2847
+ fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2816
2848
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2817
2849
  val == .true
2850
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2851
+ battle.rng.secondaryChance(player, 25)
2818
2852
  else if (showdown)
2819
2853
  battle.rng.chance(u8, 25, 256)
2820
2854
  else
@@ -2824,11 +2858,13 @@ pub const Rolls = struct {
2824
2858
  return proc;
2825
2859
  }
2826
2860
 
2827
- inline fn secondaryChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2861
+ fn secondaryChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2828
2862
  const rate: u8 = if (low) 26 else 77;
2829
2863
 
2830
2864
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2831
2865
  val == .true
2866
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2867
+ battle.rng.secondaryChance(player, rate)
2832
2868
  else if (showdown)
2833
2869
  battle.rng.chance(u8, rate, 256)
2834
2870
  else
@@ -2838,11 +2874,13 @@ pub const Rolls = struct {
2838
2874
  return proc;
2839
2875
  }
2840
2876
 
2841
- inline fn poisonChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2877
+ fn poisonChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2842
2878
  const rate: u8 = if (low) 52 else 103;
2843
2879
 
2844
2880
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2845
2881
  val == .true
2882
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2883
+ battle.rng.secondaryChance(player, rate)
2846
2884
  else if (showdown)
2847
2885
  battle.rng.chance(u8, rate, 256)
2848
2886
  else
@@ -2852,9 +2890,11 @@ pub const Rolls = struct {
2852
2890
  return proc;
2853
2891
  }
2854
2892
 
2855
- inline fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2893
+ fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2856
2894
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2857
2895
  val == .true
2896
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2897
+ battle.rng.secondaryChance(player, 85)
2858
2898
  else if (showdown)
2859
2899
  battle.rng.chance(u8, 85, 256)
2860
2900
  else
@@ -2864,14 +2904,14 @@ pub const Rolls = struct {
2864
2904
  return proc;
2865
2905
  }
2866
2906
 
2867
- inline fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2907
+ fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2868
2908
  const move: Move = if (options.calc.overridden(player, .metronome)) |val|
2869
2909
  val
2870
- else if (showdown) move: {
2871
- const r = battle.rng.range(u8, 0, @intFromEnum(Move.Struggle) - 2);
2872
- const mod = @as(u2, (if (r < @intFromEnum(Move.Metronome) - 1) 1 else 2));
2873
- break :move @enumFromInt(r + mod);
2874
- } else move: {
2910
+ else if (@hasDecl(@TypeOf(battle.rng), "metronome"))
2911
+ battle.rng.metronome(player)
2912
+ else if (showdown)
2913
+ Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
2914
+ else move: {
2875
2915
  while (true) {
2876
2916
  const r = battle.rng.next();
2877
2917
  if (r == 0 or r == @intFromEnum(Move.Metronome)) continue;
@@ -2884,9 +2924,11 @@ pub const Rolls = struct {
2884
2924
  return move;
2885
2925
  }
2886
2926
 
2887
- inline fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2927
+ fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2888
2928
  const power = if (options.calc.overridden(player, .psywave)) |val|
2889
2929
  val - 1
2930
+ else if (@hasDecl(@TypeOf(battle.rng), "psywave"))
2931
+ battle.rng.psywave(player)
2890
2932
  else if (showdown)
2891
2933
  battle.rng.range(u8, 0, max)
2892
2934
  else power: {
@@ -2901,11 +2943,13 @@ pub const Rolls = struct {
2901
2943
  return power;
2902
2944
  }
2903
2945
 
2904
- inline fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2946
+ fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2905
2947
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2906
2948
  @intCast(val)
2949
+ else if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
2950
+ battle.rng.sleepDuration(player)
2907
2951
  else if (showdown)
2908
- @intCast(battle.rng.range(u8, 1, 8))
2952
+ battle.rng.range(u3, 1, 8)
2909
2953
  else duration: {
2910
2954
  while (true) {
2911
2955
  const r = battle.rng.next() & 7;
@@ -2914,50 +2958,58 @@ pub const Rolls = struct {
2914
2958
  };
2915
2959
 
2916
2960
  assert(duration >= 1 and duration <= 7);
2917
- options.chance.duration(.sleep, player, player.foe(), duration);
2961
+ options.chance.duration(player, duration);
2918
2962
  return duration;
2919
2963
  }
2920
2964
 
2921
- inline fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2965
+ fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2922
2966
  const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
2923
2967
  val
2968
+ else if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
2969
+ battle.rng.disableDuration(player)
2924
2970
  else if (showdown)
2925
- @intCast(battle.rng.range(u8, 1, 9))
2971
+ battle.rng.range(u4, 1, 9)
2926
2972
  else
2927
2973
  @intCast((battle.rng.next() & 7) + 1);
2928
2974
 
2929
2975
  assert(duration >= 1 and duration <= 8);
2930
- options.chance.duration(.disable, player, player.foe(), duration);
2976
+ options.chance.duration(player, duration);
2931
2977
  return duration;
2932
2978
  }
2933
2979
 
2934
- inline fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
2980
+ fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
2935
2981
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2936
2982
  @intCast(val)
2983
+ else if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
2984
+ battle.rng.confusionDuration(player)
2937
2985
  else if (showdown)
2938
- @intCast(battle.rng.range(u8, 2, 6))
2986
+ battle.rng.range(u3, 2, 6)
2939
2987
  else
2940
2988
  @intCast((battle.rng.next() & 3) + 2);
2941
2989
 
2942
2990
  assert(duration >= 2 and duration <= 5);
2943
- options.chance.duration(.confusion, player, if (self) player else player.foe(), duration);
2991
+ options.chance.duration(player, duration);
2944
2992
  return duration;
2945
2993
  }
2946
2994
 
2947
- inline fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2995
+ fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2948
2996
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2949
2997
  @intCast(val)
2998
+ else if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
2999
+ battle.rng.attackingDuration(player)
2950
3000
  else if (showdown)
2951
- @intCast(battle.rng.range(u4, 2, 4))
3001
+ battle.rng.range(u3, 2, 4)
2952
3002
  else
2953
3003
  @intCast((battle.rng.next() & 1) + 2);
2954
3004
 
2955
3005
  assert(duration >= 2 and duration <= 3);
2956
- options.chance.duration(.attacking, player, player, duration);
3006
+ options.chance.duration(player, duration);
2957
3007
  return duration;
2958
3008
  }
2959
3009
 
2960
- inline fn distribution(
3010
+ const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
3011
+
3012
+ fn distribution(
2961
3013
  battle: anytype,
2962
3014
  comptime effect: Move.Effect,
2963
3015
  player: Player,
@@ -2966,8 +3018,10 @@ pub const Rolls = struct {
2966
3018
  const roll = if (effect == .Binding) .duration else .multi_hit;
2967
3019
  const n: u3 = if (options.calc.overridden(player, roll)) |val|
2968
3020
  @intCast(val)
3021
+ else if (@hasDecl(@TypeOf(battle.rng), "distribution"))
3022
+ battle.rng.distribution(player)
2969
3023
  else if (showdown)
2970
- DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)]
3024
+ DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
2971
3025
  else n: {
2972
3026
  const r = (battle.rng.next() & 3);
2973
3027
  break :n @intCast((if (r < 2) r else battle.rng.next() & 3) + 2);
@@ -2975,7 +3029,7 @@ pub const Rolls = struct {
2975
3029
 
2976
3030
  assert(n >= 2 and n <= 5);
2977
3031
  if (effect == .Binding) {
2978
- options.chance.duration(.binding, player, player, n);
3032
+ options.chance.duration(player, n);
2979
3033
  } else {
2980
3034
  try options.chance.multiHit(player, n);
2981
3035
  }
@@ -2998,7 +3052,9 @@ pub const Rolls = struct {
2998
3052
  else
2999
3053
  null;
3000
3054
 
3001
- const slot: u4 = overridden orelse slot: {
3055
+ const slot: u4 = overridden orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
3056
+ battle.rng.moveSlot(player, moves, check_pp)
3057
+ else slot: {
3002
3058
  if (showdown) {
3003
3059
  if (check_pp == 0) {
3004
3060
  var i: usize = moves.len;
@@ -3014,10 +3070,9 @@ pub const Rolls = struct {
3014
3070
  while (i < moves.len and r > 0) : (i += 1) {
3015
3071
  if (moves[i].pp > 0) {
3016
3072
  r -= 1;
3017
- if (r == 0) break;
3073
+ if (r == 0) break :slot @intCast(i + 1);
3018
3074
  }
3019
3075
  }
3020
- break :slot @intCast(i + 1);
3021
3076
  }
3022
3077
  }
3023
3078
 
@@ -3039,11 +3094,11 @@ test "RNG agreement" {
3039
3094
  expected[i] = @intCast(i * 0x1000000);
3040
3095
  }
3041
3096
 
3042
- var spe = rng.FixedRNG(1, expected.len){ .rolls = expected };
3043
- var cfz = rng.FixedRNG(1, expected.len){ .rolls = expected };
3044
- var par = rng.FixedRNG(1, expected.len){ .rolls = expected };
3045
- var brn = rng.FixedRNG(1, expected.len){ .rolls = expected };
3046
- var eff = rng.FixedRNG(1, expected.len){ .rolls = expected };
3097
+ var spe: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3098
+ var cfz: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3099
+ var par: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3100
+ var brn: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3101
+ var eff: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3047
3102
 
3048
3103
  for (0..expected.len) |i| {
3049
3104
  try expectEqual(spe.range(u8, 0, 2) == 0, i < Gen12.percent(50) + 1);
@@ -3079,11 +3134,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3079
3134
  const side = battle.side(player);
3080
3135
  const foe = battle.foe(player);
3081
3136
 
3082
- var active = &side.active;
3137
+ const active = &side.active;
3083
3138
  const stored = side.stored();
3084
3139
 
3085
3140
  // While players are not given any input options on the cartridge in these cases,
3086
- // Pokémon Showdown instead produces a list with a single move that must be chosen.
3141
+ // Pokémon Showdown instead produces a list with a single move that must be chosen
3087
3142
  //
3088
3143
  // Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
3089
3144
  // here when not in compatibility mode, but the engine needs to be able to differentiate
@@ -3091,7 +3146,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3091
3146
  // passing due to being forced into using a move. Instead of introducing another option
3092
3147
  // we simply repurpose Move with no move slot, even though pedantically this is not
3093
3148
  // strictly correct as the player would not have been presented the option to move or
3094
- // switch at all.
3149
+ // switch at all
3095
3150
  if (isForced(active)) {
3096
3151
  out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
3097
3152
  n += 1;
@@ -3110,7 +3165,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3110
3165
  // On the cartridge, all of these happen after "FIGHT" (indicating you are not
3111
3166
  // switching) but before you are allowed to select a move. Pokémon Showdown instead
3112
3167
  // either disables all other moves in the case of limited or requires you to select a
3113
- // move normally if sleeping/frozen/bound.
3168
+ // move normally if sleeping/frozen/bound
3114
3169
  if (!showdown and (limited or foe.active.volatiles.Binding or
3115
3170
  Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
3116
3171
  {
@@ -3123,7 +3178,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3123
3178
  // Pokémon Showdown handles Bide and Binding moves by checking if the move in question
3124
3179
  // is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
3125
3180
  // Move will not result in forcing the subsequent use unless the user also had the
3126
- // proc-ed move in their moveset) and disabling all other moves.
3181
+ // proc-ed move in their moveset) and disabling all other moves
3127
3182
  if (limited) {
3128
3183
  assert(showdown);
3129
3184
  assert(side.last_selected_move != .None);
@@ -3133,7 +3188,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3133
3188
  if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
3134
3189
  // Pokémon Showdown displays Struggle if limited to Bide but unable to pick
3135
3190
  const struggle =
3136
- m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);
3191
+ m.id == .Bide and (m.pp == 0 or active.volatiles.disable_move == slot);
3137
3192
  const s = if (struggle) 0 else slot;
3138
3193
  out[n] = .{ .type = .Move, .data = s };
3139
3194
  n += 1;
@@ -3148,7 +3203,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3148
3203
  const m = active.moves[slot - 1];
3149
3204
  if (m.id == .None) break;
3150
3205
  if (m.pp == 0) continue;
3151
- if (active.volatiles.disabled_move == slot) continue;
3206
+ if (active.volatiles.disable_move == slot) continue;
3152
3207
  out[n] = .{ .type = .Move, .data = slot };
3153
3208
  n += 1;
3154
3209
  }