@pkmn/engine 0.1.0-dev.a02611a4 → 0.1.0-dev.a1fcc5d9

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 (76) hide show
  1. package/LICENSE +1 -3
  2. package/README.md +46 -19
  3. package/build/pkg/addon/browser.d.ts +3 -0
  4. package/build/pkg/addon/browser.js +43 -0
  5. package/build/pkg/addon/browser.js.map +1 -0
  6. package/build/pkg/addon/node.d.ts +3 -0
  7. package/build/pkg/addon/node.js +96 -0
  8. package/build/pkg/addon/node.js.map +1 -0
  9. package/build/pkg/addon.d.ts +2 -0
  10. package/build/pkg/addon.js +35 -58
  11. package/build/pkg/addon.js.map +1 -1
  12. package/build/pkg/data/ids.json +12 -12
  13. package/build/pkg/data/layout.json +2 -2
  14. package/build/pkg/data/protocol.json +69 -12
  15. package/build/pkg/data.d.ts +27 -5
  16. package/build/pkg/data.js.map +1 -1
  17. package/build/pkg/gen1.js +3 -3
  18. package/build/pkg/gen1.js.map +1 -1
  19. package/build/pkg/index.d.ts +24 -10
  20. package/build/pkg/index.js +15 -10
  21. package/build/pkg/index.js.map +1 -1
  22. package/build/pkg/protocol.d.ts +20 -1
  23. package/build/pkg/protocol.js +123 -102
  24. package/build/pkg/protocol.js.map +1 -1
  25. package/build/tools/debug.js +36 -68
  26. package/build/tools/debug.js.map +1 -1
  27. package/build/tools/display/debug.jsx +141 -0
  28. package/build/tools/display/debug.jsx.map +1 -0
  29. package/build/tools/display/index.js +132 -0
  30. package/build/tools/display/index.js.map +1 -0
  31. package/build/tools/display/ui.jsx +349 -0
  32. package/build/tools/display/ui.jsx.map +1 -0
  33. package/build/tools/display/util.js +18 -0
  34. package/build/tools/display/util.js.map +1 -0
  35. package/build.zig +227 -96
  36. package/package.json +68 -32
  37. package/src/bin/install-pkmn-engine +29 -19
  38. package/src/bin/pkmn-debug +1 -1
  39. package/src/lib/common/array.zig +77 -0
  40. package/src/lib/common/data.zig +12 -24
  41. package/src/lib/common/debug.zig +39 -8
  42. package/src/lib/common/js.zig +4 -2
  43. package/src/lib/common/optional.zig +21 -10
  44. package/src/lib/common/options.zig +4 -0
  45. package/src/lib/common/protocol.zig +774 -356
  46. package/src/lib/common/rational.zig +132 -36
  47. package/src/lib/common/rng.zig +22 -13
  48. package/src/lib/common/util.zig +50 -1
  49. package/src/lib/gen1/calc.zig +685 -132
  50. package/src/lib/gen1/chance.zig +408 -283
  51. package/src/lib/gen1/data/moves.zig +192 -180
  52. package/src/lib/gen1/data/species.zig +3 -3
  53. package/src/lib/gen1/data/types.zig +7 -7
  54. package/src/lib/gen1/data.zig +48 -59
  55. package/src/lib/gen1/helpers.zig +87 -552
  56. package/src/lib/gen1/mechanics.zig +600 -543
  57. package/src/lib/gen2/calc.zig +161 -0
  58. package/src/lib/gen2/chance.zig +902 -0
  59. package/src/lib/gen2/data/items.zig +12 -18
  60. package/src/lib/gen2/data/moves.zig +661 -580
  61. package/src/lib/gen2/data/species.zig +1 -1
  62. package/src/lib/gen2/data/types.zig +134 -31
  63. package/src/lib/gen2/data.zig +97 -76
  64. package/src/lib/gen2/generated.zig +806 -0
  65. package/src/lib/gen2/helpers.zig +45 -22
  66. package/src/lib/gen2/mechanics.zig +3296 -90
  67. package/src/lib/{binding/node.zig → node.zig} +10 -13
  68. package/src/lib/pkmn.zig +32 -15
  69. package/src/lib/{binding/wasm.zig → wasm.zig} +2 -1
  70. package/src/tools/display/dom.ts +96 -0
  71. package/src/tools/display/ui.css +359 -0
  72. package/build/test/display.js +0 -400
  73. package/build/test/display.js.map +0 -1
  74. package/src/lib/gen1/README.md +0 -892
  75. package/src/lib/gen2/README.md +0 -308
  76. package/src/test/display/pkmn.html.tmpl +0 -368
@@ -7,32 +7,30 @@ const DEBUG = @import("../common/debug.zig").print;
7
7
  const protocol = @import("../common/protocol.zig");
8
8
  const rng = @import("../common/rng.zig");
9
9
 
10
- const Action = @import("chance.zig").Action;
11
-
12
10
  const data = @import("data.zig");
11
+ const chance = @import("chance.zig");
13
12
 
14
13
  const assert = std.debug.assert;
15
14
 
16
15
  const expectEqual = std.testing.expectEqual;
17
16
 
17
+ const showdown = pkmn.options.showdown;
18
+
18
19
  const Choice = common.Choice;
19
20
  const ID = common.ID;
20
21
  const Player = common.Player;
21
22
  const Result = common.Result;
22
23
 
23
- const showdown = pkmn.options.showdown;
24
-
25
24
  const Boost = protocol.Boost;
26
25
  const Damage = protocol.Damage;
26
+ const Fail = protocol.Fail;
27
27
  const Heal = protocol.Heal;
28
28
 
29
29
  const Gen12 = rng.Gen12;
30
30
 
31
- const ActivePokemon = data.ActivePokemon;
32
31
  const Effectiveness = data.Effectiveness;
33
32
  const Move = data.Move;
34
33
  const MoveSlot = data.MoveSlot;
35
- const Pokemon = data.Pokemon;
36
34
  const Side = data.Side;
37
35
  const Species = data.Species;
38
36
  const Stats = data.Stats;
@@ -59,34 +57,26 @@ const BOOSTS = &[_][2]u8{
59
57
 
60
58
  const MAX_STAT_VALUE = 999;
61
59
 
62
- const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
63
-
64
60
  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
61
  assert(c1.type != .Pass or c2.type != .Pass or battle.turn == 0);
70
62
  if (battle.turn == 0) return start(battle, options);
71
63
 
72
64
  var s1 = false;
73
65
  var s2 = false;
74
- var f1: ?Move = null;
75
- var f2: ?Move = null;
76
66
 
77
- if (selectMove(battle, .P1, c1, c2, &s1, &f1)) |r| return r;
78
- if (selectMove(battle, .P2, c2, c1, &s2, &f2)) |r| return r;
67
+ if (selectMove(battle, .P1, c1, c2, &s1)) |r| return r;
68
+ if (selectMove(battle, .P2, c2, c1, &s2)) |r| return r;
79
69
 
80
70
  var p1 = battle.side(.P1);
81
71
  var p2 = battle.side(.P2);
82
72
 
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;
73
+ const r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
74
+ const r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
85
75
 
86
76
  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;
77
+ if (try doTurn(battle, .P1, c1, r1, s1, .P2, c2, r2, s2, options)) |r| return r;
88
78
  } else {
89
- if (try doTurn(battle, .P2, c2, r2, s2, f2, .P1, c1, r1, s1, f1, options)) |r| return r;
79
+ if (try doTurn(battle, .P2, c2, r2, s2, .P1, c1, r1, s1, options)) |r| return r;
90
80
  }
91
81
 
92
82
  if (p1.active.volatiles.attacks == 0) p1.active.volatiles.Binding = false;
@@ -99,11 +89,11 @@ fn start(battle: anytype, options: anytype) !Result {
99
89
  const p1 = battle.side(.P1);
100
90
  const p2 = battle.side(.P2);
101
91
 
102
- var p1_slot = findFirstAlive(p1);
92
+ const p1_slot = findFirstAlive(p1);
103
93
  assert(!showdown or p1_slot == 1);
104
94
  if (p1_slot == 0) return if (findFirstAlive(p2) == 0) Result.Tie else Result.Lose;
105
95
 
106
- var p2_slot = findFirstAlive(p2);
96
+ const p2_slot = findFirstAlive(p2);
107
97
  assert(!showdown or p2_slot == 1);
108
98
  if (p2_slot == 0) return Result.Win;
109
99
 
@@ -124,7 +114,6 @@ fn selectMove(
124
114
  choice: Choice,
125
115
  foe_choice: Choice,
126
116
  skip_turn: *bool,
127
- from: *?Move,
128
117
  ) ?Result {
129
118
  if (choice.type == .Pass) return null;
130
119
 
@@ -141,7 +130,6 @@ fn selectMove(
141
130
  return null;
142
131
  }
143
132
  if (volatiles.Rage) {
144
- from.* = side.last_used_move;
145
133
  if (showdown) {
146
134
  if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
147
135
  saveMove(battle, player, null);
@@ -151,7 +139,6 @@ fn selectMove(
151
139
  // Pokémon Showdown removes Flinch at the end-of-turn in its residual handler
152
140
  if (!showdown) volatiles.Flinch = false;
153
141
  if (volatiles.Thrashing or volatiles.Charging) {
154
- from.* = side.last_selected_move;
155
142
  if (showdown) {
156
143
  if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
157
144
  saveMove(battle, player, null);
@@ -172,15 +159,13 @@ fn selectMove(
172
159
  return null;
173
160
  }
174
161
  if (volatiles.Binding) {
175
- from.* = side.last_used_move;
176
162
  if (showdown) {
177
- if (foe_choice.type == .Switch) from.* = null;
178
163
  // Pokémon Showdown overwrites Mirror Move with whatever was selected - really this
179
164
  // should set side.last_selected_move = last.id to reuse Mirror Move and fail in order
180
165
  // to satisfy the conditions of the Desync Clause Mod. However, because Binding is still
181
166
  // set the selected move will not actually be used, it will just be reported as having
182
167
  // been used (this differs from how Pokémon Showdown works, but its impossible to
183
- // replicate the incorrect behavior with the correct mechanisms).
168
+ // replicate the incorrect behavior with the correct mechanisms)
184
169
  saveMove(battle, player, choice);
185
170
  } else {
186
171
  assert(choice.data == 0);
@@ -210,16 +195,13 @@ fn selectMove(
210
195
  if (choice.data == 0) {
211
196
  const struggle = ok: {
212
197
  for (side.active.moves, 0..) |move, i| {
213
- if (move.pp > 0 and volatiles.disabled_move != i + 1) break :ok false;
198
+ if (move.pp > 0 and volatiles.disable_move != i + 1) break :ok false;
214
199
  }
215
200
  break :ok true;
216
201
  };
217
-
218
202
  assert(struggle);
219
- saveMove(battle, player, choice);
220
- } else {
221
- saveMove(battle, player, choice);
222
203
  }
204
+ saveMove(battle, player, choice);
223
205
 
224
206
  return null;
225
207
  }
@@ -232,7 +214,7 @@ fn saveMove(battle: anytype, player: Player, choice: ?Choice) void {
232
214
  if (c.data == 0) {
233
215
  side.last_selected_move = .Struggle;
234
216
  } else {
235
- assert(showdown or side.active.volatiles.disabled_move != c.data);
217
+ assert(showdown or side.active.volatiles.disable_move != c.data);
236
218
  const move = side.active.move(c.data);
237
219
  // You cannot *select* a move with 0 PP (except on Pokémon Showdown where that is
238
220
  // sometimes required...), but a 0 PP move can be used automatically
@@ -270,17 +252,17 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: a
270
252
  active.moves = incoming.moves;
271
253
 
272
254
  statusModify(incoming.status, &active.stats);
255
+ options.chance.switched(player, slot);
273
256
 
274
257
  foe.active.volatiles.Binding = false;
275
258
 
276
- try options.log.switched(battle.active(player), incoming);
277
- options.chance.switched(player, side.order[0], out);
259
+ try options.log.switched(.{ battle.active(player), incoming });
278
260
 
279
261
  if (showdown and incoming.status == Status.TOX) {
280
262
  incoming.status = Status.init(.PSN);
281
263
  // Technically, Pokémon Showdown adds these after *both* Pokémon have switched, but we'd
282
264
  // rather not clutter up turnOrder just for this (incorrect) log message
283
- try options.log.status(battle.active(player), incoming.status, .Silent);
265
+ try options.log.status(.{ battle.active(player), incoming.status, .Silent });
284
266
  }
285
267
  }
286
268
 
@@ -343,12 +325,10 @@ fn doTurn(
343
325
  player_choice: Choice,
344
326
  player_rewrap: bool,
345
327
  player_skip: bool,
346
- player_from: ?Move,
347
328
  foe_player: Player,
348
329
  foe_choice: Choice,
349
330
  foe_rewrap: bool,
350
331
  foe_skip: bool,
351
- foe_from: ?Move,
352
332
  options: anytype,
353
333
  ) !?Result {
354
334
  assert(player_choice.type != .Pass);
@@ -361,19 +341,18 @@ fn doTurn(
361
341
  player_choice,
362
342
  player_rewrap,
363
343
  player_skip,
364
- player_from,
365
344
  &residual,
366
345
  options,
367
346
  )) |r| return r;
368
347
  if (!replace) {
369
348
  if (player_choice.type != .Switch) {
370
- if (try checkFaint(battle, foe_player, options)) |r| return r;
349
+ if (try checkFaint(battle, foe_player, true, options)) |r| return r;
371
350
  }
372
351
  if (residual) try handleResidual(battle, player, options);
373
- if (try checkFaint(battle, player, options)) |r| return r;
352
+ if (try checkFaint(battle, player, false, options)) |r| return r;
374
353
  } else if (foe_choice.type == .Pass) return null;
375
354
 
376
- options.chance.clearPending();
355
+ if (!showdown) options.chance.clearPending();
377
356
 
378
357
  residual = true;
379
358
  replace = battle.side(foe_player).stored().hp == 0;
@@ -384,16 +363,15 @@ fn doTurn(
384
363
  foe_choice,
385
364
  foe_rewrap,
386
365
  foe_skip,
387
- foe_from,
388
366
  &residual,
389
367
  options,
390
368
  )) |r| return r;
391
369
  if (!replace) {
392
370
  if (!calc and foe_choice.type != .Switch) {
393
- if (try checkFaint(battle, player, options)) |r| return r;
371
+ if (try checkFaint(battle, player, true, options)) |r| return r;
394
372
  }
395
373
  if (residual) try handleResidual(battle, foe_player, options);
396
- if (try checkFaint(battle, foe_player, options)) |r| return r;
374
+ if (try checkFaint(battle, foe_player, false, options)) |r| return r;
397
375
  }
398
376
 
399
377
  // Flinch is bugged on Pokémon Showdown because it gets implemented with a duration which causes
@@ -412,7 +390,6 @@ fn executeMove(
412
390
  choice: Choice,
413
391
  rewrap: bool,
414
392
  skip: bool,
415
- from: ?Move,
416
393
  residual: *bool,
417
394
  options: anytype,
418
395
  ) !?Result {
@@ -429,7 +406,7 @@ fn executeMove(
429
406
  if (side.last_selected_move == .SKIP_TURN) {
430
407
  assert(!showdown);
431
408
  if (battle.foe(player).active.volatiles.Binding) {
432
- try options.log.cant(battle.active(player), .Bound);
409
+ try options.log.cant(.{ battle.active(player), .Bound });
433
410
  }
434
411
  return null;
435
412
  }
@@ -447,11 +424,13 @@ fn executeMove(
447
424
  mslot = @intCast(battle.lastMove(player).index);
448
425
  const stored = side.stored();
449
426
  // 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);
427
+ auto = isForced(&side.active) or
428
+ side.active.volatiles.Binding or side.active.volatiles.Bide or
429
+ side.last_selected_move == .HyperBeam or
430
+ Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
452
431
  // If it wasn't Hyper Beam or the continuation of a move effect then we must have just
453
432
  // 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.
433
+ // index 1 and the current Pokémon has the same move in its first slot
455
434
  if (!auto) {
456
435
  // side.active.moves(slot) is safe to check even though the slot in question might not
457
436
  // technically be from this Pokémon because it must be exactly 1 to not desync and
@@ -470,7 +449,7 @@ fn executeMove(
470
449
 
471
450
  var skip_can = false;
472
451
  var skip_pp = false;
473
- switch (try beforeMove(battle, player, skip, from, residual, options)) {
452
+ switch (try beforeMove(battle, player, skip, residual, options)) {
474
453
  .done => return null,
475
454
  .skip_can => skip_can = true,
476
455
  .skip_pp => skip_pp = true,
@@ -478,9 +457,9 @@ fn executeMove(
478
457
  .err => return @as(?Result, Result.Error),
479
458
  }
480
459
 
481
- if (!skip_can and !try canMove(battle, player, mslot, auto, skip_pp, from, residual, options)) {
482
- return null;
483
- }
460
+ const can = skip_can or
461
+ try canMove(battle, player, mslot, auto, skip_pp, .None, residual, options);
462
+ if (!can) return null;
484
463
 
485
464
  return doMove(battle, player, mslot, rewrap, auto, residual, options);
486
465
  }
@@ -491,7 +470,6 @@ fn beforeMove(
491
470
  battle: anytype,
492
471
  player: Player,
493
472
  skip: bool,
494
- from: ?Move,
495
473
  residual: *bool,
496
474
  options: anytype,
497
475
  ) !BeforeMove {
@@ -505,36 +483,41 @@ fn beforeMove(
505
483
 
506
484
  if (Status.is(stored.status, .SLP)) {
507
485
  const before = stored.status;
486
+ const slf = Status.is(stored.status, .EXT);
508
487
  // 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;
488
+ if (options.calc.overridden(player, .sleep)) |obs| switch (obs) {
489
+ .started, .ended => stored.status = 0,
490
+ .continuing => if (Status.duration(stored.status) > 1) {
491
+ stored.status -= 1;
492
+ },
493
+ else => unreachable,
511
494
  } else {
512
495
  stored.status -= 1;
513
496
  }
514
497
 
515
498
  const duration = Status.duration(stored.status);
516
- if (!Status.is(stored.status, .EXT)) {
517
- try options.chance.sleep(player, duration);
518
- }
499
+ try options.chance.sleep(player, if (slf)
500
+ .None
501
+ else if (duration == 0) .ended else .continuing);
519
502
 
520
503
  if (duration == 0) {
521
- try log.curestatus(ident, before, .Message);
504
+ try log.curestatus(.{ ident, before, .Message });
522
505
  stored.status = 0; // clears EXT if present
523
506
  } else {
524
- try log.cant(ident, .Sleep);
507
+ try log.cant(.{ ident, .Sleep });
525
508
  }
526
509
  side.last_used_move = .None;
527
510
  return .done;
528
511
  }
529
512
 
530
513
  if (Status.is(stored.status, .FRZ)) {
531
- try log.cant(ident, .Freeze);
514
+ try log.cant(.{ ident, .Freeze });
532
515
  side.last_used_move = .None;
533
516
  return .done;
534
517
  }
535
518
 
536
519
  if (skip or foe.active.volatiles.Binding) {
537
- try log.cant(ident, .Bound);
520
+ try log.cant(.{ ident, .Bound });
538
521
  return .done;
539
522
  }
540
523
 
@@ -542,27 +525,27 @@ fn beforeMove(
542
525
  // Pokémon Showdown doesn't clear Flinch until its imaginary "residual" phase, meaning
543
526
  // Pokémon can sometimes flinch multiple times from the same original hit
544
527
  if (!showdown) volatiles.Flinch = false;
545
- try log.cant(ident, .Flinch);
528
+ try log.cant(.{ ident, .Flinch });
546
529
  return .done;
547
530
  }
548
531
 
549
532
  if (volatiles.Recharging) {
550
533
  volatiles.Recharging = false;
551
- try log.cant(ident, .Recharge);
534
+ try log.cant(.{ ident, .Recharge });
552
535
  return .done;
553
536
  }
554
537
 
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);
538
+ if (volatiles.disable_duration > 0) {
539
+ volatiles.disable_duration =
540
+ decrement(.disable, player, options, volatiles.disable_duration);
541
+ try options.chance.disable(
542
+ player,
543
+ if (volatiles.disable_duration == 0) .ended else .continuing,
544
+ );
562
545
 
563
- if (volatiles.disabled_duration == 0) {
564
- volatiles.disabled_move = 0;
565
- try log.end(ident, .Disable);
546
+ if (volatiles.disable_duration == 0) {
547
+ volatiles.disable_move = 0;
548
+ try log.end(.{ ident, .Disable });
566
549
  }
567
550
  }
568
551
  // Pokémon Showdown's disable condition has a single onBeforeMove handler
@@ -577,19 +560,24 @@ fn beforeMove(
577
560
 
578
561
  if (volatiles.Confusion) {
579
562
  assert(volatiles.confusion > 0);
580
-
581
- if (options.calc.modify(player, .confusion)) |extend| {
582
- if (!extend) volatiles.confusion = 0;
563
+ if (options.calc.overridden(player, .confusion)) |obs| switch (obs) {
564
+ .started, .ended => volatiles.confusion = 0,
565
+ .continuing => if (volatiles.confusion > 1) {
566
+ volatiles.confusion -= 1;
567
+ },
568
+ .overwritten => {},
569
+ else => unreachable,
583
570
  } else {
584
571
  volatiles.confusion -= 1;
585
572
  }
586
- try options.chance.confusion(player, volatiles.confusion);
573
+
574
+ try options.chance.confusion(player, if (volatiles.confusion == 0) .ended else .continuing);
587
575
 
588
576
  if (volatiles.confusion == 0) {
589
577
  volatiles.Confusion = false;
590
- try log.end(ident, .Confusion);
578
+ try log.end(.{ ident, .Confusion });
591
579
  } else {
592
- try log.activate(ident, .Confusion);
580
+ try log.activate(.{ ident, .Confusion });
593
581
 
594
582
  if (try Rolls.confused(battle, player, options)) {
595
583
  assert(!volatiles.MultiHit);
@@ -601,7 +589,9 @@ fn beforeMove(
601
589
  volatiles.Charging = false;
602
590
  volatiles.Binding = false;
603
591
  volatiles.Invulnerable = false;
604
- options.chance.clearDurations(player, null);
592
+
593
+ options.chance.observe(.attacking, player, .None);
594
+ options.chance.observe(.binding, player, .None);
605
595
  {
606
596
  // This feels (and is) disgusting but the cartridge literally just overwrites
607
597
  // the opponent's defense with the user's defense and resets it after. As a
@@ -609,7 +599,9 @@ fn beforeMove(
609
599
  const def = foe.active.stats.def;
610
600
  foe.active.stats.def = active.stats.def;
611
601
  defer foe.active.stats.def = def;
612
- if (!calcDamage(battle, player, player.foe(), null, false)) return .err;
602
+ if (!calcDamage(battle, player, player.foe(), null, false, options)) {
603
+ return .err;
604
+ }
613
605
  }
614
606
  // Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
615
607
  // on the targeting behavior of the confused Pokémon's selected move which results
@@ -638,9 +630,12 @@ fn beforeMove(
638
630
  volatiles.Thrashing = false;
639
631
  volatiles.Charging = false;
640
632
  volatiles.Binding = false;
641
- options.chance.clearDurations(player, null);
633
+
634
+ options.chance.observe(.attacking, player, .None);
635
+ options.chance.observe(.binding, player, .None);
636
+
642
637
  // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
643
- try log.cant(ident, .Paralysis);
638
+ try log.cant(.{ ident, .Paralysis });
644
639
  return .done;
645
640
  }
646
641
 
@@ -656,28 +651,22 @@ fn beforeMove(
656
651
  volatiles.state +%= battle.last_damage;
657
652
  }
658
653
 
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);
654
+ volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
655
+ try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
667
656
 
668
657
  if (volatiles.attacks != 0) {
669
- try log.activate(ident, .Bide);
658
+ try log.activate(.{ ident, .Bide });
670
659
  return .done;
671
660
  }
672
661
 
673
662
  volatiles.Bide = false;
674
- try log.end(ident, .Bide);
663
+ try log.end(.{ ident, .Bide });
675
664
 
676
665
  battle.last_damage = volatiles.state *% 2;
677
666
  volatiles.state = 0;
678
667
 
679
668
  if (battle.last_damage == 0) {
680
- try log.fail(ident, .None);
669
+ try log.fail(.{ ident, .None });
681
670
  return .done;
682
671
  }
683
672
 
@@ -697,47 +686,42 @@ fn beforeMove(
697
686
  }
698
687
 
699
688
  if (volatiles.Thrashing) {
700
- assert(volatiles.attacks > 0);
701
- assert(from != null);
689
+ try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
702
690
 
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);
691
+ volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
692
+ try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
709
693
 
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
- }
694
+ if (volatiles.attacks == 0) {
695
+ const overwritten = volatiles.Confusion;
696
+
697
+ volatiles.Thrashing = false;
698
+ volatiles.Confusion = true;
699
+ volatiles.confusion = Rolls.confusionDuration(battle, player, options);
700
+
701
+ // On Pokémon Showdown this could leak information to the opponent that thrashing has
702
+ // ended (the engine's protocol is always from the omniscient perspective and clients
703
+ // can be expected to recognize this pattern and pass the information only to the
704
+ // correct player)
705
+ try log.start(.{ battle.active(player), .ConfusionSilent });
706
+ options.chance.observe(.confusion, player, if (overwritten)
707
+ if (pkmn.options.overwrite) .overwritten else .continuing
708
+ else
709
+ .started);
710
+ if (!overwritten) options.calc.confusion(player);
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);
721
+ volatiles.attacks = decrement(.binding, player, options, volatiles.attacks);
722
+ try options.chance.binding(player, if (volatiles.attacks == 0) .ended else .continuing);
727
723
 
728
- if (options.calc.modify(player, .binding)) |extend| {
729
- if (!extend) volatiles.attacks = 0;
730
- } else {
731
- volatiles.attacks -= 1;
732
- }
733
- try options.chance.binding(player, volatiles.attacks);
734
-
735
- try log.move(
736
- battle.active(player),
737
- side.last_selected_move,
738
- battle.active(player.foe()),
739
- side.last_selected_move,
740
- );
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,33 +1034,31 @@ 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);
1040
- } else if (showdown) {
1037
+ try log.hitcount(.{ battle.active(player.foe()), hit });
1038
+ } else if (showdown and move.effect == .Binding) {
1041
1039
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
1042
- if (move.effect == .Binding) {
1043
- try Effects.binding(battle, player, rewrap, options);
1044
- if (immune) {
1045
- battle.last_damage = 0;
1046
- assert(foe.stored().hp > 0);
1047
- // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
1048
- if (sub) {
1049
- try log.activate(battle.active(player.foe()), .Substitute);
1050
- } else {
1051
- try log.damage(battle.active(player.foe()), foe.stored(), .None);
1052
- try buildRage(battle, player.foe(), options);
1053
- }
1054
- return null;
1040
+ try Effects.binding(battle, player, rewrap, options);
1041
+ if (immune) {
1042
+ battle.last_damage = 0;
1043
+ assert(foe.stored().hp > 0);
1044
+ // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
1045
+ if (sub) {
1046
+ try log.activate(.{ battle.active(player.foe()), .Substitute });
1047
+ } else {
1048
+ try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
1049
+ try buildRage(battle, player.foe(), options);
1055
1050
  }
1051
+ return null;
1056
1052
  }
1057
1053
  }
1058
1054
 
1059
1055
  // 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.
1056
+ // if an effect was intended to "always happen" it will still get skipped
1061
1057
  if (nullified) return null;
1062
1058
 
1063
1059
  // On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
1064
1060
  // 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.
1061
+ // setup before the loop means we can avoid having to waste time doing no-op handler searches
1066
1062
  if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
1067
1063
 
1068
1064
  if (foe.stored().hp == 0) return null;
@@ -1077,7 +1073,7 @@ fn doMove(
1077
1073
  // below). For Twineedle we change the data to that of one with PoisonChance1 given its
1078
1074
  // MultiHit behavior is complete after the loop above, though Pokémon Showdown handles the
1079
1075
  // Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
1080
- // puts the |-status| message before |-hitcount| instead of after.
1076
+ // puts the |-status| message before |-hitcount| instead of after
1081
1077
  if (move.effect == .Twineedle) {
1082
1078
  if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1083
1079
  } else if (move.effect == .Disable) {
@@ -1096,12 +1092,14 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
1096
1092
  const side = battle.side(player);
1097
1093
 
1098
1094
  // Base speed is used for the critical hit calculation, even when Transform-ed
1099
- var rate = @as(u16, Species.chance(side.stored().species));
1095
+ var rate: u16 = Species.chance(side.stored().species);
1100
1096
  // GLITCH: Focus Energy reduces critical hit rate instead of increasing it
1101
1097
  rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
1102
1098
  rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
1103
1099
 
1104
- return Rolls.criticalHit(battle, player, @intCast(rate), options);
1100
+ const spurious =
1101
+ !showdown and ((move.bp == 0 and move.effect != .OHKO) or move.target == .Depends);
1102
+ return Rolls.criticalHit(battle, player, @intCast(rate), spurious, options);
1105
1103
  }
1106
1104
 
1107
1105
  fn calcDamage(
@@ -1110,6 +1108,7 @@ fn calcDamage(
1110
1108
  target_player: Player,
1111
1109
  m: ?Move.Data,
1112
1110
  crit: bool,
1111
+ options: anytype,
1113
1112
  ) bool {
1114
1113
  // Confusion (indicated when m == null) just needs a 40 BP physical move
1115
1114
  const cfz = m == null;
@@ -1121,28 +1120,8 @@ fn calcDamage(
1121
1120
 
1122
1121
  const special = move.type.special();
1123
1122
 
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
1123
+ var atk = attack(side, crit, special);
1124
+ var def = defense(target, crit, special, cfz);
1146
1125
 
1147
1126
  // Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
1148
1127
  // will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
@@ -1153,9 +1132,9 @@ fn calcDamage(
1153
1132
  def = @max((def / 4) & 255, if (showdown) 1 else 0);
1154
1133
  }
1155
1134
 
1156
- const lvl = @as(u32, side.stored().level * @as(u2, if (crit) 2 else 1));
1135
+ const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
1157
1136
 
1158
- def = @as(u32, if (move.effect == .Explode) @max(def / 2, 1) else def);
1137
+ def = if (move.effect == .Explode) @max(def / 2, 1) else def;
1159
1138
 
1160
1139
  if (def == 0) return false;
1161
1140
 
@@ -1167,11 +1146,41 @@ fn calcDamage(
1167
1146
  d = @min(997, d);
1168
1147
  d += 2;
1169
1148
 
1149
+ // Whether or not a critical hit occurred can change whether a division-by-zero freeze is
1150
+ // encountered so we need to make sure that in the event of a miss we commit the status of the
1151
+ // crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
1152
+ // that results in counter intuitive probabilities in the common case
1153
+ if (pkmn.options.chance and !showdown) {
1154
+ atk = attack(side, !crit, special);
1155
+ def = defense(target, !crit, special, false);
1156
+ if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
1157
+ }
1158
+
1170
1159
  battle.last_damage = @intCast(d);
1171
1160
 
1172
1161
  return true;
1173
1162
  }
1174
1163
 
1164
+ fn attack(side: *Side, crit: bool, special: bool) u32 {
1165
+ return if (crit)
1166
+ if (special) side.stored().stats.spc else side.stored().stats.atk
1167
+ else if (special) side.active.stats.spc else side.active.stats.atk;
1168
+ }
1169
+
1170
+ fn defense(target: *Side, crit: bool, special: bool, cfz: bool) u32 {
1171
+ return if (crit)
1172
+ if (special) target.stored().stats.spc else target.stored().stats.def
1173
+ else
1174
+ // GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
1175
+ if (special)
1176
+ target.active.stats.spc *
1177
+ @as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
1178
+ else
1179
+ // Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
1180
+ target.active.stats.def *
1181
+ @as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
1182
+ }
1183
+
1175
1184
  fn adjustDamage(battle: anytype, player: Player) u16 {
1176
1185
  const side = battle.side(player);
1177
1186
  const foe = battle.foe(player);
@@ -1216,7 +1225,6 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1216
1225
  const foe = battle.foe(player);
1217
1226
 
1218
1227
  if (!try checkHit(battle, player, move, options)) return null;
1219
- try options.chance.commit(player, .hit);
1220
1228
 
1221
1229
  battle.last_damage = switch (side.last_selected_move) {
1222
1230
  .SuperFang => @max(foe.stored().hp / 2, 1),
@@ -1246,7 +1254,7 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1246
1254
  const foe = battle.foe(player);
1247
1255
 
1248
1256
  if (battle.last_damage == 0) {
1249
- try options.log.fail(battle.active(player), .None);
1257
+ try options.log.fail(.{ battle.active(player), .None });
1250
1258
  return null;
1251
1259
  }
1252
1260
 
@@ -1264,22 +1272,21 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1264
1272
  foe_last_selected_move.type == .Fighting);
1265
1273
 
1266
1274
  if (!used and !selected) {
1267
- try options.log.fail(battle.active(player), .None);
1275
+ try options.log.fail(.{ battle.active(player), .None });
1268
1276
  return null;
1269
1277
  }
1270
1278
 
1271
1279
  if (!used or !selected) {
1272
1280
  // GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
1273
1281
  if (!showdown) return Result.Error;
1274
- try options.log.fail(battle.active(player), .None);
1282
+ try options.log.fail(.{ battle.active(player), .None });
1275
1283
  return null;
1276
1284
  }
1277
1285
 
1278
1286
  battle.last_damage *|= 2;
1279
1287
 
1280
- // Pokémon Showdown calls checkHit before Counter
1288
+ // Pokémon Showdown calls moveHit before Counter
1281
1289
  if (!showdown and !try checkHit(battle, player, move, options)) return null;
1282
- try options.chance.commit(player, .hit);
1283
1290
 
1284
1291
  const sub = showdown and foe.active.volatiles.Substitute;
1285
1292
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -1307,20 +1314,20 @@ fn applyDamage(
1307
1314
  subbed.active.volatiles.substitute = 0;
1308
1315
  subbed.active.volatiles.Substitute = false;
1309
1316
  // 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);
1317
+ try options.log.end(.{ battle.active(sub_player), .Substitute });
1318
+ if (reason != .Confusion) options.calc.capped(sub_player);
1312
1319
  return true;
1313
1320
  } else {
1314
1321
  // Safe to truncate since less than subbed.volatiles.substitute which is a u8
1315
1322
  subbed.active.volatiles.substitute -= @intCast(battle.last_damage);
1316
- try options.log.activate(battle.active(sub_player), .Substitute);
1323
+ try options.log.activate(.{ battle.active(sub_player), .Substitute });
1317
1324
  return false;
1318
1325
  }
1319
1326
  }
1320
1327
 
1321
1328
  if (battle.last_damage > target.stored().hp) battle.last_damage = target.stored().hp;
1322
1329
  target.stored().hp -= battle.last_damage;
1323
- try options.log.damage(battle.active(target_player), target.stored(), reason);
1330
+ try options.log.damage(.{ battle.active(target_player), target.stored(), reason });
1324
1331
  return false;
1325
1332
  }
1326
1333
 
@@ -1339,10 +1346,10 @@ fn mirrorMove(
1339
1346
  side.last_selected_move = foe.last_used_move;
1340
1347
 
1341
1348
  if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
1342
- try options.log.fail(battle.active(player), .None);
1349
+ try options.log.fail(.{ battle.active(player), .None });
1343
1350
  return null;
1344
1351
  } else if (!showdown or foe.last_used_move != .Struggle) {
1345
- incrementPP(side, mslot);
1352
+ incrementPP(side, player, mslot, options);
1346
1353
  }
1347
1354
 
1348
1355
  if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
@@ -1363,7 +1370,7 @@ fn metronome(
1363
1370
  var side = battle.side(player);
1364
1371
 
1365
1372
  side.last_selected_move = try Rolls.metronome(battle, player, options);
1366
- incrementPP(side, mslot);
1373
+ incrementPP(side, player, mslot, options);
1367
1374
 
1368
1375
  if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
1369
1376
  return null;
@@ -1375,20 +1382,23 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
1375
1382
  var immune = false;
1376
1383
  var mist = false;
1377
1384
 
1378
- if (moveHit(battle, player, move, &immune, &mist, options)) return true;
1385
+ if (try moveHit(battle, player, move, &immune, &mist, options)) {
1386
+ if (!showdown) try options.chance.commit(player, .hit);
1387
+ return true;
1388
+ }
1379
1389
 
1380
1390
  assert(!immune);
1381
1391
  if (mist) {
1382
1392
  assert(!showdown);
1383
1393
  const foe_ident = battle.active(player.foe());
1384
- try options.log.activate(foe_ident, .Mist);
1385
- try options.log.fail(foe_ident, .None);
1394
+ try options.log.activate(.{ foe_ident, .Mist });
1395
+ try options.log.fail(.{ foe_ident, .None });
1386
1396
  } else {
1387
- try options.chance.commit(player, .miss);
1388
- try options.log.lastmiss();
1389
- try options.log.miss(battle.active(player));
1397
+ try options.log.lastmiss(.{});
1398
+ try options.log.miss(.{battle.active(player)});
1390
1399
  }
1391
1400
 
1401
+ if (!showdown) try options.chance.commit(player, .miss);
1392
1402
  return false;
1393
1403
  }
1394
1404
 
@@ -1399,7 +1409,7 @@ fn moveHit(
1399
1409
  immune: *bool,
1400
1410
  mist: *bool,
1401
1411
  options: anytype,
1402
- ) bool {
1412
+ ) !bool {
1403
1413
  var side = battle.side(player);
1404
1414
  const foe = battle.foe(player);
1405
1415
 
@@ -1438,7 +1448,7 @@ fn moveHit(
1438
1448
  const overwritten = overwrite and state > 0;
1439
1449
  assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
1440
1450
 
1441
- var accuracy = if (overwritten) state else @as(u16, Gen12.percent(move.accuracy));
1451
+ var accuracy: u16 = if (overwritten) state else move.accuracy;
1442
1452
  var boost = BOOSTS[@as(u4, @intCast(@as(i8, side.active.boosts.accuracy) + 6))];
1443
1453
  accuracy = accuracy * boost[0] / boost[1];
1444
1454
  boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
@@ -1450,7 +1460,7 @@ fn moveHit(
1450
1460
  if (overwrite) side.active.volatiles.state = accuracy;
1451
1461
 
1452
1462
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1453
- break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
1463
+ break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
1454
1464
  };
1455
1465
 
1456
1466
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1461,13 +1471,17 @@ fn moveHit(
1461
1471
 
1462
1472
  if (!miss) return true;
1463
1473
  if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
1474
+
1464
1475
  side.active.volatiles.Binding = false;
1476
+ options.chance.observe(.binding, player, .None);
1477
+
1465
1478
  return false;
1466
1479
  }
1467
1480
 
1468
1481
  fn checkFaint(
1469
1482
  battle: anytype,
1470
1483
  player: Player,
1484
+ cap: bool,
1471
1485
  options: anytype,
1472
1486
  ) @TypeOf(options.log).Error!?Result {
1473
1487
  const side = battle.side(player);
@@ -1477,33 +1491,32 @@ fn checkFaint(
1477
1491
  const foe_fainted = foe.stored().hp == 0;
1478
1492
 
1479
1493
  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;
1494
+ const foe_out = foe_fainted and findFirstAlive(foe) == 0;
1495
+ const more = player_out or foe_out;
1483
1496
 
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;
1497
+ if (try faint(battle, player, !(more or foe_fainted), cap, options)) |r| return r;
1498
+ if (foe_fainted) if (try faint(battle, player.foe(), !more, false, options)) |r| return r;
1486
1499
 
1487
1500
  assert(!side.active.volatiles.MultiHit);
1488
1501
  assert(!foe.active.volatiles.MultiHit);
1489
1502
 
1490
- if (tie) {
1491
- try options.log.tie();
1503
+ if (player_out and foe_out) {
1504
+ try options.log.tie(.{});
1492
1505
  return Result.Tie;
1493
1506
  } else if (player_out) {
1494
- try options.log.win(player.foe());
1507
+ try options.log.win(.{player.foe()});
1495
1508
  return if (player == .P1) Result.Lose else Result.Win;
1496
1509
  } else if (foe_out) {
1497
- try options.log.win(player);
1510
+ try options.log.win(.{player});
1498
1511
  return if (player == .P1) Result.Win else Result.Lose;
1499
1512
  }
1500
1513
 
1501
1514
  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 };
1515
+ if (player == .P1) return .{ .p1 = .Switch, .p2 = foe_choice };
1516
+ return .{ .p1 = foe_choice, .p2 = .Switch };
1504
1517
  }
1505
1518
 
1506
- fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
1519
+ fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytype) !?Result {
1507
1520
  var side = battle.side(player);
1508
1521
  var foe = battle.foe(player);
1509
1522
  assert(side.stored().hp == 0);
@@ -1536,8 +1549,10 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
1536
1549
  side.stored().stats.spe;
1537
1550
  }
1538
1551
 
1539
- try options.log.faint(battle.active(player), done);
1540
- options.calc.capped(player);
1552
+ try options.log.faint(.{ battle.active(player), done });
1553
+ // We can only safely set capped if the faint occured as the direct result of an opponent's
1554
+ // move (as opposed to due to residual damage, recoil, Explosion, etc)
1555
+ if (cap) options.calc.capped(player);
1541
1556
  return null;
1542
1557
  }
1543
1558
 
@@ -1548,21 +1563,19 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1548
1563
  var volatiles = &side.active.volatiles;
1549
1564
 
1550
1565
  const brn = Status.is(stored.status, .BRN);
1551
- if (brn or Status.is(stored.status, .PSN)) blk: {
1566
+ if (brn or Status.is(stored.status, .PSN)) {
1552
1567
  var damage = @max(stored.stats.hp / 16, 1);
1553
-
1554
1568
  if (volatiles.Toxic) {
1555
1569
  volatiles.toxic += 1;
1556
1570
  damage *= volatiles.toxic;
1557
1571
  }
1572
+ damage = @min(damage, stored.hp);
1558
1573
 
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);
1574
+ if (!showdown or damage > 0) {
1575
+ stored.hp -= damage;
1576
+ // Pokémon Showdown uses damage [of] here but its not relevant in Generation I
1577
+ try options.log.damage(.{ ident, stored, if (brn) Damage.Burn else Damage.Poison });
1578
+ }
1566
1579
  }
1567
1580
 
1568
1581
  if (volatiles.LeechSeed) {
@@ -1586,14 +1599,14 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1586
1599
  const amount = @min(damage, stored.hp);
1587
1600
  stored.hp -= amount;
1588
1601
 
1589
- // As above, Pokémon Showdown uses damageOf but its not relevant
1590
- if (amount > 0) try options.log.damage(ident, stored, .LeechSeed);
1602
+ // As above, Pokémon Showdown uses damage [of] but its not relevant
1603
+ if (amount > 0) try options.log.damage(.{ ident, stored, .LeechSeed });
1591
1604
 
1592
1605
  const before = foe_stored.hp;
1593
1606
  // Uncapped damage is added back to the foe
1594
1607
  foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
1595
1608
  // 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);
1609
+ if (foe_stored.hp > before) try options.log.heal(.{ foe_ident, foe_stored, .Silent });
1597
1610
  }
1598
1611
  }
1599
1612
 
@@ -1604,18 +1617,18 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
1604
1617
  battle.turn += 1;
1605
1618
 
1606
1619
  if (showdown and pkmn.options.ebc and checkEBC(battle)) {
1607
- try options.log.tie();
1620
+ try options.log.tie(.{});
1608
1621
  return Result.Tie;
1609
1622
  }
1610
1623
 
1611
1624
  if (showdown and battle.turn >= 1000) {
1612
- try options.log.tie();
1625
+ try options.log.tie(.{});
1613
1626
  return Result.Tie;
1614
1627
  } else if (battle.turn >= 65535) {
1615
1628
  return Result.Error;
1616
1629
  }
1617
1630
 
1618
- try options.log.turn(battle.turn);
1631
+ try options.log.turn(.{battle.turn});
1619
1632
 
1620
1633
  return Result.Default;
1621
1634
  }
@@ -1675,9 +1688,9 @@ fn checkEBC(battle: anytype) bool {
1675
1688
  }
1676
1689
 
1677
1690
  fn disabled(side: *Side, ident: ID, options: anytype) !bool {
1678
- if (side.active.volatiles.disabled_move != 0) {
1691
+ if (side.active.volatiles.disable_move != 0) {
1679
1692
  // 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;
1693
+ const m = side.active.moves[side.active.volatiles.disable_move - 1].id;
1681
1694
  // side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
1682
1695
  const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
1683
1696
  m
@@ -1685,32 +1698,21 @@ fn disabled(side: *Side, ident: ID, options: anytype) !bool {
1685
1698
  side.last_selected_move;
1686
1699
  if (m != .None and m == last) {
1687
1700
  side.active.volatiles.Charging = false;
1688
- try options.log.disabled(ident, last);
1701
+ try options.log.cant(.{ ident, .Disable, last });
1689
1702
  return true;
1690
1703
  }
1691
1704
  }
1692
1705
  return false;
1693
1706
  }
1694
1707
 
1695
- inline fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1708
+ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1696
1709
  const side = battle.side(who);
1697
1710
  if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
1698
1711
  try Effects.boost(battle, who, Move.get(.Rage), options);
1699
1712
  }
1700
1713
  }
1701
1714
 
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(
1715
+ fn onBegin(
1714
1716
  battle: anytype,
1715
1717
  player: Player,
1716
1718
  move: Move.Data,
@@ -1718,7 +1720,6 @@ inline fn onBegin(
1718
1720
  residual: *bool,
1719
1721
  options: anytype,
1720
1722
  ) !void {
1721
- assert(move.effect.onBegin());
1722
1723
  return switch (move.effect) {
1723
1724
  .Confusion => Effects.confusion(battle, player, move, options),
1724
1725
  .Conversion => Effects.conversion(battle, player, options),
@@ -1740,8 +1741,7 @@ inline fn onBegin(
1740
1741
  };
1741
1742
  }
1742
1743
 
1743
- inline fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1744
- assert(move.effect.onEnd());
1744
+ fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1745
1745
  return switch (move.effect) {
1746
1746
  .Bide => Effects.bide(battle, player, options),
1747
1747
  // zig fmt: off
@@ -1756,14 +1756,13 @@ inline fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anyty
1756
1756
  };
1757
1757
  }
1758
1758
 
1759
- inline fn alwaysHappens(
1759
+ fn alwaysHappens(
1760
1760
  battle: anytype,
1761
1761
  player: Player,
1762
1762
  move: Move.Data,
1763
1763
  residual: *bool,
1764
1764
  options: anytype,
1765
1765
  ) !void {
1766
- assert(move.effect.alwaysHappens());
1767
1766
  return switch (move.effect) {
1768
1767
  .DrainHP, .DreamEater => Effects.drainHP(battle, player, options),
1769
1768
  .Explode => Effects.explode(battle, player),
@@ -1775,7 +1774,7 @@ inline fn alwaysHappens(
1775
1774
  };
1776
1775
  }
1777
1776
 
1778
- inline fn moveEffect(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1777
+ fn moveEffect(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1779
1778
  return switch (move.effect) {
1780
1779
  .BurnChance1, .BurnChance2 => Effects.burnChance(battle, player, move, options),
1781
1780
  .ConfusionChance => Effects.confusion(battle, player, move, options),
@@ -1799,10 +1798,12 @@ pub const Effects = struct {
1799
1798
 
1800
1799
  side.active.volatiles.Bide = true;
1801
1800
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1801
+
1802
1802
  side.active.volatiles.state = 0;
1803
1803
  side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
1804
+ options.chance.observe(.attacking, player, .started);
1804
1805
 
1805
- try options.log.start(battle.active(player), .Bide);
1806
+ try options.log.start(.{ battle.active(player), .Bide });
1806
1807
  }
1807
1808
 
1808
1809
  fn burnChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1816,11 +1817,11 @@ pub const Effects = struct {
1816
1817
  // GLITCH: Freeze top move selection desync can occur if thawed Pokémon is slower
1817
1818
  if (Status.is(foe_stored.status, .FRZ)) {
1818
1819
  assert(move.type == .Fire);
1819
- try options.log.curestatus(
1820
+ try options.log.curestatus(.{
1820
1821
  battle.active(player.foe()),
1821
1822
  foe_stored.status,
1822
1823
  .Message,
1823
- );
1824
+ });
1824
1825
  foe_stored.status = 0;
1825
1826
  }
1826
1827
  return;
@@ -1834,7 +1835,7 @@ pub const Effects = struct {
1834
1835
  foe_stored.status = Status.init(.BRN);
1835
1836
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
1836
1837
 
1837
- try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
1838
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
1838
1839
  }
1839
1840
 
1840
1841
  fn charge(battle: anytype, player: Player, options: anytype) !void {
@@ -1844,8 +1845,8 @@ pub const Effects = struct {
1844
1845
  volatiles.Charging = true;
1845
1846
  const move = side.last_selected_move;
1846
1847
  if (move == .Fly or move == .Dig) volatiles.Invulnerable = true;
1847
- try options.log.laststill();
1848
- try options.log.prepare(battle.active(player), move);
1848
+ try options.log.laststill(.{});
1849
+ try options.log.prepare(.{ battle.active(player), move });
1849
1850
  }
1850
1851
 
1851
1852
  fn confusion(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1856,42 +1857,39 @@ pub const Effects = struct {
1856
1857
  if (!try Rolls.confusionChance(battle, player, options)) return;
1857
1858
  } else {
1858
1859
  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
- }
1860
+ if (!try checkHit(battle, player, move, options)) return;
1861
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1864
1862
  } 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
- }
1863
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1864
+ if (!try checkHit(battle, player, move, options)) return;
1870
1865
  }
1871
- try options.chance.commit(player, .hit);
1872
1866
  }
1873
1867
 
1874
1868
  if (foe.active.volatiles.Confusion) return;
1875
1869
  foe.active.volatiles.Confusion = true;
1876
- foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
1877
1870
 
1878
- try options.log.start(battle.active(player.foe()), .Confusion);
1871
+ foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
1872
+ options.chance.observe(.confusion, player.foe(), .started);
1873
+ options.calc.confusion(player.foe());
1874
+
1875
+ try options.log.start(.{ battle.active(player.foe()), .Confusion });
1879
1876
  }
1880
1877
 
1881
1878
  fn conversion(battle: anytype, player: Player, options: anytype) !void {
1882
1879
  const foe = battle.foe(player);
1883
1880
 
1884
1881
  if (foe.active.volatiles.Invulnerable) {
1885
- try options.log.lastmiss();
1886
- return options.log.miss(battle.active(player));
1882
+ try options.log.lastmiss(.{});
1883
+ return options.log.miss(.{battle.active(player)});
1887
1884
  }
1888
1885
 
1889
1886
  battle.side(player).active.types = foe.active.types;
1890
- return options.log.typechange(
1887
+ return options.log.start(.{
1891
1888
  battle.active(player),
1889
+ .TypeChange,
1892
1890
  foe.active.types,
1893
1891
  battle.active(player.foe()),
1894
- );
1892
+ });
1895
1893
  }
1896
1894
 
1897
1895
  fn disable(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
@@ -1900,13 +1898,10 @@ pub const Effects = struct {
1900
1898
  const foe_ident = battle.active(player.foe());
1901
1899
 
1902
1900
  // 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
- }
1901
+ if (!showdown and !try checkHit(battle, player, move, options)) return null;
1907
1902
 
1908
- if (volatiles.disabled_move != 0) {
1909
- try options.log.fail(foe_ident, .None);
1903
+ if (volatiles.disable_move != 0) {
1904
+ try options.log.fail(.{ foe_ident, .None });
1910
1905
  return null;
1911
1906
  }
1912
1907
 
@@ -1922,7 +1917,7 @@ pub const Effects = struct {
1922
1917
  // Technically this is still considered simply a "miss" on the cartridge,
1923
1918
  // but diverging from Pokémon Showdown here would mostly just be pedantic
1924
1919
  if (n == 0) {
1925
- try options.log.fail(foe_ident, .None);
1920
+ try options.log.fail(.{ foe_ident, .None });
1926
1921
  return null;
1927
1922
  } else if (err) {
1928
1923
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
@@ -1930,18 +1925,19 @@ pub const Effects = struct {
1930
1925
  return Result.Error;
1931
1926
  }
1932
1927
 
1933
- volatiles.disabled_move =
1928
+ volatiles.disable_move =
1934
1929
  @intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
1935
- volatiles.disabled_duration = Rolls.disableDuration(battle, player, options);
1930
+ volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
1931
+ options.chance.observe(.disable, player.foe(), .started);
1936
1932
 
1937
- const id = foe.active.move(volatiles.disabled_move).id;
1938
- try options.log.startEffect(foe_ident, .Disable, id);
1933
+ const id = foe.active.move(volatiles.disable_move).id;
1934
+ try options.log.start(.{ foe_ident, .Disable, id });
1939
1935
  return null;
1940
1936
  }
1941
1937
 
1942
1938
  fn drainHP(battle: anytype, player: Player, options: anytype) !void {
1943
- var side = battle.side(player);
1944
- var stored = side.stored();
1939
+ var stored = battle.side(player).stored();
1940
+ const ident = battle.active(player);
1945
1941
 
1946
1942
  if (battle.last_damage == 0) {
1947
1943
  assert(showdown);
@@ -1954,7 +1950,7 @@ pub const Effects = struct {
1954
1950
  if (stored.hp == stored.stats.hp) return;
1955
1951
  stored.hp = @min(stored.stats.hp, stored.hp + drain);
1956
1952
 
1957
- try options.log.drain(battle.active(player), stored, battle.active(player.foe()));
1953
+ try options.log.heal(.{ ident, stored, .Drain, battle.active(player.foe()) });
1958
1954
  }
1959
1955
 
1960
1956
  fn explode(battle: anytype, player: Player) !void {
@@ -1986,7 +1982,7 @@ pub const Effects = struct {
1986
1982
  if (side.active.volatiles.FocusEnergy) return;
1987
1983
  side.active.volatiles.FocusEnergy = true;
1988
1984
 
1989
- try options.log.start(battle.active(player), .FocusEnergy);
1985
+ try options.log.start(.{ battle.active(player), .FocusEnergy });
1990
1986
  }
1991
1987
 
1992
1988
  fn freezeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2008,7 +2004,7 @@ pub const Effects = struct {
2008
2004
  foe_stored.status = Status.init(.FRZ);
2009
2005
  // GLITCH: Hyper Beam recharging status is not cleared
2010
2006
 
2011
- try options.log.status(foe_ident, foe_stored.status, .None);
2007
+ try options.log.status(.{ foe_ident, foe_stored.status, .None });
2012
2008
  }
2013
2009
 
2014
2010
  fn haze(battle: anytype, player: Player, options: anytype) !void {
@@ -2016,7 +2012,7 @@ pub const Effects = struct {
2016
2012
  var side = battle.side(player);
2017
2013
  var foe = battle.foe(player);
2018
2014
 
2019
- var side_stored = side.stored();
2015
+ const side_stored = side.stored();
2020
2016
  var foe_stored = foe.stored();
2021
2017
 
2022
2018
  const player_ident = battle.active(player);
@@ -2028,8 +2024,8 @@ pub const Effects = struct {
2028
2024
  side.active.stats = side_stored.stats;
2029
2025
  foe.active.stats = foe_stored.stats;
2030
2026
 
2031
- try log.activate(player_ident, .Haze);
2032
- try log.clearallboost();
2027
+ try log.activate(.{ player_ident, .Haze });
2028
+ try log.clearallboost(.{});
2033
2029
 
2034
2030
  // Pokémon Showdown clears P1 then P2 instead of status -> side -> foe
2035
2031
  if (showdown) {
@@ -2037,24 +2033,26 @@ pub const Effects = struct {
2037
2033
  const p: Player = @enumFromInt(i);
2038
2034
  // Pokémon Showdown incorrectly does not prevent sleep/freeze from moving
2039
2035
  if (p != player and Status.any(s.stored().status)) {
2040
- try log.curestatus(foe_ident, foe_stored.status, .Silent);
2036
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2041
2037
  s.stored().status = 0;
2038
+ options.chance.observe(.sleep, p, .None);
2042
2039
  } else if (showdown and s.stored().status == Status.TOX) {
2043
2040
  s.stored().status = Status.init(.PSN);
2044
- try log.status(battle.active(p), s.stored().status, .None);
2041
+ try log.status(.{ battle.active(p), s.stored().status, .None });
2045
2042
  }
2046
- try clearVolatiles(battle, p, p != player, options);
2043
+ try clearVolatiles(battle, p, options);
2047
2044
  }
2048
2045
  } else {
2049
2046
  if (Status.any(foe_stored.status)) {
2050
2047
  if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
2051
2048
  foe.last_selected_move = .SKIP_TURN;
2049
+ options.chance.observe(.sleep, player.foe(), .None);
2052
2050
  }
2053
- try log.curestatus(foe_ident, foe_stored.status, .Silent);
2051
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2054
2052
  foe_stored.status = 0;
2055
2053
  }
2056
- try clearVolatiles(battle, player, false, options);
2057
- try clearVolatiles(battle, player.foe(), true, options);
2054
+ try clearVolatiles(battle, player, options);
2055
+ try clearVolatiles(battle, player.foe(), options);
2058
2056
  }
2059
2057
  }
2060
2058
 
@@ -2066,7 +2064,7 @@ pub const Effects = struct {
2066
2064
  // GLITCH: HP recovery move failure glitches
2067
2065
  const delta = stored.stats.hp - stored.hp;
2068
2066
  if (delta == 0 or (delta & 255 == 255 and stored.hp % 256 != 0)) {
2069
- return try options.log.fail(ident, .None);
2067
+ return options.log.fail(.{ ident, .None });
2070
2068
  }
2071
2069
 
2072
2070
  const rest = side.last_selected_move == .Rest;
@@ -2074,17 +2072,17 @@ pub const Effects = struct {
2074
2072
  // Adding the sleep status runs the sleep condition handler to roll duration
2075
2073
  if (showdown) battle.rng.advance(1);
2076
2074
  stored.status = Status.slf(2);
2077
- try options.log.statusFrom(ident, stored.status, Move.Rest);
2075
+ try options.log.status(.{ ident, stored.status, .From, Move.Rest });
2078
2076
  stored.hp = stored.stats.hp;
2079
2077
  } else {
2080
2078
  stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
2081
2079
  }
2082
- try options.log.heal(ident, stored, if (rest) Heal.Silent else Heal.None);
2080
+ try options.log.heal(.{ ident, stored, if (rest) Heal.Silent else Heal.None });
2083
2081
  }
2084
2082
 
2085
2083
  fn hyperBeam(battle: anytype, player: Player, options: anytype) !void {
2086
2084
  battle.side(player).active.volatiles.Recharging = true;
2087
- try options.log.mustrecharge(battle.active(player));
2085
+ try options.log.mustrecharge(.{battle.active(player)});
2088
2086
  }
2089
2087
 
2090
2088
  fn leechSeed(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2093,33 +2091,31 @@ pub const Effects = struct {
2093
2091
  if (showdown) {
2094
2092
  // Invulnerability trumps type immunity on Pokémon Showdown
2095
2093
  if (!foe.active.volatiles.Invulnerable and foe.active.types.includes(.Grass)) {
2096
- return options.log.immune(battle.active(player.foe()), .None);
2094
+ return options.log.immune(.{ battle.active(player.foe()), .None });
2097
2095
  }
2098
2096
  if (!try checkHit(battle, player, move, options)) return;
2099
2097
  if (foe.active.volatiles.LeechSeed) return;
2100
2098
  } else {
2101
2099
  if (!try checkHit(battle, player, move, options)) return;
2102
2100
  if (foe.active.types.includes(.Grass) or foe.active.volatiles.LeechSeed) {
2103
- try options.log.lastmiss();
2104
- return options.log.miss(battle.active(player));
2101
+ try options.log.lastmiss(.{});
2102
+ return options.log.miss(.{battle.active(player)});
2105
2103
  }
2106
2104
  }
2107
2105
 
2108
- try options.chance.commit(player, .hit);
2109
2106
  foe.active.volatiles.LeechSeed = true;
2110
2107
 
2111
- try options.log.start(battle.active(player.foe()), .LeechSeed);
2108
+ try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
2112
2109
  }
2113
2110
 
2114
2111
  fn lightScreen(battle: anytype, player: Player, options: anytype) !void {
2115
2112
  var side = battle.side(player);
2113
+ const ident = battle.active(player);
2116
2114
 
2117
- if (side.active.volatiles.LightScreen) {
2118
- return options.log.fail(battle.active(player), .None);
2119
- }
2115
+ if (side.active.volatiles.LightScreen) return options.log.fail(.{ ident, .None });
2120
2116
  side.active.volatiles.LightScreen = true;
2121
2117
 
2122
- try options.log.start(battle.active(player), .LightScreen);
2118
+ try options.log.start(.{ ident, .LightScreen });
2123
2119
  }
2124
2120
 
2125
2121
  fn mimic(battle: anytype, player: Player, move: Move.Data, mslot: u8, options: anytype) !void {
@@ -2147,17 +2143,15 @@ pub const Effects = struct {
2147
2143
  if (!has_mimic) {
2148
2144
  // Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
2149
2145
  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);
2146
+ return options.log.fail(.{ battle.active(player.foe()), .None });
2152
2147
  }
2153
2148
  }
2154
2149
  if (!try checkHit(battle, player, move, options)) return;
2155
- try options.chance.commit(player, .hit);
2156
2150
 
2157
2151
  const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
2158
2152
  side.active.move(oslot).id = foe.active.move(rslot).id;
2159
2153
 
2160
- try options.log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
2154
+ try options.log.start(.{ battle.active(player), .Mimic, side.active.move(oslot).id });
2161
2155
  }
2162
2156
 
2163
2157
  fn mist(battle: anytype, player: Player, options: anytype) !void {
@@ -2166,7 +2160,7 @@ pub const Effects = struct {
2166
2160
  if (side.active.volatiles.Mist) return;
2167
2161
  side.active.volatiles.Mist = true;
2168
2162
 
2169
- try options.log.start(battle.active(player), .Mist);
2163
+ try options.log.start(.{ battle.active(player), .Mist });
2170
2164
  }
2171
2165
 
2172
2166
  fn multiHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2192,25 +2186,25 @@ pub const Effects = struct {
2192
2186
 
2193
2187
  if (showdown) {
2194
2188
  // 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;
2189
+ if (immune and !foe.active.volatiles.Invulnerable) {
2190
+ return log.immune(.{ foe_ident, .None });
2191
+ } else if (!try checkHit(battle, player, move, options)) return;
2197
2192
  }
2198
2193
  if (Status.any(foe_stored.status)) {
2199
- return log.fail(
2194
+ return log.fail(.{
2200
2195
  foe_ident,
2201
- if (Status.is(foe_stored.status, .PAR)) .Paralysis else .None,
2202
- );
2196
+ if (Status.is(foe_stored.status, .PAR)) Fail.Paralysis else Fail.None,
2197
+ });
2203
2198
  }
2204
2199
  if (!showdown) {
2205
- if (immune) return log.immune(foe_ident, .None);
2200
+ if (immune) return log.immune(.{ foe_ident, .None });
2206
2201
  if (!try checkHit(battle, player, move, options)) return;
2207
2202
  }
2208
- try options.chance.commit(player, .hit);
2209
2203
 
2210
2204
  foe_stored.status = Status.init(.PAR);
2211
2205
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2212
2206
 
2213
- try log.status(foe_ident, foe_stored.status, .None);
2207
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2214
2208
  }
2215
2209
 
2216
2210
  fn paralyzeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2232,12 +2226,12 @@ pub const Effects = struct {
2232
2226
  foe_stored.status = Status.init(.PAR);
2233
2227
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2234
2228
 
2235
- try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
2229
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
2236
2230
  }
2237
2231
 
2238
2232
  fn payDay(battle: anytype, player: Player, options: anytype) !void {
2239
2233
  if (!showdown or !battle.foe(player).active.volatiles.Substitute) {
2240
- try options.log.fieldactivate();
2234
+ try options.log.fieldactivate(.{});
2241
2235
  }
2242
2236
  }
2243
2237
 
@@ -2252,28 +2246,27 @@ pub const Effects = struct {
2252
2246
  return;
2253
2247
  } else if (foe.active.volatiles.Substitute) {
2254
2248
  if (move.effect != .Poison) return;
2255
- return log.fail(foe_ident, .None);
2249
+ return log.fail(.{ foe_ident, .None });
2256
2250
  } else if (Status.any(foe_stored.status)) {
2257
2251
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2258
2252
  // Pokémon Showdown considers Toxic to be a status even in Generation I and so
2259
2253
  // 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))
2254
+ return log.fail(.{ foe_ident, if (Status.is(foe_stored.status, .PSN))
2261
2255
  if (!showdown)
2262
- .Poison
2256
+ Fail.Poison
2263
2257
  else if (toxic == (foe_stored.status == Status.TOX))
2264
- if (toxic) .Toxic else .Poison
2258
+ if (toxic) Fail.Toxic else Fail.Poison
2265
2259
  else
2266
- .None
2260
+ Fail.None
2267
2261
  else
2268
- .None);
2262
+ Fail.None });
2269
2263
  } else if (foe.active.types.includes(.Poison)) {
2270
2264
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2271
- return log.immune(foe_ident, .None);
2265
+ return log.immune(.{ foe_ident, .None });
2272
2266
  }
2273
2267
 
2274
2268
  if (move.effect == .Poison) {
2275
2269
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2276
- try options.chance.commit(player, .hit);
2277
2270
  } else {
2278
2271
  if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
2279
2272
  return;
@@ -2287,7 +2280,7 @@ pub const Effects = struct {
2287
2280
  foe.active.volatiles.toxic = 0;
2288
2281
  }
2289
2282
 
2290
- try log.status(foe_ident, foe_stored.status, .None);
2283
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2291
2284
  }
2292
2285
 
2293
2286
  fn rage(battle: anytype, player: Player) !void {
@@ -2307,22 +2300,23 @@ pub const Effects = struct {
2307
2300
  @as(u8, if (side.last_selected_move == .Struggle) 2 else 4), 1));
2308
2301
  stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
2309
2302
 
2310
- try options.log.damageOf(
2303
+ try options.log.damage(.{
2311
2304
  battle.active(player),
2312
2305
  stored,
2313
2306
  .RecoilOf,
2314
2307
  battle.active(player.foe()),
2315
- );
2308
+ });
2316
2309
  if (showdown and stored.hp == 0) residual.* = false;
2317
2310
  }
2318
2311
 
2319
2312
  fn reflect(battle: anytype, player: Player, options: anytype) !void {
2320
2313
  var side = battle.side(player);
2314
+ const ident = battle.active(player);
2321
2315
 
2322
- if (side.active.volatiles.Reflect) return options.log.fail(battle.active(player), .None);
2316
+ if (side.active.volatiles.Reflect) return options.log.fail(.{ ident, .None });
2323
2317
  side.active.volatiles.Reflect = true;
2324
2318
 
2325
- try options.log.start(battle.active(player), .Reflect);
2319
+ try options.log.start(.{ ident, .Reflect });
2326
2320
  }
2327
2321
 
2328
2322
  fn sleep(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2336,14 +2330,13 @@ pub const Effects = struct {
2336
2330
  // the Sleep Clause Mod might activate, causing us to not actually bypass
2337
2331
  } else {
2338
2332
  if (Status.any(foe_stored.status)) {
2339
- return options.log.fail(
2333
+ return options.log.fail(.{
2340
2334
  foe_ident,
2341
- if (Status.is(foe_stored.status, .SLP)) .Sleep else .None,
2342
- );
2335
+ if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
2336
+ });
2343
2337
  }
2344
- // If checkHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2338
+ // If moveHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2345
2339
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2346
- try options.chance.commit(player, .hit);
2347
2340
  }
2348
2341
 
2349
2342
  // Sleep Clause Mod
@@ -2355,19 +2348,20 @@ pub const Effects = struct {
2355
2348
  foe.active.volatiles.Recharging = false;
2356
2349
 
2357
2350
  foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2351
+ options.chance.observe(.sleep, player.foe(), .started);
2352
+
2358
2353
  const last = battle.side(player).last_selected_move;
2359
- try options.log.statusFrom(foe_ident, foe_stored.status, last);
2354
+ try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
2360
2355
  }
2361
2356
 
2362
2357
  fn splash(battle: anytype, player: Player, options: anytype) !void {
2363
- try options.log.activate(battle.active(player), .Splash);
2358
+ try options.log.activate(.{ battle.active(player), .Splash });
2364
2359
  }
2365
2360
 
2366
2361
  fn substitute(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
2367
2362
  var side = battle.side(player);
2368
2363
  if (side.active.volatiles.Substitute) {
2369
- try options.log.fail(battle.active(player), .Substitute);
2370
- return;
2364
+ return options.log.fail(.{ battle.active(player), .Substitute });
2371
2365
  }
2372
2366
 
2373
2367
  assert(side.stored().stats.hp <= 1023);
@@ -2379,28 +2373,30 @@ pub const Effects = struct {
2379
2373
  // avoid having to convert to floating point)
2380
2374
  const required_hp = if (showdown) @divFloor(side.stored().stats.hp - 1, 4) + 1 else hp;
2381
2375
  if (side.stored().hp < required_hp) {
2382
- try options.log.fail(battle.active(player), .Weak);
2383
- return;
2376
+ return options.log.fail(.{ battle.active(player), .Weak });
2384
2377
  }
2385
2378
 
2386
2379
  // GLITCH: can leave the user with 0 HP (faints later) because didn't check '<=' above
2387
2380
  side.stored().hp -= hp;
2388
2381
  side.active.volatiles.substitute = hp + 1;
2389
2382
  side.active.volatiles.Substitute = true;
2390
- try options.log.start(battle.active(player), .Substitute);
2383
+ try options.log.start(.{ battle.active(player), .Substitute });
2391
2384
  if (hp > 0) {
2392
- try options.log.damage(battle.active(player), side.stored(), .None);
2385
+ try options.log.damage(.{ battle.active(player), side.stored(), .None });
2393
2386
  if (showdown and side.stored().hp == 0) residual.* = false;
2394
2387
  }
2395
2388
  }
2396
2389
 
2397
2390
  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
2391
  // 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;
2392
+ if (showdown) {
2393
+ if (battle.side(player).last_selected_move == .Teleport) return;
2394
+ _ = try checkHit(battle, player, move, options);
2395
+ battle.last_damage = 0;
2396
+ } else {
2397
+ try options.log.fail(.{ battle.active(player), .None });
2398
+ try options.log.laststill(.{});
2399
+ }
2404
2400
  }
2405
2401
 
2406
2402
  fn thrashing(battle: anytype, player: Player, options: anytype) void {
@@ -2410,6 +2406,8 @@ pub const Effects = struct {
2410
2406
 
2411
2407
  volatiles.Thrashing = true;
2412
2408
  volatiles.attacks = Rolls.attackingDuration(battle, player, options);
2409
+
2410
+ options.chance.observe(.attacking, player, .started);
2413
2411
  }
2414
2412
 
2415
2413
  fn transform(battle: anytype, player: Player, options: anytype) !void {
@@ -2438,7 +2436,7 @@ pub const Effects = struct {
2438
2436
  side.active.moves[i].pp = if (m.id != .None) 5 else 0;
2439
2437
  }
2440
2438
 
2441
- try options.log.transform(battle.active(player), foe_ident);
2439
+ try options.log.transform(.{ battle.active(player), foe_ident });
2442
2440
  }
2443
2441
 
2444
2442
  fn binding(battle: anytype, player: Player, rewrap: bool, options: anytype) !void {
@@ -2455,6 +2453,7 @@ pub const Effects = struct {
2455
2453
 
2456
2454
  side.active.volatiles.attacks =
2457
2455
  try Rolls.distribution(battle, .Binding, player, options) - 1;
2456
+ options.chance.observe(.binding, player, .started);
2458
2457
  }
2459
2458
 
2460
2459
  fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2468,87 +2467,87 @@ pub const Effects = struct {
2468
2467
  switch (move.effect) {
2469
2468
  .AttackUp1, .AttackUp2, .Rage => {
2470
2469
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2471
- if (boosts.atk == 6) return try log.fail(ident, .None);
2470
+ if (boosts.atk == 6) return log.fail(.{ ident, .None });
2472
2471
  const n: u2 = if (move.effect == .AttackUp2) 2 else 1;
2473
2472
  boosts.atk = @as(i4, @intCast(@min(6, @as(i8, boosts.atk) + n)));
2474
- const reason = if (move.effect == .Rage) Boost.Rage else Boost.Attack;
2473
+ const reason: Boost = if (move.effect == .Rage) .Rage else .Attack;
2475
2474
  if (stats.atk == MAX_STAT_VALUE) {
2476
2475
  boosts.atk -= 1;
2477
2476
  if (showdown) {
2478
- try log.boost(ident, reason, n);
2479
- try log.boost(ident, Boost.Attack, -1);
2477
+ try log.boost(.{ ident, reason, n });
2478
+ try log.boost(.{ ident, .Attack, -1 });
2480
2479
  if (move.effect == .Rage) return;
2481
2480
  }
2482
- return try log.fail(ident, .None);
2481
+ return log.fail(.{ ident, .None });
2483
2482
  }
2484
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2483
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2485
2484
  const stat = unmodifiedStats(battle, side).atk;
2486
2485
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2487
- try log.boost(ident, reason, n);
2486
+ try log.boost(.{ ident, reason, n });
2488
2487
  // Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
2489
2488
  if (showdown and move.effect == .Rage) return;
2490
2489
  },
2491
2490
  .DefenseUp1, .DefenseUp2 => {
2492
2491
  assert(boosts.def >= -6 and boosts.def <= 6);
2493
- if (boosts.def == 6) return try log.fail(ident, .None);
2492
+ if (boosts.def == 6) return log.fail(.{ ident, .None });
2494
2493
  const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
2495
2494
  boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
2496
2495
  if (stats.def == MAX_STAT_VALUE) {
2497
2496
  boosts.def -= 1;
2498
2497
  if (showdown) {
2499
- try log.boost(ident, .Defense, n);
2500
- try log.boost(ident, .Defense, -1);
2498
+ try log.boost(.{ ident, .Defense, n });
2499
+ try log.boost(.{ ident, .Defense, -1 });
2501
2500
  }
2502
- return try log.fail(ident, .None);
2501
+ return log.fail(.{ ident, .None });
2503
2502
  }
2504
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2503
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2505
2504
  const stat = unmodifiedStats(battle, side).def;
2506
2505
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2507
- try log.boost(ident, .Defense, n);
2506
+ try log.boost(.{ ident, .Defense, n });
2508
2507
  },
2509
2508
  .SpeedUp2 => {
2510
2509
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2511
- if (boosts.spe == 6) return try log.fail(ident, .None);
2510
+ if (boosts.spe == 6) return log.fail(.{ ident, .None });
2512
2511
  boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
2513
2512
  if (stats.spe == MAX_STAT_VALUE) {
2514
2513
  boosts.spe -= 1;
2515
2514
  if (showdown) {
2516
- try log.boost(ident, .Speed, 2);
2517
- try log.boost(ident, .Speed, -1);
2515
+ try log.boost(.{ ident, .Speed, 2 });
2516
+ try log.boost(.{ ident, .Speed, -1 });
2518
2517
  }
2519
- return try log.fail(ident, .None);
2518
+ return log.fail(.{ ident, .None });
2520
2519
  }
2521
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2520
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2522
2521
  const stat = unmodifiedStats(battle, side).spe;
2523
2522
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2524
- try log.boost(ident, .Speed, 2);
2523
+ try log.boost(.{ ident, .Speed, 2 });
2525
2524
  },
2526
2525
  .SpecialUp1, .SpecialUp2 => {
2527
2526
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2528
- if (boosts.spc == 6) return try log.fail(ident, .None);
2527
+ if (boosts.spc == 6) return log.fail(.{ ident, .None });
2529
2528
  const n: u2 = if (move.effect == .SpecialUp2) 2 else 1;
2530
2529
  boosts.spc = @intCast(@min(6, @as(i8, boosts.spc) + n));
2531
2530
  if (stats.spc == MAX_STAT_VALUE) {
2532
2531
  boosts.spc -= 1;
2533
2532
  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);
2533
+ try log.boost(.{ ident, .SpecialAttack, n });
2534
+ try log.boost(.{ ident, .SpecialAttack, -1 });
2535
+ try log.boost(.{ ident, .SpecialDefense, n });
2536
+ try log.boost(.{ ident, .SpecialDefense, -1 });
2538
2537
  }
2539
- return try log.fail(ident, .None);
2538
+ return log.fail(.{ ident, .None });
2540
2539
  }
2541
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2540
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2542
2541
  const stat = unmodifiedStats(battle, side).spc;
2543
2542
  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);
2543
+ try log.boost(.{ ident, .SpecialAttack, n });
2544
+ try log.boost(.{ ident, .SpecialDefense, n });
2546
2545
  },
2547
2546
  .EvasionUp1 => {
2548
2547
  assert(boosts.evasion >= -6 and boosts.evasion <= 6);
2549
- if (boosts.evasion == 6) return try log.fail(ident, .None);
2548
+ if (boosts.evasion == 6) return log.fail(.{ ident, .None });
2550
2549
  boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
2551
- try log.boost(ident, .Evasion, 1);
2550
+ try log.boost(.{ ident, .Evasion, 1 });
2552
2551
  },
2553
2552
  else => unreachable,
2554
2553
  }
@@ -2563,7 +2562,7 @@ pub const Effects = struct {
2563
2562
  const foe_ident = battle.active(player.foe());
2564
2563
  const secondary = move.effect.isStatDownChance();
2565
2564
 
2566
- if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(foe_ident, .None);
2565
+ if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(.{ foe_ident, .None });
2567
2566
 
2568
2567
  if (secondary) {
2569
2568
  const proc = try Rolls.unboost(battle, player, options);
@@ -2571,7 +2570,6 @@ pub const Effects = struct {
2571
2570
  } else {
2572
2571
  // checkHit already checks for Invulnerable
2573
2572
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2574
- try options.chance.commit(player, .hit);
2575
2573
  }
2576
2574
 
2577
2575
  var stats = &foe.active.stats;
@@ -2581,82 +2579,82 @@ pub const Effects = struct {
2581
2579
  switch (move.effect) {
2582
2580
  .AttackDown1, .AttackDownChance => {
2583
2581
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2584
- if (boosts.atk == -6) return if (fail) try log.fail(foe_ident, .None);
2582
+ if (boosts.atk == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2585
2583
  boosts.atk = @intCast(@max(-6, @as(i8, boosts.atk) - 1));
2586
2584
  if (stats.atk == 1) {
2587
2585
  boosts.atk += 1;
2588
2586
  if (showdown) {
2589
- try log.boost(foe_ident, .Attack, -1);
2590
- try log.boost(foe_ident, .Attack, 1);
2587
+ try log.boost(.{ foe_ident, .Attack, -1 });
2588
+ try log.boost(.{ foe_ident, .Attack, 1 });
2591
2589
  }
2592
- return try log.fail(foe_ident, .None);
2590
+ return log.fail(.{ foe_ident, .None });
2593
2591
  }
2594
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2592
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2595
2593
  const stat = unmodifiedStats(battle, foe).atk;
2596
2594
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2597
- try log.boost(foe_ident, .Attack, -1);
2595
+ try log.boost(.{ foe_ident, .Attack, -1 });
2598
2596
  },
2599
2597
  .DefenseDown1, .DefenseDown2, .DefenseDownChance => {
2600
2598
  assert(boosts.def >= -6 and boosts.def <= 6);
2601
- if (boosts.def == -6) return if (fail) try log.fail(foe_ident, .None);
2599
+ if (boosts.def == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2602
2600
  const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
2603
2601
  boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
2604
2602
  if (stats.def == 1) {
2605
2603
  boosts.def += 1;
2606
2604
  if (showdown) {
2607
- try log.boost(foe_ident, .Defense, -@as(i8, n));
2608
- try log.boost(foe_ident, .Defense, 1);
2605
+ try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
2606
+ try log.boost(.{ foe_ident, .Defense, 1 });
2609
2607
  }
2610
- return try log.fail(foe_ident, .None);
2608
+ return log.fail(.{ foe_ident, .None });
2611
2609
  }
2612
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2610
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2613
2611
  const stat = unmodifiedStats(battle, foe).def;
2614
2612
  stats.def = @max(1, stat * mod[0] / mod[1]);
2615
- try log.boost(foe_ident, .Defense, -@as(i8, n));
2613
+ try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
2616
2614
  },
2617
2615
  .SpeedDown1, .SpeedDownChance => {
2618
2616
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2619
- if (boosts.spe == -6) return if (fail) try log.fail(foe_ident, .None);
2617
+ if (boosts.spe == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2620
2618
  boosts.spe = @intCast(@max(-6, @as(i8, boosts.spe) - 1));
2621
2619
  if (stats.spe == 1) {
2622
2620
  boosts.spe += 1;
2623
2621
  if (showdown) {
2624
- try log.boost(foe_ident, .Speed, -1);
2625
- try log.boost(foe_ident, .Speed, 1);
2622
+ try log.boost(.{ foe_ident, .Speed, -1 });
2623
+ try log.boost(.{ foe_ident, .Speed, 1 });
2626
2624
  }
2627
- return try log.fail(foe_ident, .None);
2625
+ return log.fail(.{ foe_ident, .None });
2628
2626
  }
2629
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2627
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2630
2628
  const stat = unmodifiedStats(battle, foe).spe;
2631
2629
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2632
- try log.boost(foe_ident, .Speed, -1);
2630
+ try log.boost(.{ foe_ident, .Speed, -1 });
2633
2631
  assert(boosts.spe >= -6);
2634
2632
  },
2635
2633
  .SpecialDownChance => {
2636
2634
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2637
- if (boosts.spc == -6) return if (fail) try log.fail(foe_ident, .None);
2635
+ if (boosts.spc == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2638
2636
  boosts.spc = @intCast(@max(-6, @as(i8, boosts.spc) - 1));
2639
2637
  if (stats.spc == 1) {
2640
2638
  boosts.spc += 1;
2641
2639
  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);
2640
+ try log.boost(.{ foe_ident, .SpecialAttack, -1 });
2641
+ try log.boost(.{ foe_ident, .SpecialAttack, 1 });
2642
+ try log.boost(.{ foe_ident, .SpecialDefense, -1 });
2643
+ try log.boost(.{ foe_ident, .SpecialDefense, 1 });
2646
2644
  }
2647
- return try log.fail(foe_ident, .None);
2645
+ return log.fail(.{ foe_ident, .None });
2648
2646
  }
2649
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2647
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2650
2648
  const stat = unmodifiedStats(battle, foe).spc;
2651
2649
  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);
2650
+ try log.boost(.{ foe_ident, .SpecialAttack, -1 });
2651
+ try log.boost(.{ foe_ident, .SpecialDefense, -1 });
2654
2652
  },
2655
2653
  .AccuracyDown1 => {
2656
2654
  assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
2657
- if (boosts.accuracy == -6) return if (fail) try log.fail(foe_ident, .None);
2655
+ if (boosts.accuracy == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2658
2656
  boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
2659
- try log.boost(foe_ident, .Accuracy, -1);
2657
+ try log.boost(.{ foe_ident, .Accuracy, -1 });
2660
2658
  },
2661
2659
  else => unreachable,
2662
2660
  }
@@ -2680,40 +2678,40 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
2680
2678
  }
2681
2679
  }
2682
2680
 
2683
- inline fn isForced(active: anytype) bool {
2681
+ fn isForced(active: anytype) bool {
2684
2682
  return active.volatiles.Recharging or active.volatiles.Rage or
2685
2683
  active.volatiles.Thrashing or active.volatiles.Charging;
2686
2684
  }
2687
2685
 
2688
- fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !void {
2686
+ fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
2689
2687
  var log = options.log;
2690
2688
  var side = battle.side(who);
2691
2689
  var volatiles = &side.active.volatiles;
2692
2690
  const ident = battle.active(who);
2693
2691
 
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);
2692
+ if (volatiles.disable_move != 0) {
2693
+ volatiles.disable_move = 0;
2694
+ volatiles.disable_duration = 0;
2695
+ options.chance.observe(.disable, who, .None);
2696
+ try log.end(.{ ident, .DisableSilent });
2700
2697
  }
2701
2698
  if (volatiles.Confusion) {
2702
2699
  // volatiles.confusion is left unchanged
2703
2700
  volatiles.Confusion = false;
2704
- try log.end(ident, .ConfusionSilent);
2701
+ options.chance.observe(.confusion, who, .None);
2702
+ try log.end(.{ ident, .ConfusionSilent });
2705
2703
  }
2706
2704
  if (volatiles.Mist) {
2707
2705
  volatiles.Mist = false;
2708
- try log.end(ident, .Mist);
2706
+ try log.end(.{ ident, .MistSilent });
2709
2707
  }
2710
2708
  if (volatiles.FocusEnergy) {
2711
2709
  volatiles.FocusEnergy = false;
2712
- try log.end(ident, .FocusEnergy);
2710
+ try log.end(.{ ident, .FocusEnergySilent });
2713
2711
  }
2714
2712
  if (volatiles.LeechSeed) {
2715
2713
  volatiles.LeechSeed = false;
2716
- try log.end(ident, .LeechSeed);
2714
+ try log.end(.{ ident, .LeechSeedSilent });
2717
2715
  }
2718
2716
  if (!showdown and volatiles.Toxic) {
2719
2717
  volatiles.Toxic = false;
@@ -2721,24 +2719,39 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2721
2719
  }
2722
2720
  if (volatiles.LightScreen) {
2723
2721
  volatiles.LightScreen = false;
2724
- try log.end(ident, .LightScreen);
2722
+ try log.end(.{ ident, .LightScreenSilent });
2725
2723
  }
2726
2724
  if (volatiles.Reflect) {
2727
2725
  volatiles.Reflect = false;
2728
- try log.end(ident, .Reflect);
2726
+ try log.end(.{ ident, .ReflectSilent });
2729
2727
  }
2730
2728
  if (showdown and volatiles.Toxic) {
2731
2729
  volatiles.Toxic = false;
2732
2730
  // Pokémon Showdown erroneously clears the toxic counter
2733
2731
  volatiles.toxic = 0;
2734
- try log.end(ident, .Toxic);
2732
+ try log.end(.{ ident, .ToxicSilent });
2735
2733
  }
2736
2734
  }
2737
2735
 
2736
+ fn decrement(
2737
+ comptime field: chance.Action.Field,
2738
+ player: Player,
2739
+ options: anytype,
2740
+ n: anytype,
2741
+ ) @TypeOf(n) {
2742
+ return if (options.calc.overridden(player, field)) |obs| switch (obs) {
2743
+ .started, .ended => 0,
2744
+ .continuing => if (n > 1) n - 1 else n,
2745
+ else => unreachable,
2746
+ } else n - 1;
2747
+ }
2748
+
2738
2749
  pub const Rolls = struct {
2739
- inline fn speedTie(battle: anytype, options: anytype) !bool {
2750
+ fn speedTie(battle: anytype, options: anytype) !bool {
2740
2751
  const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
2741
2752
  player == .P1
2753
+ else if (@hasDecl(@TypeOf(battle.rng), "speedTie"))
2754
+ battle.rng.speedTie()
2742
2755
  else if (showdown)
2743
2756
  battle.rng.range(u8, 0, 2) == 0
2744
2757
  else
@@ -2748,21 +2761,31 @@ pub const Rolls = struct {
2748
2761
  return p1;
2749
2762
  }
2750
2763
 
2751
- inline fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2764
+ fn criticalHit(
2765
+ battle: anytype,
2766
+ player: Player,
2767
+ rate: u8,
2768
+ spurious: bool,
2769
+ options: anytype,
2770
+ ) !bool {
2752
2771
  const crit = if (options.calc.overridden(player, .critical_hit)) |val|
2753
2772
  val == .true
2773
+ else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
2774
+ battle.rng.criticalHit(player, rate)
2754
2775
  else if (showdown)
2755
2776
  battle.rng.chance(u8, rate, 256)
2756
2777
  else
2757
2778
  std.math.rotl(u8, battle.rng.next(), 3) < rate;
2758
2779
 
2759
- try options.chance.criticalHit(player, crit, rate);
2780
+ if (!spurious) try options.chance.criticalHit(player, crit, rate);
2760
2781
  return crit;
2761
2782
  }
2762
2783
 
2763
- inline fn damage(battle: anytype, player: anytype, options: anytype) !u8 {
2784
+ fn damage(battle: anytype, player: Player, options: anytype) !u8 {
2764
2785
  const roll = if (options.calc.overridden(player, .damage)) |val|
2765
2786
  val
2787
+ else if (@hasDecl(@TypeOf(battle.rng), "damage"))
2788
+ battle.rng.damage(player)
2766
2789
  else roll: {
2767
2790
  if (showdown) break :roll battle.rng.range(u8, 217, 256);
2768
2791
  while (true) {
@@ -2776,21 +2799,27 @@ pub const Rolls = struct {
2776
2799
  return roll;
2777
2800
  }
2778
2801
 
2779
- inline fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) bool {
2802
+ const uN = if (pkmn.options.miss) u8 else u9;
2803
+ fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) !bool {
2804
+ const acc: uN = if (pkmn.options.miss) accuracy else @as(uN, accuracy) + 1;
2780
2805
  const ok = if (options.calc.overridden(player, .hit)) |val|
2781
2806
  val == .true
2807
+ else if (@hasDecl(@TypeOf(battle.rng), "hit"))
2808
+ battle.rng.hit(player, acc)
2782
2809
  else if (showdown)
2783
- battle.rng.chance(u8, accuracy, 256)
2810
+ battle.rng.chance(uN, acc, 256)
2784
2811
  else
2785
- battle.rng.next() < accuracy;
2812
+ battle.rng.next() < acc;
2786
2813
 
2787
- options.chance.hit(ok, accuracy);
2814
+ try options.chance.hit(player, ok, acc);
2788
2815
  return ok;
2789
2816
  }
2790
2817
 
2791
- inline fn confused(battle: anytype, player: Player, options: anytype) !bool {
2818
+ fn confused(battle: anytype, player: Player, options: anytype) !bool {
2792
2819
  const cfz = if (options.calc.overridden(player, .confused)) |val|
2793
2820
  val == .true
2821
+ else if (@hasDecl(@TypeOf(battle.rng), "confused"))
2822
+ battle.rng.confused(player)
2794
2823
  else if (showdown)
2795
2824
  !battle.rng.chance(u8, 128, 256)
2796
2825
  else
@@ -2800,9 +2829,11 @@ pub const Rolls = struct {
2800
2829
  return cfz;
2801
2830
  }
2802
2831
 
2803
- inline fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2832
+ fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2804
2833
  const par = if (options.calc.overridden(player, .paralyzed)) |val|
2805
2834
  val == .true
2835
+ else if (@hasDecl(@TypeOf(battle.rng), "paralyzed"))
2836
+ battle.rng.paralyzed(player)
2806
2837
  else if (showdown)
2807
2838
  battle.rng.chance(u8, 63, 256)
2808
2839
  else
@@ -2812,9 +2843,11 @@ pub const Rolls = struct {
2812
2843
  return par;
2813
2844
  }
2814
2845
 
2815
- inline fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2846
+ fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2816
2847
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2817
2848
  val == .true
2849
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2850
+ battle.rng.secondaryChance(player, 25)
2818
2851
  else if (showdown)
2819
2852
  battle.rng.chance(u8, 25, 256)
2820
2853
  else
@@ -2824,11 +2857,13 @@ pub const Rolls = struct {
2824
2857
  return proc;
2825
2858
  }
2826
2859
 
2827
- inline fn secondaryChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2860
+ fn secondaryChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2828
2861
  const rate: u8 = if (low) 26 else 77;
2829
2862
 
2830
2863
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2831
2864
  val == .true
2865
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2866
+ battle.rng.secondaryChance(player, rate)
2832
2867
  else if (showdown)
2833
2868
  battle.rng.chance(u8, rate, 256)
2834
2869
  else
@@ -2838,11 +2873,13 @@ pub const Rolls = struct {
2838
2873
  return proc;
2839
2874
  }
2840
2875
 
2841
- inline fn poisonChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2876
+ fn poisonChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2842
2877
  const rate: u8 = if (low) 52 else 103;
2843
2878
 
2844
2879
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2845
2880
  val == .true
2881
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2882
+ battle.rng.secondaryChance(player, rate)
2846
2883
  else if (showdown)
2847
2884
  battle.rng.chance(u8, rate, 256)
2848
2885
  else
@@ -2852,9 +2889,11 @@ pub const Rolls = struct {
2852
2889
  return proc;
2853
2890
  }
2854
2891
 
2855
- inline fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2892
+ fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2856
2893
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2857
2894
  val == .true
2895
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2896
+ battle.rng.secondaryChance(player, 85)
2858
2897
  else if (showdown)
2859
2898
  battle.rng.chance(u8, 85, 256)
2860
2899
  else
@@ -2864,14 +2903,14 @@ pub const Rolls = struct {
2864
2903
  return proc;
2865
2904
  }
2866
2905
 
2867
- inline fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2906
+ fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2868
2907
  const move: Move = if (options.calc.overridden(player, .metronome)) |val|
2869
2908
  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: {
2909
+ else if (@hasDecl(@TypeOf(battle.rng), "metronome"))
2910
+ battle.rng.metronome(player)
2911
+ else if (showdown)
2912
+ Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
2913
+ else move: {
2875
2914
  while (true) {
2876
2915
  const r = battle.rng.next();
2877
2916
  if (r == 0 or r == @intFromEnum(Move.Metronome)) continue;
@@ -2884,9 +2923,11 @@ pub const Rolls = struct {
2884
2923
  return move;
2885
2924
  }
2886
2925
 
2887
- inline fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2926
+ fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2888
2927
  const power = if (options.calc.overridden(player, .psywave)) |val|
2889
2928
  val - 1
2929
+ else if (@hasDecl(@TypeOf(battle.rng), "psywave"))
2930
+ battle.rng.psywave(player)
2890
2931
  else if (showdown)
2891
2932
  battle.rng.range(u8, 0, max)
2892
2933
  else power: {
@@ -2901,11 +2942,14 @@ pub const Rolls = struct {
2901
2942
  return power;
2902
2943
  }
2903
2944
 
2904
- inline fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2905
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2906
- @intCast(val)
2945
+ fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2946
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
2947
+ if (val >= 1 and val <= 7) @as(u3, @intCast(val)) else null
2948
+ else
2949
+ null) orelse 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,60 +2958,74 @@ 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 {
2922
- const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
2923
- val
2965
+ fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2966
+ const duration: u4 = (if (options.calc.overridden(player, .duration)) |val|
2967
+ if (val >= 1 and val <= 8) val else null
2968
+ else
2969
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
2970
+ battle.rng.disableDuration(player)
2924
2971
  else if (showdown)
2925
- @intCast(battle.rng.range(u8, 1, 9))
2972
+ battle.rng.range(u4, 1, 9)
2926
2973
  else
2927
2974
  @intCast((battle.rng.next() & 7) + 1);
2928
2975
 
2929
2976
  assert(duration >= 1 and duration <= 8);
2930
- options.chance.duration(.disable, player, player.foe(), duration);
2977
+ options.chance.duration(player, duration);
2931
2978
  return duration;
2932
2979
  }
2933
2980
 
2934
- inline fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
2935
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2936
- @intCast(val)
2981
+ fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
2982
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
2983
+ if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
2984
+ else
2985
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
2986
+ battle.rng.confusionDuration(player)
2937
2987
  else if (showdown)
2938
- @intCast(battle.rng.range(u8, 2, 6))
2988
+ battle.rng.range(u3, 2, 6)
2939
2989
  else
2940
2990
  @intCast((battle.rng.next() & 3) + 2);
2941
2991
 
2942
2992
  assert(duration >= 2 and duration <= 5);
2943
- options.chance.duration(.confusion, player, if (self) player else player.foe(), duration);
2993
+ options.chance.duration(player, duration);
2944
2994
  return duration;
2945
2995
  }
2946
2996
 
2947
- inline fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2948
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2949
- @intCast(val)
2997
+ fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2998
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
2999
+ if (val >= 2 and val <= 3) @as(u3, @intCast(val)) else null
3000
+ else
3001
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
3002
+ battle.rng.attackingDuration(player)
2950
3003
  else if (showdown)
2951
- @intCast(battle.rng.range(u4, 2, 4))
3004
+ battle.rng.range(u3, 2, 4)
2952
3005
  else
2953
3006
  @intCast((battle.rng.next() & 1) + 2);
2954
3007
 
2955
3008
  assert(duration >= 2 and duration <= 3);
2956
- options.chance.duration(.attacking, player, player, duration);
3009
+ options.chance.duration(player, duration);
2957
3010
  return duration;
2958
3011
  }
2959
3012
 
2960
- inline fn distribution(
3013
+ const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
3014
+
3015
+ fn distribution(
2961
3016
  battle: anytype,
2962
3017
  comptime effect: Move.Effect,
2963
3018
  player: Player,
2964
3019
  options: anytype,
2965
3020
  ) !u3 {
2966
3021
  const roll = if (effect == .Binding) .duration else .multi_hit;
2967
- const n: u3 = if (options.calc.overridden(player, roll)) |val|
2968
- @intCast(val)
3022
+ const n: u3 = (if (options.calc.overridden(player, roll)) |val|
3023
+ if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
3024
+ else
3025
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "distribution"))
3026
+ battle.rng.distribution(player)
2969
3027
  else if (showdown)
2970
- DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)]
3028
+ DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
2971
3029
  else n: {
2972
3030
  const r = (battle.rng.next() & 3);
2973
3031
  break :n @intCast((if (r < 2) r else battle.rng.next() & 3) + 2);
@@ -2975,7 +3033,7 @@ pub const Rolls = struct {
2975
3033
 
2976
3034
  assert(n >= 2 and n <= 5);
2977
3035
  if (effect == .Binding) {
2978
- options.chance.duration(.binding, player, player, n);
3036
+ options.chance.duration(player, n);
2979
3037
  } else {
2980
3038
  try options.chance.multiHit(player, n);
2981
3039
  }
@@ -2990,15 +3048,15 @@ pub const Rolls = struct {
2990
3048
  options: anytype,
2991
3049
  ) !u4 {
2992
3050
  // TODO: consider throwing error instead of rerolling?
2993
- const overridden = if (options.calc.overridden(player, .move_slot)) |val|
3051
+ const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
2994
3052
  if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
2995
3053
  val
2996
3054
  else
2997
3055
  null
2998
3056
  else
2999
- null;
3000
-
3001
- const slot: u4 = overridden orelse slot: {
3057
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
3058
+ battle.rng.moveSlot(player, moves, check_pp)
3059
+ else slot: {
3002
3060
  if (showdown) {
3003
3061
  if (check_pp == 0) {
3004
3062
  var i: usize = moves.len;
@@ -3014,10 +3072,9 @@ pub const Rolls = struct {
3014
3072
  while (i < moves.len and r > 0) : (i += 1) {
3015
3073
  if (moves[i].pp > 0) {
3016
3074
  r -= 1;
3017
- if (r == 0) break;
3075
+ if (r == 0) break :slot @intCast(i + 1);
3018
3076
  }
3019
3077
  }
3020
- break :slot @intCast(i + 1);
3021
3078
  }
3022
3079
  }
3023
3080
 
@@ -3039,11 +3096,11 @@ test "RNG agreement" {
3039
3096
  expected[i] = @intCast(i * 0x1000000);
3040
3097
  }
3041
3098
 
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 };
3099
+ var spe: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3100
+ var cfz: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3101
+ var par: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3102
+ var brn: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3103
+ var eff: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3047
3104
 
3048
3105
  for (0..expected.len) |i| {
3049
3106
  try expectEqual(spe.range(u8, 0, 2) == 0, i < Gen12.percent(50) + 1);
@@ -3079,11 +3136,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3079
3136
  const side = battle.side(player);
3080
3137
  const foe = battle.foe(player);
3081
3138
 
3082
- var active = &side.active;
3139
+ const active = &side.active;
3083
3140
  const stored = side.stored();
3084
3141
 
3085
3142
  // 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.
3143
+ // Pokémon Showdown instead produces a list with a single move that must be chosen
3087
3144
  //
3088
3145
  // Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
3089
3146
  // here when not in compatibility mode, but the engine needs to be able to differentiate
@@ -3091,7 +3148,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3091
3148
  // passing due to being forced into using a move. Instead of introducing another option
3092
3149
  // we simply repurpose Move with no move slot, even though pedantically this is not
3093
3150
  // strictly correct as the player would not have been presented the option to move or
3094
- // switch at all.
3151
+ // switch at all
3095
3152
  if (isForced(active)) {
3096
3153
  out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
3097
3154
  n += 1;
@@ -3110,7 +3167,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3110
3167
  // On the cartridge, all of these happen after "FIGHT" (indicating you are not
3111
3168
  // switching) but before you are allowed to select a move. Pokémon Showdown instead
3112
3169
  // either disables all other moves in the case of limited or requires you to select a
3113
- // move normally if sleeping/frozen/bound.
3170
+ // move normally if sleeping/frozen/bound
3114
3171
  if (!showdown and (limited or foe.active.volatiles.Binding or
3115
3172
  Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
3116
3173
  {
@@ -3123,7 +3180,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3123
3180
  // Pokémon Showdown handles Bide and Binding moves by checking if the move in question
3124
3181
  // is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
3125
3182
  // 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.
3183
+ // proc-ed move in their moveset) and disabling all other moves
3127
3184
  if (limited) {
3128
3185
  assert(showdown);
3129
3186
  assert(side.last_selected_move != .None);
@@ -3133,7 +3190,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3133
3190
  if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
3134
3191
  // Pokémon Showdown displays Struggle if limited to Bide but unable to pick
3135
3192
  const struggle =
3136
- m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);
3193
+ m.id == .Bide and (m.pp == 0 or active.volatiles.disable_move == slot);
3137
3194
  const s = if (struggle) 0 else slot;
3138
3195
  out[n] = .{ .type = .Move, .data = s };
3139
3196
  n += 1;
@@ -3148,7 +3205,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3148
3205
  const m = active.moves[slot - 1];
3149
3206
  if (m.id == .None) break;
3150
3207
  if (m.pp == 0) continue;
3151
- if (active.volatiles.disabled_move == slot) continue;
3208
+ if (active.volatiles.disable_move == slot) continue;
3152
3209
  out[n] = .{ .type = .Move, .data = slot };
3153
3210
  n += 1;
3154
3211
  }