@pkmn/engine 0.1.0-dev.526bb2e1 → 0.1.0-dev.53787794

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