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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/LICENSE +1 -3
  2. package/README.md +46 -19
  3. package/build/pkg/addon/browser.d.ts +3 -0
  4. package/build/pkg/addon/browser.js +43 -0
  5. package/build/pkg/addon/browser.js.map +1 -0
  6. package/build/pkg/addon/node.d.ts +3 -0
  7. package/build/pkg/addon/node.js +96 -0
  8. package/build/pkg/addon/node.js.map +1 -0
  9. package/build/pkg/addon.d.ts +2 -0
  10. package/build/pkg/addon.js +35 -58
  11. package/build/pkg/addon.js.map +1 -1
  12. package/build/pkg/data/ids.json +12 -12
  13. package/build/pkg/data/layout.json +2 -2
  14. package/build/pkg/data/protocol.json +69 -12
  15. package/build/pkg/data.d.ts +27 -5
  16. package/build/pkg/data.js.map +1 -1
  17. package/build/pkg/gen1.js +3 -3
  18. package/build/pkg/gen1.js.map +1 -1
  19. package/build/pkg/index.d.ts +24 -10
  20. package/build/pkg/index.js +15 -10
  21. package/build/pkg/index.js.map +1 -1
  22. package/build/pkg/protocol.d.ts +20 -1
  23. package/build/pkg/protocol.js +123 -102
  24. package/build/pkg/protocol.js.map +1 -1
  25. package/build/tools/debug.js +36 -68
  26. package/build/tools/debug.js.map +1 -1
  27. package/build/tools/display/debug.jsx +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 +230 -99
  36. package/package.json +63 -30
  37. package/src/bin/install-pkmn-engine +29 -19
  38. package/src/bin/pkmn-debug +1 -1
  39. package/src/lib/common/array.zig +77 -0
  40. package/src/lib/common/data.zig +12 -24
  41. package/src/lib/common/debug.zig +11 -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 +807 -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 +660 -137
  50. package/src/lib/gen1/chance.zig +409 -284
  51. package/src/lib/gen1/data/moves.zig +192 -180
  52. package/src/lib/gen1/data/species.zig +3 -3
  53. package/src/lib/gen1/data/types.zig +7 -7
  54. package/src/lib/gen1/data.zig +48 -59
  55. package/src/lib/gen1/helpers.zig +94 -552
  56. package/src/lib/gen1/mechanics.zig +580 -518
  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 -892
  75. package/src/lib/gen2/README.md +0 -308
  76. package/src/test/display/pkmn.html.tmpl +0 -368
@@ -7,32 +7,30 @@ const DEBUG = @import("../common/debug.zig").print;
7
7
  const protocol = @import("../common/protocol.zig");
8
8
  const rng = @import("../common/rng.zig");
9
9
 
10
- const Action = @import("chance.zig").Action;
11
-
12
10
  const data = @import("data.zig");
11
+ const chance = @import("chance.zig");
13
12
 
14
13
  const assert = std.debug.assert;
15
14
 
16
15
  const expectEqual = std.testing.expectEqual;
17
16
 
17
+ const showdown = pkmn.options.showdown;
18
+
18
19
  const Choice = common.Choice;
19
20
  const ID = common.ID;
20
21
  const Player = common.Player;
21
22
  const Result = common.Result;
22
23
 
23
- const showdown = pkmn.options.showdown;
24
-
25
24
  const Boost = protocol.Boost;
26
25
  const Damage = protocol.Damage;
26
+ const Fail = protocol.Fail;
27
27
  const Heal = protocol.Heal;
28
28
 
29
29
  const Gen12 = rng.Gen12;
30
30
 
31
- const ActivePokemon = data.ActivePokemon;
32
31
  const Effectiveness = data.Effectiveness;
33
32
  const Move = data.Move;
34
33
  const MoveSlot = data.MoveSlot;
35
- const Pokemon = data.Pokemon;
36
34
  const Side = data.Side;
37
35
  const Species = data.Species;
38
36
  const Stats = data.Stats;
@@ -59,34 +57,26 @@ const BOOSTS = &[_][2]u8{
59
57
 
60
58
  const MAX_STAT_VALUE = 999;
61
59
 
62
- const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
63
-
64
60
  pub fn update(battle: anytype, c1: Choice, c2: Choice, options: anytype) !Result {
65
- // FIXME
66
- // std.debug.print("\n", .{});
67
- // DEBUG(battle.turn);
68
- // std.debug.print("// ", .{});
69
61
  assert(c1.type != .Pass or c2.type != .Pass or battle.turn == 0);
70
62
  if (battle.turn == 0) return start(battle, options);
71
63
 
72
64
  var s1 = false;
73
65
  var s2 = false;
74
- var f1: ?Move = null;
75
- var f2: ?Move = null;
76
66
 
77
- if (selectMove(battle, .P1, c1, c2, &s1, &f1)) |r| return r;
78
- if (selectMove(battle, .P2, c2, c1, &s2, &f2)) |r| return r;
67
+ if (selectMove(battle, .P1, c1, c2, &s1)) |r| return r;
68
+ if (selectMove(battle, .P2, c2, c1, &s2)) |r| return r;
79
69
 
80
70
  var p1 = battle.side(.P1);
81
71
  var p2 = battle.side(.P2);
82
72
 
83
- var r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
84
- var r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
73
+ const r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
74
+ const r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
85
75
 
86
76
  if (try turnOrder(battle, c1, c2, options) == .P1) {
87
- if (try doTurn(battle, .P1, c1, r1, s1, f1, .P2, c2, r2, s2, f2, options)) |r| return r;
77
+ if (try doTurn(battle, .P1, c1, r1, s1, .P2, c2, r2, s2, options)) |r| return r;
88
78
  } else {
89
- if (try doTurn(battle, .P2, c2, r2, s2, f2, .P1, c1, r1, s1, f1, options)) |r| return r;
79
+ if (try doTurn(battle, .P2, c2, r2, s2, .P1, c1, r1, s1, options)) |r| return r;
90
80
  }
91
81
 
92
82
  if (p1.active.volatiles.attacks == 0) p1.active.volatiles.Binding = false;
@@ -99,11 +89,11 @@ fn start(battle: anytype, options: anytype) !Result {
99
89
  const p1 = battle.side(.P1);
100
90
  const p2 = battle.side(.P2);
101
91
 
102
- var p1_slot = findFirstAlive(p1);
92
+ const p1_slot = findFirstAlive(p1);
103
93
  assert(!showdown or p1_slot == 1);
104
94
  if (p1_slot == 0) return if (findFirstAlive(p2) == 0) Result.Tie else Result.Lose;
105
95
 
106
- var p2_slot = findFirstAlive(p2);
96
+ const p2_slot = findFirstAlive(p2);
107
97
  assert(!showdown or p2_slot == 1);
108
98
  if (p2_slot == 0) return Result.Win;
109
99
 
@@ -124,7 +114,6 @@ fn selectMove(
124
114
  choice: Choice,
125
115
  foe_choice: Choice,
126
116
  skip_turn: *bool,
127
- from: *?Move,
128
117
  ) ?Result {
129
118
  if (choice.type == .Pass) return null;
130
119
 
@@ -141,7 +130,6 @@ fn selectMove(
141
130
  return null;
142
131
  }
143
132
  if (volatiles.Rage) {
144
- from.* = side.last_used_move;
145
133
  if (showdown) {
146
134
  if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
147
135
  saveMove(battle, player, null);
@@ -151,7 +139,6 @@ fn selectMove(
151
139
  // Pokémon Showdown removes Flinch at the end-of-turn in its residual handler
152
140
  if (!showdown) volatiles.Flinch = false;
153
141
  if (volatiles.Thrashing or volatiles.Charging) {
154
- from.* = side.last_selected_move;
155
142
  if (showdown) {
156
143
  if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
157
144
  saveMove(battle, player, null);
@@ -172,15 +159,13 @@ fn selectMove(
172
159
  return null;
173
160
  }
174
161
  if (volatiles.Binding) {
175
- from.* = side.last_used_move;
176
162
  if (showdown) {
177
- if (foe_choice.type == .Switch) from.* = null;
178
163
  // Pokémon Showdown overwrites Mirror Move with whatever was selected - really this
179
164
  // should set side.last_selected_move = last.id to reuse Mirror Move and fail in order
180
165
  // to satisfy the conditions of the Desync Clause Mod. However, because Binding is still
181
166
  // set the selected move will not actually be used, it will just be reported as having
182
167
  // been used (this differs from how Pokémon Showdown works, but its impossible to
183
- // replicate the incorrect behavior with the correct mechanisms).
168
+ // replicate the incorrect behavior with the correct mechanisms)
184
169
  saveMove(battle, player, choice);
185
170
  } else {
186
171
  assert(choice.data == 0);
@@ -210,16 +195,13 @@ fn selectMove(
210
195
  if (choice.data == 0) {
211
196
  const struggle = ok: {
212
197
  for (side.active.moves, 0..) |move, i| {
213
- if (move.pp > 0 and volatiles.disabled_move != i + 1) break :ok false;
198
+ if (move.pp > 0 and volatiles.disable_move != i + 1) break :ok false;
214
199
  }
215
200
  break :ok true;
216
201
  };
217
-
218
202
  assert(struggle);
219
- saveMove(battle, player, choice);
220
- } else {
221
- saveMove(battle, player, choice);
222
203
  }
204
+ saveMove(battle, player, choice);
223
205
 
224
206
  return null;
225
207
  }
@@ -232,7 +214,7 @@ fn saveMove(battle: anytype, player: Player, choice: ?Choice) void {
232
214
  if (c.data == 0) {
233
215
  side.last_selected_move = .Struggle;
234
216
  } else {
235
- assert(showdown or side.active.volatiles.disabled_move != c.data);
217
+ assert(showdown or side.active.volatiles.disable_move != c.data);
236
218
  const move = side.active.move(c.data);
237
219
  // You cannot *select* a move with 0 PP (except on Pokémon Showdown where that is
238
220
  // sometimes required...), but a 0 PP move can be used automatically
@@ -270,17 +252,17 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: a
270
252
  active.moves = incoming.moves;
271
253
 
272
254
  statusModify(incoming.status, &active.stats);
255
+ options.chance.switched(player, slot);
273
256
 
274
257
  foe.active.volatiles.Binding = false;
275
258
 
276
- try options.log.switched(battle.active(player), incoming);
277
- options.chance.switched(player, side.order[0], out);
259
+ try options.log.switched(.{ battle.active(player), incoming });
278
260
 
279
261
  if (showdown and incoming.status == Status.TOX) {
280
262
  incoming.status = Status.init(.PSN);
281
263
  // Technically, Pokémon Showdown adds these after *both* Pokémon have switched, but we'd
282
264
  // rather not clutter up turnOrder just for this (incorrect) log message
283
- try options.log.status(battle.active(player), incoming.status, .Silent);
265
+ try options.log.status(.{ battle.active(player), incoming.status, .Silent });
284
266
  }
285
267
  }
286
268
 
@@ -343,12 +325,10 @@ fn doTurn(
343
325
  player_choice: Choice,
344
326
  player_rewrap: bool,
345
327
  player_skip: bool,
346
- player_from: ?Move,
347
328
  foe_player: Player,
348
329
  foe_choice: Choice,
349
330
  foe_rewrap: bool,
350
331
  foe_skip: bool,
351
- foe_from: ?Move,
352
332
  options: anytype,
353
333
  ) !?Result {
354
334
  assert(player_choice.type != .Pass);
@@ -361,19 +341,19 @@ fn doTurn(
361
341
  player_choice,
362
342
  player_rewrap,
363
343
  player_skip,
364
- player_from,
344
+ true,
365
345
  &residual,
366
346
  options,
367
347
  )) |r| return r;
368
348
  if (!replace) {
369
349
  if (player_choice.type != .Switch) {
370
- if (try checkFaint(battle, foe_player, options)) |r| return r;
350
+ if (try checkFaint(battle, foe_player, true, options)) |r| return r;
371
351
  }
372
352
  if (residual) try handleResidual(battle, player, options);
373
- if (try checkFaint(battle, player, options)) |r| return r;
353
+ if (try checkFaint(battle, player, false, options)) |r| return r;
374
354
  } else if (foe_choice.type == .Pass) return null;
375
355
 
376
- options.chance.clearPending();
356
+ if (!showdown) options.chance.clearPending();
377
357
 
378
358
  residual = true;
379
359
  replace = battle.side(foe_player).stored().hp == 0;
@@ -384,16 +364,16 @@ fn doTurn(
384
364
  foe_choice,
385
365
  foe_rewrap,
386
366
  foe_skip,
387
- foe_from,
367
+ false,
388
368
  &residual,
389
369
  options,
390
370
  )) |r| return r;
391
371
  if (!replace) {
392
372
  if (!calc and foe_choice.type != .Switch) {
393
- if (try checkFaint(battle, player, options)) |r| return r;
373
+ if (try checkFaint(battle, player, true, options)) |r| return r;
394
374
  }
395
375
  if (residual) try handleResidual(battle, foe_player, options);
396
- if (try checkFaint(battle, foe_player, options)) |r| return r;
376
+ if (try checkFaint(battle, foe_player, false, options)) |r| return r;
397
377
  }
398
378
 
399
379
  // Flinch is bugged on Pokémon Showdown because it gets implemented with a duration which causes
@@ -412,7 +392,7 @@ fn executeMove(
412
392
  choice: Choice,
413
393
  rewrap: bool,
414
394
  skip: bool,
415
- from: ?Move,
395
+ first: bool,
416
396
  residual: *bool,
417
397
  options: anytype,
418
398
  ) !?Result {
@@ -429,7 +409,7 @@ fn executeMove(
429
409
  if (side.last_selected_move == .SKIP_TURN) {
430
410
  assert(!showdown);
431
411
  if (battle.foe(player).active.volatiles.Binding) {
432
- try options.log.cant(battle.active(player), .Bound);
412
+ try options.log.cant(.{ battle.active(player), .Bound });
433
413
  }
434
414
  return null;
435
415
  }
@@ -447,11 +427,13 @@ fn executeMove(
447
427
  mslot = @intCast(battle.lastMove(player).index);
448
428
  const stored = side.stored();
449
429
  // GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
450
- auto = side.last_selected_move == .HyperBeam or side.active.volatiles.Bide or
451
- from != null or Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
430
+ auto = isForced(&side.active) or
431
+ side.active.volatiles.Binding or side.active.volatiles.Bide or
432
+ side.last_selected_move == .HyperBeam or
433
+ Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
452
434
  // If it wasn't Hyper Beam or the continuation of a move effect then we must have just
453
435
  // thawed, in which case we will desync unless the last_selected_move happened to be at
454
- // index 1 and the current Pokémon has the same move in its first slot.
436
+ // index 1 and the current Pokémon has the same move in its first slot
455
437
  if (!auto) {
456
438
  // side.active.moves(slot) is safe to check even though the slot in question might not
457
439
  // technically be from this Pokémon because it must be exactly 1 to not desync and
@@ -470,7 +452,7 @@ fn executeMove(
470
452
 
471
453
  var skip_can = false;
472
454
  var skip_pp = false;
473
- switch (try beforeMove(battle, player, skip, from, residual, options)) {
455
+ switch (try beforeMove(battle, player, skip, first, residual, options)) {
474
456
  .done => return null,
475
457
  .skip_can => skip_can = true,
476
458
  .skip_pp => skip_pp = true,
@@ -478,9 +460,9 @@ fn executeMove(
478
460
  .err => return @as(?Result, Result.Error),
479
461
  }
480
462
 
481
- if (!skip_can and !try canMove(battle, player, mslot, auto, skip_pp, from, residual, options)) {
482
- return null;
483
- }
463
+ const can = skip_can or
464
+ try canMove(battle, player, mslot, auto, skip_pp, .None, residual, options);
465
+ if (!can) return null;
484
466
 
485
467
  return doMove(battle, player, mslot, rewrap, auto, residual, options);
486
468
  }
@@ -491,7 +473,7 @@ fn beforeMove(
491
473
  battle: anytype,
492
474
  player: Player,
493
475
  skip: bool,
494
- from: ?Move,
476
+ first: bool,
495
477
  residual: *bool,
496
478
  options: anytype,
497
479
  ) !BeforeMove {
@@ -505,36 +487,41 @@ fn beforeMove(
505
487
 
506
488
  if (Status.is(stored.status, .SLP)) {
507
489
  const before = stored.status;
490
+ const slf = Status.is(stored.status, .EXT);
508
491
  // Even if the EXT bit is set this will still correctly modify the sleep duration
509
- if (options.calc.modify(player, .sleep)) |extend| {
510
- if (!extend) stored.status = 0;
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,
511
498
  } else {
512
499
  stored.status -= 1;
513
500
  }
514
501
 
515
502
  const duration = Status.duration(stored.status);
516
- if (!Status.is(stored.status, .EXT)) {
517
- try options.chance.sleep(player, duration);
518
- }
503
+ try options.chance.sleep(player, if (slf)
504
+ .None
505
+ else if (duration == 0) .ended else .continuing);
519
506
 
520
507
  if (duration == 0) {
521
- try log.curestatus(ident, before, .Message);
508
+ try log.curestatus(.{ ident, before, .Message });
522
509
  stored.status = 0; // clears EXT if present
523
510
  } else {
524
- try log.cant(ident, .Sleep);
511
+ try log.cant(.{ ident, .Sleep });
525
512
  }
526
513
  side.last_used_move = .None;
527
514
  return .done;
528
515
  }
529
516
 
530
517
  if (Status.is(stored.status, .FRZ)) {
531
- try log.cant(ident, .Freeze);
518
+ try log.cant(.{ ident, .Freeze });
532
519
  side.last_used_move = .None;
533
520
  return .done;
534
521
  }
535
522
 
536
523
  if (skip or foe.active.volatiles.Binding) {
537
- try log.cant(ident, .Bound);
524
+ try log.cant(.{ ident, .Bound });
538
525
  return .done;
539
526
  }
540
527
 
@@ -542,27 +529,27 @@ fn beforeMove(
542
529
  // Pokémon Showdown doesn't clear Flinch until its imaginary "residual" phase, meaning
543
530
  // Pokémon can sometimes flinch multiple times from the same original hit
544
531
  if (!showdown) volatiles.Flinch = false;
545
- try log.cant(ident, .Flinch);
532
+ try log.cant(.{ ident, .Flinch });
546
533
  return .done;
547
534
  }
548
535
 
549
536
  if (volatiles.Recharging) {
550
537
  volatiles.Recharging = false;
551
- try log.cant(ident, .Recharge);
538
+ try log.cant(.{ ident, .Recharge });
552
539
  return .done;
553
540
  }
554
541
 
555
- if (volatiles.disabled_duration > 0) {
556
- if (options.calc.modify(player, .disable)) |extend| {
557
- if (!extend) volatiles.disabled_duration = 0;
558
- } else {
559
- volatiles.disabled_duration -= 1;
560
- }
561
- try options.chance.disable(player, volatiles.disabled_duration);
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
+ );
562
549
 
563
- if (volatiles.disabled_duration == 0) {
564
- volatiles.disabled_move = 0;
565
- try log.end(ident, .Disable);
550
+ if (volatiles.disable_duration == 0) {
551
+ volatiles.disable_move = 0;
552
+ try log.end(.{ ident, .Disable });
566
553
  }
567
554
  }
568
555
  // Pokémon Showdown's disable condition has a single onBeforeMove handler
@@ -577,19 +564,31 @@ fn beforeMove(
577
564
 
578
565
  if (volatiles.Confusion) {
579
566
  assert(volatiles.confusion > 0);
580
-
581
- if (options.calc.modify(player, .confusion)) |extend| {
582
- 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,
583
581
  } else {
584
582
  volatiles.confusion -= 1;
585
583
  }
586
- try options.chance.confusion(player, volatiles.confusion);
584
+
585
+ try options.chance.confusion(player, if (volatiles.confusion == 0) .ended else .continuing);
587
586
 
588
587
  if (volatiles.confusion == 0) {
589
588
  volatiles.Confusion = false;
590
- try log.end(ident, .Confusion);
589
+ try log.end(.{ ident, .Confusion });
591
590
  } else {
592
- try log.activate(ident, .Confusion);
591
+ try log.activate(.{ ident, .Confusion });
593
592
 
594
593
  if (try Rolls.confused(battle, player, options)) {
595
594
  assert(!volatiles.MultiHit);
@@ -601,7 +600,9 @@ fn beforeMove(
601
600
  volatiles.Charging = false;
602
601
  volatiles.Binding = false;
603
602
  volatiles.Invulnerable = false;
604
- options.chance.clearDurations(player, null);
603
+
604
+ options.chance.observe(.attacking, player, .None);
605
+ options.chance.observe(.binding, player, .None);
605
606
  {
606
607
  // This feels (and is) disgusting but the cartridge literally just overwrites
607
608
  // the opponent's defense with the user's defense and resets it after. As a
@@ -609,7 +610,9 @@ fn beforeMove(
609
610
  const def = foe.active.stats.def;
610
611
  foe.active.stats.def = active.stats.def;
611
612
  defer foe.active.stats.def = def;
612
- if (!calcDamage(battle, player, player.foe(), null, false)) return .err;
613
+ if (!calcDamage(battle, player, player.foe(), null, false, options)) {
614
+ return .err;
615
+ }
613
616
  }
614
617
  // Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
615
618
  // on the targeting behavior of the confused Pokémon's selected move which results
@@ -638,9 +641,12 @@ fn beforeMove(
638
641
  volatiles.Thrashing = false;
639
642
  volatiles.Charging = false;
640
643
  volatiles.Binding = false;
641
- options.chance.clearDurations(player, null);
644
+
645
+ options.chance.observe(.attacking, player, .None);
646
+ options.chance.observe(.binding, player, .None);
647
+
642
648
  // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
643
- try log.cant(ident, .Paralysis);
649
+ try log.cant(.{ ident, .Paralysis });
644
650
  return .done;
645
651
  }
646
652
 
@@ -656,28 +662,22 @@ fn beforeMove(
656
662
  volatiles.state +%= battle.last_damage;
657
663
  }
658
664
 
659
- assert(volatiles.attacks > 0);
660
-
661
- if (options.calc.modify(player, .attacking)) |extend| {
662
- if (!extend) volatiles.attacks = 0;
663
- } else {
664
- volatiles.attacks -= 1;
665
- }
666
- try options.chance.attacking(player, volatiles.attacks);
665
+ volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
666
+ try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
667
667
 
668
668
  if (volatiles.attacks != 0) {
669
- try log.activate(ident, .Bide);
669
+ try log.activate(.{ ident, .Bide });
670
670
  return .done;
671
671
  }
672
672
 
673
673
  volatiles.Bide = false;
674
- try log.end(ident, .Bide);
674
+ try log.end(.{ ident, .Bide });
675
675
 
676
676
  battle.last_damage = volatiles.state *% 2;
677
677
  volatiles.state = 0;
678
678
 
679
679
  if (battle.last_damage == 0) {
680
- try log.fail(ident, .None);
680
+ try log.fail(.{ ident, .None });
681
681
  return .done;
682
682
  }
683
683
 
@@ -697,47 +697,38 @@ fn beforeMove(
697
697
  }
698
698
 
699
699
  if (volatiles.Thrashing) {
700
- assert(volatiles.attacks > 0);
701
- assert(from != null);
700
+ try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
702
701
 
703
- if (options.calc.modify(player, .attacking)) |extend| {
704
- if (!extend) volatiles.attacks = 0;
705
- } else {
706
- volatiles.attacks -= 1;
707
- }
708
- try options.chance.attacking(player, volatiles.attacks);
702
+ volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
703
+ try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
709
704
 
710
- if (!showdown and handleThrashing(battle, active, player, options)) {
711
- try log.start(battle.active(player), .ConfusionSilent);
712
- }
713
- try log.move(ident, side.last_selected_move, battle.active(player.foe()), from);
714
- if (showdown) {
715
- // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
716
- // used in useMove and doesn't have the notion of skipping canMove semantics
717
- side.last_used_move = side.last_selected_move;
718
- if (handleThrashing(battle, active, player, options)) {
719
- try log.start(battle.active(player), .ConfusionSilent);
720
- }
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);
721
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
+
722
724
  return .skip_can;
723
725
  }
724
726
 
725
727
  if (volatiles.Binding) {
726
- assert(volatiles.attacks > 0);
728
+ volatiles.attacks = decrement(.binding, player, options, volatiles.attacks);
729
+ try options.chance.binding(player, if (volatiles.attacks == 0) .ended else .continuing);
727
730
 
728
- if (options.calc.modify(player, .binding)) |extend| {
729
- if (!extend) volatiles.attacks = 0;
730
- } else {
731
- volatiles.attacks -= 1;
732
- }
733
- try options.chance.binding(player, volatiles.attacks);
734
-
735
- try log.move(
736
- battle.active(player),
737
- side.last_selected_move,
738
- battle.active(player.foe()),
739
- side.last_selected_move,
740
- );
731
+ try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
741
732
  if (showdown or battle.last_damage != 0) {
742
733
  const sub = showdown and foe.active.volatiles.Substitute;
743
734
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -757,7 +748,7 @@ fn canMove(
757
748
  mslot: u4,
758
749
  auto: bool,
759
750
  skip_pp: bool,
760
- from: ?Move,
751
+ from: Move,
761
752
  residual: *bool,
762
753
  options: anytype,
763
754
  ) !bool {
@@ -765,33 +756,21 @@ fn canMove(
765
756
  const player_ident = battle.active(player);
766
757
  const move = Move.get(side.last_selected_move);
767
758
 
768
- const foe_last = battle.foe(player).last_used_move;
769
- const special = from != null and (from.? == .Metronome and
770
- (from.? == .MirrorMove and !(foe_last == .None or foe_last == .MirrorMove)));
771
-
772
- var skip = skip_pp;
773
759
  if (side.active.volatiles.Charging) {
774
760
  side.active.volatiles.Charging = false;
775
761
  side.active.volatiles.Invulnerable = false;
776
762
  } else if (move.effect == .Charge) {
777
- try options.log.move(player_ident, side.last_selected_move, .{}, from);
763
+ try options.log.move(.{ player_ident, side.last_selected_move, ID{}, from });
778
764
  setCounterable(battle, player, side, move);
779
765
  try Effects.charge(battle, player, options);
780
766
  return false;
781
767
  }
782
768
 
783
- if (!showdown or !special) side.last_used_move = side.last_selected_move;
784
- if (!skip) decrementPP(side, mslot, auto);
769
+ side.last_used_move = side.last_selected_move;
770
+ if (!skip_pp) decrementPP(side, mslot, auto);
785
771
 
786
772
  const target = if (move.target == .Self) player else player.foe();
787
- // Pokémon Showdown's protocol for Rage and Binding moves should come with a [from], though we
788
- // don't have space to be able to track it in all circumstances and thus usually try to infer it
789
- // from side.last_used_move. However, this gets reset by the opponent switching or fainting,
790
- // meaning sometimes we lose this information. from is still set because it not being null is
791
- // relevant for control flow, but we don't have anything useful to put in the log. This is
792
- // regrettable, but this information is more "nice to have" than required
793
- const f = if (from != null and from.? == .None) null else from;
794
- try options.log.move(player_ident, side.last_selected_move, battle.active(target), f);
773
+ try options.log.move(.{ player_ident, side.last_selected_move, battle.active(target), from });
795
774
  setCounterable(battle, player, side, move);
796
775
 
797
776
  if (move.effect.onBegin()) {
@@ -829,16 +808,18 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
829
808
  assert(!volatiles.Rage and !volatiles.Thrashing and !volatiles.MultiHit);
830
809
  if (volatiles.Bide) return;
831
810
 
832
- assert(active.move(mslot).pp > 0 or auto);
833
- 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;
834
814
  if (volatiles.Transform) return;
835
815
 
836
- assert(side.stored().move(mslot).pp > 0 or auto);
837
- side.stored().move(mslot).pp = @as(u6, @intCast(side.stored().move(mslot).pp)) -% 1;
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;
838
819
  assert(active.move(mslot).pp == side.stored().move(mslot).pp);
839
820
  }
840
821
 
841
- fn incrementPP(side: *Side, mslot: u4) void {
822
+ fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
842
823
  var active = &side.active;
843
824
  const volatiles = &active.volatiles;
844
825
 
@@ -846,8 +827,11 @@ fn incrementPP(side: *Side, mslot: u4) void {
846
827
  // GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
847
828
  if (showdown and volatiles.Transform) return;
848
829
 
830
+ const n = options.calc.overridden(player, .pp) orelse 1;
831
+ if (volatiles.Transform and !options.chance.pp(player, n)) return;
832
+
849
833
  assert(mslot > 0 and mslot <= 4);
850
- 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;
851
835
  }
852
836
 
853
837
  // Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
@@ -895,13 +879,13 @@ fn doMove(
895
879
  break :miss true;
896
880
  }
897
881
  break :miss (move.target != .Self and
898
- !moveHit(battle, player, move, &immune, &mist, options));
882
+ !try moveHit(battle, player, move, &immune, &mist, options));
899
883
  };
900
884
  assert(!immune or miss or (showdown and move.effect == .Binding));
901
885
 
902
886
  var late = showdown and move.effect != .Explode;
903
887
  const skip = status or immune;
904
- if ((!showdown or (!skip or counter)) and !miss) blk: {
888
+ if ((!showdown or (!skip or counter)) and !miss) damage: {
905
889
  if (showdown and move.effect.isMulti()) {
906
890
  try Effects.multiHit(battle, player, move, options);
907
891
  hits = side.active.volatiles.attacks;
@@ -923,8 +907,9 @@ fn doMove(
923
907
  ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
924
908
  // This can overflow after adjustDamage, but will still be sufficient to OHKO
925
909
  battle.last_damage = if (ohko) 65535 else 0;
926
- if (showdown) break :blk; // skip adjustDamage / randomizeDamage
927
- } else if (!calcDamage(battle, player, player.foe(), move, crit)) {
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);
928
913
  return @as(?Result, Result.Error);
929
914
  }
930
915
  if (battle.last_damage == 0) {
@@ -938,11 +923,12 @@ fn doMove(
938
923
  }
939
924
  }
940
925
 
941
- var zero = battle.last_damage == 0;
942
- miss = if (showdown or skip)
943
- miss
944
- else
945
- (!moveHit(battle, player, move, &immune, &mist, options) or zero);
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
+ }
946
932
 
947
933
  assert(showdown or miss or battle.last_damage > 0 or skip);
948
934
  assert((!showdown and miss) or !(ohko and immune));
@@ -966,16 +952,37 @@ fn doMove(
966
952
  ohko = (!showdown or (!immune and !invulnerable)) and
967
953
  move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
968
954
  if (ohko) {
969
- try log.immune(foe_ident, .OHKO);
955
+ try log.immune(.{ foe_ident, .OHKO });
970
956
  } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
971
- 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
+ }
972
979
  } else if (mist) {
973
- if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
974
- 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 });
975
982
  } else {
976
- if (showdown or !zero) try options.chance.commit(player, .miss);
977
- try log.lastmiss();
978
- 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)});
979
986
  }
980
987
  if (move.effect == .JumpKick) {
981
988
  // Recoil is supposed to be damage/8 but damage will always be 0 here
@@ -988,17 +995,15 @@ fn doMove(
988
995
  try buildRage(battle, player.foe(), options);
989
996
  } else if (showdown and move.effect == .Disable) {
990
997
  try buildRage(battle, player.foe(), options);
991
- } else if (!showdown and immune and move.effect == .Binding) {
992
- try options.chance.commit(player, .binding);
993
998
  }
994
999
  return null;
995
- } else {
1000
+ } else if (!showdown) {
996
1001
  try options.chance.commit(player, .hit);
997
1002
  }
998
1003
 
999
1004
  // On the cartridge MultiHit doesn't get set up until after damage has been applied for the
1000
1005
  // first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
1001
- // it up above before damage calculation).
1006
+ // it up above before damage calculation)
1002
1007
  if (!showdown and move.effect.isMulti()) {
1003
1008
  try Effects.multiHit(battle, player, move, options);
1004
1009
  hits = side.active.volatiles.attacks;
@@ -1011,15 +1016,15 @@ fn doMove(
1011
1016
  var hit: u4 = 0;
1012
1017
  while (hit < hits) {
1013
1018
  if (hit == 0) {
1014
- if (crit) try log.crit(battle.active(player.foe()));
1019
+ if (crit) try log.crit(.{battle.active(player.foe())});
1015
1020
  if (effectiveness > Effectiveness.neutral) {
1016
- try log.supereffective(battle.active(player.foe()));
1021
+ try log.supereffective(.{battle.active(player.foe())});
1017
1022
  } else if (effectiveness < Effectiveness.neutral) {
1018
- try log.resisted(battle.active(player.foe()));
1023
+ try log.resisted(.{battle.active(player.foe())});
1019
1024
  }
1020
1025
  }
1021
1026
  if (!skip) nullified = try applyDamage(battle, player.foe(), player.foe(), .None, options);
1022
- if (hit == 0 and ohko) try log.ohko();
1027
+ if (hit == 0 and ohko) try log.ohko(.{});
1023
1028
  hit += 1;
1024
1029
  if (foe.stored().hp == 0) break;
1025
1030
  if (!late and (!sub or move.effect == .Explode)) {
@@ -1036,7 +1041,7 @@ fn doMove(
1036
1041
  if (showdown and move.effect == .Twineedle and !nullified and foe.stored().hp > 0) {
1037
1042
  try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1038
1043
  }
1039
- try log.hitcount(battle.active(player.foe()), hit);
1044
+ try log.hitcount(.{ battle.active(player.foe()), hit });
1040
1045
  } else if (showdown) {
1041
1046
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
1042
1047
  if (move.effect == .Binding) {
@@ -1046,9 +1051,9 @@ fn doMove(
1046
1051
  assert(foe.stored().hp > 0);
1047
1052
  // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
1048
1053
  if (sub) {
1049
- try log.activate(battle.active(player.foe()), .Substitute);
1054
+ try log.activate(.{ battle.active(player.foe()), .Substitute });
1050
1055
  } else {
1051
- try log.damage(battle.active(player.foe()), foe.stored(), .None);
1056
+ try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
1052
1057
  try buildRage(battle, player.foe(), options);
1053
1058
  }
1054
1059
  return null;
@@ -1057,12 +1062,12 @@ fn doMove(
1057
1062
  }
1058
1063
 
1059
1064
  // Substitute being broken nullifies the move's effect completely so even
1060
- // if an effect was intended to "always happen" it will still get skipped.
1065
+ // if an effect was intended to "always happen" it will still get skipped
1061
1066
  if (nullified) return null;
1062
1067
 
1063
1068
  // On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
1064
1069
  // but this is only done to setup the MultiHit looping in the first place. Moving the MultiHit
1065
- // setup before the loop means we can avoid having to waste time doing no-op handler searches.
1070
+ // setup before the loop means we can avoid having to waste time doing no-op handler searches
1066
1071
  if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
1067
1072
 
1068
1073
  if (foe.stored().hp == 0) return null;
@@ -1077,7 +1082,7 @@ fn doMove(
1077
1082
  // below). For Twineedle we change the data to that of one with PoisonChance1 given its
1078
1083
  // MultiHit behavior is complete after the loop above, though Pokémon Showdown handles the
1079
1084
  // Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
1080
- // puts the |-status| message before |-hitcount| instead of after.
1085
+ // puts the |-status| message before |-hitcount| instead of after
1081
1086
  if (move.effect == .Twineedle) {
1082
1087
  if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1083
1088
  } else if (move.effect == .Disable) {
@@ -1096,12 +1101,14 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
1096
1101
  const side = battle.side(player);
1097
1102
 
1098
1103
  // Base speed is used for the critical hit calculation, even when Transform-ed
1099
- var rate = @as(u16, Species.chance(side.stored().species));
1104
+ var rate: u16 = Species.chance(side.stored().species);
1100
1105
  // GLITCH: Focus Energy reduces critical hit rate instead of increasing it
1101
1106
  rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
1102
1107
  rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
1103
1108
 
1104
- 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);
1105
1112
  }
1106
1113
 
1107
1114
  fn calcDamage(
@@ -1110,6 +1117,7 @@ fn calcDamage(
1110
1117
  target_player: Player,
1111
1118
  m: ?Move.Data,
1112
1119
  crit: bool,
1120
+ options: anytype,
1113
1121
  ) bool {
1114
1122
  // Confusion (indicated when m == null) just needs a 40 BP physical move
1115
1123
  const cfz = m == null;
@@ -1121,28 +1129,8 @@ fn calcDamage(
1121
1129
 
1122
1130
  const special = move.type.special();
1123
1131
 
1124
- // zig fmt: off
1125
- var atk: u32 =
1126
- if (crit)
1127
- if (special) side.stored().stats.spc
1128
- else side.stored().stats.atk
1129
- else
1130
- if (special) side.active.stats.spc
1131
- else side.active.stats.atk;
1132
- var def: u32 =
1133
- if (crit)
1134
- if (special) target.stored().stats.spc
1135
- else target.stored().stats.def
1136
- else
1137
- // GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
1138
- if (special)
1139
- target.active.stats.spc *
1140
- @as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
1141
- // Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
1142
- else
1143
- target.active.stats.def *
1144
- @as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
1145
- // zig fmt: on
1132
+ var atk = attack(side, crit, special);
1133
+ var def = defense(target, crit, special, cfz);
1146
1134
 
1147
1135
  // Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
1148
1136
  // will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
@@ -1153,9 +1141,9 @@ fn calcDamage(
1153
1141
  def = @max((def / 4) & 255, if (showdown) 1 else 0);
1154
1142
  }
1155
1143
 
1156
- 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);
1157
1145
 
1158
- 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;
1159
1147
 
1160
1148
  if (def == 0) return false;
1161
1149
 
@@ -1167,11 +1155,41 @@ fn calcDamage(
1167
1155
  d = @min(997, d);
1168
1156
  d += 2;
1169
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
+
1170
1168
  battle.last_damage = @intCast(d);
1171
1169
 
1172
1170
  return true;
1173
1171
  }
1174
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
+
1175
1193
  fn adjustDamage(battle: anytype, player: Player) u16 {
1176
1194
  const side = battle.side(player);
1177
1195
  const foe = battle.foe(player);
@@ -1216,7 +1234,6 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1216
1234
  const foe = battle.foe(player);
1217
1235
 
1218
1236
  if (!try checkHit(battle, player, move, options)) return null;
1219
- try options.chance.commit(player, .hit);
1220
1237
 
1221
1238
  battle.last_damage = switch (side.last_selected_move) {
1222
1239
  .SuperFang => @max(foe.stored().hp / 2, 1),
@@ -1246,7 +1263,7 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1246
1263
  const foe = battle.foe(player);
1247
1264
 
1248
1265
  if (battle.last_damage == 0) {
1249
- try options.log.fail(battle.active(player), .None);
1266
+ try options.log.fail(.{ battle.active(player), .None });
1250
1267
  return null;
1251
1268
  }
1252
1269
 
@@ -1264,22 +1281,21 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1264
1281
  foe_last_selected_move.type == .Fighting);
1265
1282
 
1266
1283
  if (!used and !selected) {
1267
- try options.log.fail(battle.active(player), .None);
1284
+ try options.log.fail(.{ battle.active(player), .None });
1268
1285
  return null;
1269
1286
  }
1270
1287
 
1271
1288
  if (!used or !selected) {
1272
1289
  // GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
1273
1290
  if (!showdown) return Result.Error;
1274
- try options.log.fail(battle.active(player), .None);
1291
+ try options.log.fail(.{ battle.active(player), .None });
1275
1292
  return null;
1276
1293
  }
1277
1294
 
1278
1295
  battle.last_damage *|= 2;
1279
1296
 
1280
- // Pokémon Showdown calls checkHit before Counter
1297
+ // Pokémon Showdown calls moveHit before Counter
1281
1298
  if (!showdown and !try checkHit(battle, player, move, options)) return null;
1282
- try options.chance.commit(player, .hit);
1283
1299
 
1284
1300
  const sub = showdown and foe.active.volatiles.Substitute;
1285
1301
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -1307,20 +1323,20 @@ fn applyDamage(
1307
1323
  subbed.active.volatiles.substitute = 0;
1308
1324
  subbed.active.volatiles.Substitute = false;
1309
1325
  // battle.last_damage is not updated with the amount of HP the Substitute had
1310
- try options.log.end(battle.active(sub_player), .Substitute);
1311
- options.calc.capped(target_player);
1326
+ try options.log.end(.{ battle.active(sub_player), .Substitute });
1327
+ options.calc.capped(sub_player);
1312
1328
  return true;
1313
1329
  } else {
1314
1330
  // Safe to truncate since less than subbed.volatiles.substitute which is a u8
1315
1331
  subbed.active.volatiles.substitute -= @intCast(battle.last_damage);
1316
- try options.log.activate(battle.active(sub_player), .Substitute);
1332
+ try options.log.activate(.{ battle.active(sub_player), .Substitute });
1317
1333
  return false;
1318
1334
  }
1319
1335
  }
1320
1336
 
1321
1337
  if (battle.last_damage > target.stored().hp) battle.last_damage = target.stored().hp;
1322
1338
  target.stored().hp -= battle.last_damage;
1323
- try options.log.damage(battle.active(target_player), target.stored(), reason);
1339
+ try options.log.damage(.{ battle.active(target_player), target.stored(), reason });
1324
1340
  return false;
1325
1341
  }
1326
1342
 
@@ -1339,10 +1355,10 @@ fn mirrorMove(
1339
1355
  side.last_selected_move = foe.last_used_move;
1340
1356
 
1341
1357
  if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
1342
- try options.log.fail(battle.active(player), .None);
1358
+ try options.log.fail(.{ battle.active(player), .None });
1343
1359
  return null;
1344
1360
  } else if (!showdown or foe.last_used_move != .Struggle) {
1345
- incrementPP(side, mslot);
1361
+ incrementPP(side, player, mslot, options);
1346
1362
  }
1347
1363
 
1348
1364
  if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
@@ -1363,7 +1379,7 @@ fn metronome(
1363
1379
  var side = battle.side(player);
1364
1380
 
1365
1381
  side.last_selected_move = try Rolls.metronome(battle, player, options);
1366
- incrementPP(side, mslot);
1382
+ incrementPP(side, player, mslot, options);
1367
1383
 
1368
1384
  if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
1369
1385
  return null;
@@ -1375,20 +1391,23 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
1375
1391
  var immune = false;
1376
1392
  var mist = false;
1377
1393
 
1378
- 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
+ }
1379
1398
 
1380
1399
  assert(!immune);
1381
1400
  if (mist) {
1382
1401
  assert(!showdown);
1383
1402
  const foe_ident = battle.active(player.foe());
1384
- try options.log.activate(foe_ident, .Mist);
1385
- try options.log.fail(foe_ident, .None);
1403
+ try options.log.activate(.{ foe_ident, .Mist });
1404
+ try options.log.fail(.{ foe_ident, .None });
1386
1405
  } else {
1387
- try options.chance.commit(player, .miss);
1388
- try options.log.lastmiss();
1389
- try options.log.miss(battle.active(player));
1406
+ try options.log.lastmiss(.{});
1407
+ try options.log.miss(.{battle.active(player)});
1390
1408
  }
1391
1409
 
1410
+ if (!showdown) try options.chance.commit(player, .miss);
1392
1411
  return false;
1393
1412
  }
1394
1413
 
@@ -1399,7 +1418,7 @@ fn moveHit(
1399
1418
  immune: *bool,
1400
1419
  mist: *bool,
1401
1420
  options: anytype,
1402
- ) bool {
1421
+ ) !bool {
1403
1422
  var side = battle.side(player);
1404
1423
  const foe = battle.foe(player);
1405
1424
 
@@ -1438,7 +1457,7 @@ fn moveHit(
1438
1457
  const overwritten = overwrite and state > 0;
1439
1458
  assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
1440
1459
 
1441
- var accuracy = if (overwritten) state else @as(u16, Gen12.percent(move.accuracy));
1460
+ var accuracy: u16 = if (overwritten) state else move.accuracy;
1442
1461
  var boost = BOOSTS[@as(u4, @intCast(@as(i8, side.active.boosts.accuracy) + 6))];
1443
1462
  accuracy = accuracy * boost[0] / boost[1];
1444
1463
  boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
@@ -1450,7 +1469,7 @@ fn moveHit(
1450
1469
  if (overwrite) side.active.volatiles.state = accuracy;
1451
1470
 
1452
1471
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1453
- break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
1472
+ break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
1454
1473
  };
1455
1474
 
1456
1475
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1461,13 +1480,17 @@ fn moveHit(
1461
1480
 
1462
1481
  if (!miss) return true;
1463
1482
  if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
1483
+
1464
1484
  side.active.volatiles.Binding = false;
1485
+ options.chance.observe(.binding, player, .None);
1486
+
1465
1487
  return false;
1466
1488
  }
1467
1489
 
1468
1490
  fn checkFaint(
1469
1491
  battle: anytype,
1470
1492
  player: Player,
1493
+ cap: bool,
1471
1494
  options: anytype,
1472
1495
  ) @TypeOf(options.log).Error!?Result {
1473
1496
  const side = battle.side(player);
@@ -1477,33 +1500,32 @@ fn checkFaint(
1477
1500
  const foe_fainted = foe.stored().hp == 0;
1478
1501
 
1479
1502
  const player_out = findFirstAlive(side) == 0;
1480
- const foe_out = findFirstAlive(foe) == 0;
1481
- const tie = player_out and foe_out;
1482
- const more = tie or player_out or foe_out;
1503
+ const foe_out = foe_fainted and findFirstAlive(foe) == 0;
1504
+ const more = player_out or foe_out;
1483
1505
 
1484
- if (try faint(battle, player, !(more or foe_fainted), options)) |r| return r;
1485
- if (foe_fainted) if (try faint(battle, player.foe(), !more, options)) |r| return r;
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;
1486
1508
 
1487
1509
  assert(!side.active.volatiles.MultiHit);
1488
1510
  assert(!foe.active.volatiles.MultiHit);
1489
1511
 
1490
- if (tie) {
1491
- try options.log.tie();
1512
+ if (player_out and foe_out) {
1513
+ try options.log.tie(.{});
1492
1514
  return Result.Tie;
1493
1515
  } else if (player_out) {
1494
- try options.log.win(player.foe());
1516
+ try options.log.win(.{player.foe()});
1495
1517
  return if (player == .P1) Result.Lose else Result.Win;
1496
1518
  } else if (foe_out) {
1497
- try options.log.win(player);
1519
+ try options.log.win(.{player});
1498
1520
  return if (player == .P1) Result.Win else Result.Lose;
1499
1521
  }
1500
1522
 
1501
1523
  const foe_choice: Choice.Type = if (foe_fainted) .Switch else .Pass;
1502
- if (player == .P1) return Result{ .p1 = .Switch, .p2 = foe_choice };
1503
- return Result{ .p1 = foe_choice, .p2 = .Switch };
1524
+ if (player == .P1) return .{ .p1 = .Switch, .p2 = foe_choice };
1525
+ return .{ .p1 = foe_choice, .p2 = .Switch };
1504
1526
  }
1505
1527
 
1506
- 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 {
1507
1529
  var side = battle.side(player);
1508
1530
  var foe = battle.foe(player);
1509
1531
  assert(side.stored().hp == 0);
@@ -1536,8 +1558,10 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
1536
1558
  side.stored().stats.spe;
1537
1559
  }
1538
1560
 
1539
- try options.log.faint(battle.active(player), done);
1540
- 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);
1541
1565
  return null;
1542
1566
  }
1543
1567
 
@@ -1548,21 +1572,19 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1548
1572
  var volatiles = &side.active.volatiles;
1549
1573
 
1550
1574
  const brn = Status.is(stored.status, .BRN);
1551
- if (brn or Status.is(stored.status, .PSN)) blk: {
1575
+ if (brn or Status.is(stored.status, .PSN)) {
1552
1576
  var damage = @max(stored.stats.hp / 16, 1);
1553
-
1554
1577
  if (volatiles.Toxic) {
1555
1578
  volatiles.toxic += 1;
1556
1579
  damage *= volatiles.toxic;
1557
1580
  }
1581
+ damage = @min(damage, stored.hp);
1558
1582
 
1559
- const amount = @min(damage, stored.hp);
1560
- if (showdown and amount == 0) break :blk;
1561
-
1562
- stored.hp -= amount;
1563
-
1564
- // Pokémon Showdown uses damageOf here but its not relevant in Generation I
1565
- try options.log.damage(ident, stored, if (brn) Damage.Burn else Damage.Poison);
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
+ }
1566
1588
  }
1567
1589
 
1568
1590
  if (volatiles.LeechSeed) {
@@ -1586,14 +1608,14 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1586
1608
  const amount = @min(damage, stored.hp);
1587
1609
  stored.hp -= amount;
1588
1610
 
1589
- // As above, Pokémon Showdown uses damageOf but its not relevant
1590
- 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 });
1591
1613
 
1592
1614
  const before = foe_stored.hp;
1593
1615
  // Uncapped damage is added back to the foe
1594
1616
  foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
1595
1617
  // Pokémon Showdown uses the less specific heal here instead of drain... because reasons?
1596
- if (foe_stored.hp > before) try options.log.heal(foe_ident, foe_stored, .Silent);
1618
+ if (foe_stored.hp > before) try options.log.heal(.{ foe_ident, foe_stored, .Silent });
1597
1619
  }
1598
1620
  }
1599
1621
 
@@ -1604,18 +1626,18 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
1604
1626
  battle.turn += 1;
1605
1627
 
1606
1628
  if (showdown and pkmn.options.ebc and checkEBC(battle)) {
1607
- try options.log.tie();
1629
+ try options.log.tie(.{});
1608
1630
  return Result.Tie;
1609
1631
  }
1610
1632
 
1611
1633
  if (showdown and battle.turn >= 1000) {
1612
- try options.log.tie();
1634
+ try options.log.tie(.{});
1613
1635
  return Result.Tie;
1614
1636
  } else if (battle.turn >= 65535) {
1615
1637
  return Result.Error;
1616
1638
  }
1617
1639
 
1618
- try options.log.turn(battle.turn);
1640
+ try options.log.turn(.{battle.turn});
1619
1641
 
1620
1642
  return Result.Default;
1621
1643
  }
@@ -1675,9 +1697,9 @@ fn checkEBC(battle: anytype) bool {
1675
1697
  }
1676
1698
 
1677
1699
  fn disabled(side: *Side, ident: ID, options: anytype) !bool {
1678
- if (side.active.volatiles.disabled_move != 0) {
1700
+ if (side.active.volatiles.disable_move != 0) {
1679
1701
  // A Pokémon that transforms after being disabled may end up with less move slots
1680
- const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
1702
+ const m = side.active.moves[side.active.volatiles.disable_move - 1].id;
1681
1703
  // side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
1682
1704
  const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
1683
1705
  m
@@ -1685,32 +1707,21 @@ fn disabled(side: *Side, ident: ID, options: anytype) !bool {
1685
1707
  side.last_selected_move;
1686
1708
  if (m != .None and m == last) {
1687
1709
  side.active.volatiles.Charging = false;
1688
- try options.log.disabled(ident, last);
1710
+ try options.log.cant(.{ ident, .Disable, last });
1689
1711
  return true;
1690
1712
  }
1691
1713
  }
1692
1714
  return false;
1693
1715
  }
1694
1716
 
1695
- inline fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1717
+ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1696
1718
  const side = battle.side(who);
1697
1719
  if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
1698
1720
  try Effects.boost(battle, who, Move.get(.Rage), options);
1699
1721
  }
1700
1722
  }
1701
1723
 
1702
- fn handleThrashing(battle: anytype, active: *ActivePokemon, player: Player, options: anytype) bool {
1703
- var volatiles = &active.volatiles;
1704
- assert(volatiles.Thrashing);
1705
- if (volatiles.attacks > 0) return false;
1706
-
1707
- volatiles.Thrashing = false;
1708
- volatiles.Confusion = true;
1709
- volatiles.confusion = Rolls.confusionDuration(battle, player, true, options);
1710
- return true;
1711
- }
1712
-
1713
- inline fn onBegin(
1724
+ fn onBegin(
1714
1725
  battle: anytype,
1715
1726
  player: Player,
1716
1727
  move: Move.Data,
@@ -1718,7 +1729,6 @@ inline fn onBegin(
1718
1729
  residual: *bool,
1719
1730
  options: anytype,
1720
1731
  ) !void {
1721
- assert(move.effect.onBegin());
1722
1732
  return switch (move.effect) {
1723
1733
  .Confusion => Effects.confusion(battle, player, move, options),
1724
1734
  .Conversion => Effects.conversion(battle, player, options),
@@ -1740,8 +1750,7 @@ inline fn onBegin(
1740
1750
  };
1741
1751
  }
1742
1752
 
1743
- inline fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1744
- assert(move.effect.onEnd());
1753
+ fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1745
1754
  return switch (move.effect) {
1746
1755
  .Bide => Effects.bide(battle, player, options),
1747
1756
  // zig fmt: off
@@ -1756,14 +1765,13 @@ inline fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anyty
1756
1765
  };
1757
1766
  }
1758
1767
 
1759
- inline fn alwaysHappens(
1768
+ fn alwaysHappens(
1760
1769
  battle: anytype,
1761
1770
  player: Player,
1762
1771
  move: Move.Data,
1763
1772
  residual: *bool,
1764
1773
  options: anytype,
1765
1774
  ) !void {
1766
- assert(move.effect.alwaysHappens());
1767
1775
  return switch (move.effect) {
1768
1776
  .DrainHP, .DreamEater => Effects.drainHP(battle, player, options),
1769
1777
  .Explode => Effects.explode(battle, player),
@@ -1775,7 +1783,7 @@ inline fn alwaysHappens(
1775
1783
  };
1776
1784
  }
1777
1785
 
1778
- 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 {
1779
1787
  return switch (move.effect) {
1780
1788
  .BurnChance1, .BurnChance2 => Effects.burnChance(battle, player, move, options),
1781
1789
  .ConfusionChance => Effects.confusion(battle, player, move, options),
@@ -1799,10 +1807,12 @@ pub const Effects = struct {
1799
1807
 
1800
1808
  side.active.volatiles.Bide = true;
1801
1809
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1810
+
1802
1811
  side.active.volatiles.state = 0;
1803
1812
  side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
1813
+ options.chance.observe(.attacking, player, .started);
1804
1814
 
1805
- try options.log.start(battle.active(player), .Bide);
1815
+ try options.log.start(.{ battle.active(player), .Bide });
1806
1816
  }
1807
1817
 
1808
1818
  fn burnChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1816,11 +1826,11 @@ pub const Effects = struct {
1816
1826
  // GLITCH: Freeze top move selection desync can occur if thawed Pokémon is slower
1817
1827
  if (Status.is(foe_stored.status, .FRZ)) {
1818
1828
  assert(move.type == .Fire);
1819
- try options.log.curestatus(
1829
+ try options.log.curestatus(.{
1820
1830
  battle.active(player.foe()),
1821
1831
  foe_stored.status,
1822
1832
  .Message,
1823
- );
1833
+ });
1824
1834
  foe_stored.status = 0;
1825
1835
  }
1826
1836
  return;
@@ -1834,7 +1844,7 @@ pub const Effects = struct {
1834
1844
  foe_stored.status = Status.init(.BRN);
1835
1845
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
1836
1846
 
1837
- 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 });
1838
1848
  }
1839
1849
 
1840
1850
  fn charge(battle: anytype, player: Player, options: anytype) !void {
@@ -1844,8 +1854,8 @@ pub const Effects = struct {
1844
1854
  volatiles.Charging = true;
1845
1855
  const move = side.last_selected_move;
1846
1856
  if (move == .Fly or move == .Dig) volatiles.Invulnerable = true;
1847
- try options.log.laststill();
1848
- try options.log.prepare(battle.active(player), move);
1857
+ try options.log.laststill(.{});
1858
+ try options.log.prepare(.{ battle.active(player), move });
1849
1859
  }
1850
1860
 
1851
1861
  fn confusion(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1856,42 +1866,38 @@ pub const Effects = struct {
1856
1866
  if (!try Rolls.confusionChance(battle, player, options)) return;
1857
1867
  } else {
1858
1868
  if (showdown) {
1859
- if (!try checkHit(battle, player, move, options)) {
1860
- return;
1861
- } else if (sub) {
1862
- return options.log.fail(battle.active(player.foe()), .None);
1863
- }
1869
+ if (!try checkHit(battle, player, move, options)) return;
1870
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1864
1871
  } else {
1865
- if (sub) {
1866
- return options.log.fail(battle.active(player.foe()), .None);
1867
- } else if (!try checkHit(battle, player, move, options)) {
1868
- return;
1869
- }
1872
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1873
+ if (!try checkHit(battle, player, move, options)) return;
1870
1874
  }
1871
- try options.chance.commit(player, .hit);
1872
1875
  }
1873
1876
 
1874
1877
  if (foe.active.volatiles.Confusion) return;
1875
1878
  foe.active.volatiles.Confusion = true;
1876
- foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
1877
1879
 
1878
- 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 });
1879
1884
  }
1880
1885
 
1881
1886
  fn conversion(battle: anytype, player: Player, options: anytype) !void {
1882
1887
  const foe = battle.foe(player);
1883
1888
 
1884
1889
  if (foe.active.volatiles.Invulnerable) {
1885
- try options.log.lastmiss();
1886
- return options.log.miss(battle.active(player));
1890
+ try options.log.lastmiss(.{});
1891
+ return options.log.miss(.{battle.active(player)});
1887
1892
  }
1888
1893
 
1889
1894
  battle.side(player).active.types = foe.active.types;
1890
- return options.log.typechange(
1895
+ return options.log.start(.{
1891
1896
  battle.active(player),
1897
+ .TypeChange,
1892
1898
  foe.active.types,
1893
1899
  battle.active(player.foe()),
1894
- );
1900
+ });
1895
1901
  }
1896
1902
 
1897
1903
  fn disable(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
@@ -1900,13 +1906,10 @@ pub const Effects = struct {
1900
1906
  const foe_ident = battle.active(player.foe());
1901
1907
 
1902
1908
  // Pokémon Showdown handles hit/miss earlier in doMove
1903
- if (!showdown) {
1904
- if (!try checkHit(battle, player, move, options)) return null;
1905
- try options.chance.commit(player, .hit);
1906
- }
1909
+ if (!showdown and !try checkHit(battle, player, move, options)) return null;
1907
1910
 
1908
- if (volatiles.disabled_move != 0) {
1909
- try options.log.fail(foe_ident, .None);
1911
+ if (volatiles.disable_move != 0) {
1912
+ try options.log.fail(.{ foe_ident, .None });
1910
1913
  return null;
1911
1914
  }
1912
1915
 
@@ -1922,7 +1925,7 @@ pub const Effects = struct {
1922
1925
  // Technically this is still considered simply a "miss" on the cartridge,
1923
1926
  // but diverging from Pokémon Showdown here would mostly just be pedantic
1924
1927
  if (n == 0) {
1925
- try options.log.fail(foe_ident, .None);
1928
+ try options.log.fail(.{ foe_ident, .None });
1926
1929
  return null;
1927
1930
  } else if (err) {
1928
1931
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
@@ -1930,18 +1933,19 @@ pub const Effects = struct {
1930
1933
  return Result.Error;
1931
1934
  }
1932
1935
 
1933
- volatiles.disabled_move =
1936
+ volatiles.disable_move =
1934
1937
  @intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
1935
- 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);
1936
1940
 
1937
- const id = foe.active.move(volatiles.disabled_move).id;
1938
- 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 });
1939
1943
  return null;
1940
1944
  }
1941
1945
 
1942
1946
  fn drainHP(battle: anytype, player: Player, options: anytype) !void {
1943
- var side = battle.side(player);
1944
- var stored = side.stored();
1947
+ var stored = battle.side(player).stored();
1948
+ const ident = battle.active(player);
1945
1949
 
1946
1950
  if (battle.last_damage == 0) {
1947
1951
  assert(showdown);
@@ -1954,7 +1958,7 @@ pub const Effects = struct {
1954
1958
  if (stored.hp == stored.stats.hp) return;
1955
1959
  stored.hp = @min(stored.stats.hp, stored.hp + drain);
1956
1960
 
1957
- try options.log.drain(battle.active(player), stored, battle.active(player.foe()));
1961
+ try options.log.heal(.{ ident, stored, .Drain, battle.active(player.foe()) });
1958
1962
  }
1959
1963
 
1960
1964
  fn explode(battle: anytype, player: Player) !void {
@@ -1986,7 +1990,7 @@ pub const Effects = struct {
1986
1990
  if (side.active.volatiles.FocusEnergy) return;
1987
1991
  side.active.volatiles.FocusEnergy = true;
1988
1992
 
1989
- try options.log.start(battle.active(player), .FocusEnergy);
1993
+ try options.log.start(.{ battle.active(player), .FocusEnergy });
1990
1994
  }
1991
1995
 
1992
1996
  fn freezeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2008,7 +2012,7 @@ pub const Effects = struct {
2008
2012
  foe_stored.status = Status.init(.FRZ);
2009
2013
  // GLITCH: Hyper Beam recharging status is not cleared
2010
2014
 
2011
- try options.log.status(foe_ident, foe_stored.status, .None);
2015
+ try options.log.status(.{ foe_ident, foe_stored.status, .None });
2012
2016
  }
2013
2017
 
2014
2018
  fn haze(battle: anytype, player: Player, options: anytype) !void {
@@ -2016,7 +2020,7 @@ pub const Effects = struct {
2016
2020
  var side = battle.side(player);
2017
2021
  var foe = battle.foe(player);
2018
2022
 
2019
- var side_stored = side.stored();
2023
+ const side_stored = side.stored();
2020
2024
  var foe_stored = foe.stored();
2021
2025
 
2022
2026
  const player_ident = battle.active(player);
@@ -2028,8 +2032,8 @@ pub const Effects = struct {
2028
2032
  side.active.stats = side_stored.stats;
2029
2033
  foe.active.stats = foe_stored.stats;
2030
2034
 
2031
- try log.activate(player_ident, .Haze);
2032
- try log.clearallboost();
2035
+ try log.activate(.{ player_ident, .Haze });
2036
+ try log.clearallboost(.{});
2033
2037
 
2034
2038
  // Pokémon Showdown clears P1 then P2 instead of status -> side -> foe
2035
2039
  if (showdown) {
@@ -2037,24 +2041,26 @@ pub const Effects = struct {
2037
2041
  const p: Player = @enumFromInt(i);
2038
2042
  // Pokémon Showdown incorrectly does not prevent sleep/freeze from moving
2039
2043
  if (p != player and Status.any(s.stored().status)) {
2040
- try log.curestatus(foe_ident, foe_stored.status, .Silent);
2044
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2041
2045
  s.stored().status = 0;
2046
+ options.chance.observe(.sleep, p, .None);
2042
2047
  } else if (showdown and s.stored().status == Status.TOX) {
2043
2048
  s.stored().status = Status.init(.PSN);
2044
- try log.status(battle.active(p), s.stored().status, .None);
2049
+ try log.status(.{ battle.active(p), s.stored().status, .None });
2045
2050
  }
2046
- try clearVolatiles(battle, p, p != player, options);
2051
+ try clearVolatiles(battle, p, options);
2047
2052
  }
2048
2053
  } else {
2049
2054
  if (Status.any(foe_stored.status)) {
2050
2055
  if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
2051
2056
  foe.last_selected_move = .SKIP_TURN;
2057
+ options.chance.observe(.sleep, player.foe(), .None);
2052
2058
  }
2053
- try log.curestatus(foe_ident, foe_stored.status, .Silent);
2059
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2054
2060
  foe_stored.status = 0;
2055
2061
  }
2056
- try clearVolatiles(battle, player, false, options);
2057
- try clearVolatiles(battle, player.foe(), true, options);
2062
+ try clearVolatiles(battle, player, options);
2063
+ try clearVolatiles(battle, player.foe(), options);
2058
2064
  }
2059
2065
  }
2060
2066
 
@@ -2066,7 +2072,7 @@ pub const Effects = struct {
2066
2072
  // GLITCH: HP recovery move failure glitches
2067
2073
  const delta = stored.stats.hp - stored.hp;
2068
2074
  if (delta == 0 or (delta & 255 == 255 and stored.hp % 256 != 0)) {
2069
- return try options.log.fail(ident, .None);
2075
+ return options.log.fail(.{ ident, .None });
2070
2076
  }
2071
2077
 
2072
2078
  const rest = side.last_selected_move == .Rest;
@@ -2074,17 +2080,17 @@ pub const Effects = struct {
2074
2080
  // Adding the sleep status runs the sleep condition handler to roll duration
2075
2081
  if (showdown) battle.rng.advance(1);
2076
2082
  stored.status = Status.slf(2);
2077
- try options.log.statusFrom(ident, stored.status, Move.Rest);
2083
+ try options.log.status(.{ ident, stored.status, .From, Move.Rest });
2078
2084
  stored.hp = stored.stats.hp;
2079
2085
  } else {
2080
2086
  stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
2081
2087
  }
2082
- 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 });
2083
2089
  }
2084
2090
 
2085
2091
  fn hyperBeam(battle: anytype, player: Player, options: anytype) !void {
2086
2092
  battle.side(player).active.volatiles.Recharging = true;
2087
- try options.log.mustrecharge(battle.active(player));
2093
+ try options.log.mustrecharge(.{battle.active(player)});
2088
2094
  }
2089
2095
 
2090
2096
  fn leechSeed(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2093,33 +2099,31 @@ pub const Effects = struct {
2093
2099
  if (showdown) {
2094
2100
  // Invulnerability trumps type immunity on Pokémon Showdown
2095
2101
  if (!foe.active.volatiles.Invulnerable and foe.active.types.includes(.Grass)) {
2096
- return options.log.immune(battle.active(player.foe()), .None);
2102
+ return options.log.immune(.{ battle.active(player.foe()), .None });
2097
2103
  }
2098
2104
  if (!try checkHit(battle, player, move, options)) return;
2099
2105
  if (foe.active.volatiles.LeechSeed) return;
2100
2106
  } else {
2101
2107
  if (!try checkHit(battle, player, move, options)) return;
2102
2108
  if (foe.active.types.includes(.Grass) or foe.active.volatiles.LeechSeed) {
2103
- try options.log.lastmiss();
2104
- return options.log.miss(battle.active(player));
2109
+ try options.log.lastmiss(.{});
2110
+ return options.log.miss(.{battle.active(player)});
2105
2111
  }
2106
2112
  }
2107
2113
 
2108
- try options.chance.commit(player, .hit);
2109
2114
  foe.active.volatiles.LeechSeed = true;
2110
2115
 
2111
- try options.log.start(battle.active(player.foe()), .LeechSeed);
2116
+ try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
2112
2117
  }
2113
2118
 
2114
2119
  fn lightScreen(battle: anytype, player: Player, options: anytype) !void {
2115
2120
  var side = battle.side(player);
2121
+ const ident = battle.active(player);
2116
2122
 
2117
- if (side.active.volatiles.LightScreen) {
2118
- return options.log.fail(battle.active(player), .None);
2119
- }
2123
+ if (side.active.volatiles.LightScreen) return options.log.fail(.{ ident, .None });
2120
2124
  side.active.volatiles.LightScreen = true;
2121
2125
 
2122
- try options.log.start(battle.active(player), .LightScreen);
2126
+ try options.log.start(.{ ident, .LightScreen });
2123
2127
  }
2124
2128
 
2125
2129
  fn mimic(battle: anytype, player: Player, move: Move.Data, mslot: u8, options: anytype) !void {
@@ -2147,17 +2151,15 @@ pub const Effects = struct {
2147
2151
  if (!has_mimic) {
2148
2152
  // Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
2149
2153
  if (!try checkHit(battle, player, move, options)) return;
2150
- try options.chance.commit(player, .hit);
2151
- return try options.log.fail(battle.active(player.foe()), .None);
2154
+ return options.log.fail(.{ battle.active(player.foe()), .None });
2152
2155
  }
2153
2156
  }
2154
2157
  if (!try checkHit(battle, player, move, options)) return;
2155
- try options.chance.commit(player, .hit);
2156
2158
 
2157
2159
  const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
2158
2160
  side.active.move(oslot).id = foe.active.move(rslot).id;
2159
2161
 
2160
- 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 });
2161
2163
  }
2162
2164
 
2163
2165
  fn mist(battle: anytype, player: Player, options: anytype) !void {
@@ -2166,7 +2168,7 @@ pub const Effects = struct {
2166
2168
  if (side.active.volatiles.Mist) return;
2167
2169
  side.active.volatiles.Mist = true;
2168
2170
 
2169
- try options.log.start(battle.active(player), .Mist);
2171
+ try options.log.start(.{ battle.active(player), .Mist });
2170
2172
  }
2171
2173
 
2172
2174
  fn multiHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2192,25 +2194,25 @@ pub const Effects = struct {
2192
2194
 
2193
2195
  if (showdown) {
2194
2196
  // Invulnerability trumps type immunity on Pokémon Showdown
2195
- if (immune and !foe.active.volatiles.Invulnerable) return log.immune(foe_ident, .None);
2196
- if (!try checkHit(battle, player, move, options)) return;
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;
2197
2200
  }
2198
2201
  if (Status.any(foe_stored.status)) {
2199
- return log.fail(
2202
+ return log.fail(.{
2200
2203
  foe_ident,
2201
- if (Status.is(foe_stored.status, .PAR)) .Paralysis else .None,
2202
- );
2204
+ if (Status.is(foe_stored.status, .PAR)) Fail.Paralysis else Fail.None,
2205
+ });
2203
2206
  }
2204
2207
  if (!showdown) {
2205
- if (immune) return log.immune(foe_ident, .None);
2208
+ if (immune) return log.immune(.{ foe_ident, .None });
2206
2209
  if (!try checkHit(battle, player, move, options)) return;
2207
2210
  }
2208
- try options.chance.commit(player, .hit);
2209
2211
 
2210
2212
  foe_stored.status = Status.init(.PAR);
2211
2213
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2212
2214
 
2213
- try log.status(foe_ident, foe_stored.status, .None);
2215
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2214
2216
  }
2215
2217
 
2216
2218
  fn paralyzeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2232,12 +2234,12 @@ pub const Effects = struct {
2232
2234
  foe_stored.status = Status.init(.PAR);
2233
2235
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2234
2236
 
2235
- 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 });
2236
2238
  }
2237
2239
 
2238
2240
  fn payDay(battle: anytype, player: Player, options: anytype) !void {
2239
2241
  if (!showdown or !battle.foe(player).active.volatiles.Substitute) {
2240
- try options.log.fieldactivate();
2242
+ try options.log.fieldactivate(.{});
2241
2243
  }
2242
2244
  }
2243
2245
 
@@ -2252,28 +2254,27 @@ pub const Effects = struct {
2252
2254
  return;
2253
2255
  } else if (foe.active.volatiles.Substitute) {
2254
2256
  if (move.effect != .Poison) return;
2255
- return log.fail(foe_ident, .None);
2257
+ return log.fail(.{ foe_ident, .None });
2256
2258
  } else if (Status.any(foe_stored.status)) {
2257
2259
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2258
2260
  // Pokémon Showdown considers Toxic to be a status even in Generation I and so
2259
2261
  // will not include a fail reason for Toxic vs. Poison or vice-versa...
2260
- return log.fail(foe_ident, if (Status.is(foe_stored.status, .PSN))
2262
+ return log.fail(.{ foe_ident, if (Status.is(foe_stored.status, .PSN))
2261
2263
  if (!showdown)
2262
- .Poison
2264
+ Fail.Poison
2263
2265
  else if (toxic == (foe_stored.status == Status.TOX))
2264
- if (toxic) .Toxic else .Poison
2266
+ if (toxic) Fail.Toxic else Fail.Poison
2265
2267
  else
2266
- .None
2268
+ Fail.None
2267
2269
  else
2268
- .None);
2270
+ Fail.None });
2269
2271
  } else if (foe.active.types.includes(.Poison)) {
2270
2272
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2271
- return log.immune(foe_ident, .None);
2273
+ return log.immune(.{ foe_ident, .None });
2272
2274
  }
2273
2275
 
2274
2276
  if (move.effect == .Poison) {
2275
2277
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2276
- try options.chance.commit(player, .hit);
2277
2278
  } else {
2278
2279
  if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
2279
2280
  return;
@@ -2287,7 +2288,7 @@ pub const Effects = struct {
2287
2288
  foe.active.volatiles.toxic = 0;
2288
2289
  }
2289
2290
 
2290
- try log.status(foe_ident, foe_stored.status, .None);
2291
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2291
2292
  }
2292
2293
 
2293
2294
  fn rage(battle: anytype, player: Player) !void {
@@ -2307,22 +2308,23 @@ pub const Effects = struct {
2307
2308
  @as(u8, if (side.last_selected_move == .Struggle) 2 else 4), 1));
2308
2309
  stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
2309
2310
 
2310
- try options.log.damageOf(
2311
+ try options.log.damage(.{
2311
2312
  battle.active(player),
2312
2313
  stored,
2313
2314
  .RecoilOf,
2314
2315
  battle.active(player.foe()),
2315
- );
2316
+ });
2316
2317
  if (showdown and stored.hp == 0) residual.* = false;
2317
2318
  }
2318
2319
 
2319
2320
  fn reflect(battle: anytype, player: Player, options: anytype) !void {
2320
2321
  var side = battle.side(player);
2322
+ const ident = battle.active(player);
2321
2323
 
2322
- 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 });
2323
2325
  side.active.volatiles.Reflect = true;
2324
2326
 
2325
- try options.log.start(battle.active(player), .Reflect);
2327
+ try options.log.start(.{ ident, .Reflect });
2326
2328
  }
2327
2329
 
2328
2330
  fn sleep(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2336,14 +2338,13 @@ pub const Effects = struct {
2336
2338
  // the Sleep Clause Mod might activate, causing us to not actually bypass
2337
2339
  } else {
2338
2340
  if (Status.any(foe_stored.status)) {
2339
- return options.log.fail(
2341
+ return options.log.fail(.{
2340
2342
  foe_ident,
2341
- if (Status.is(foe_stored.status, .SLP)) .Sleep else .None,
2342
- );
2343
+ if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
2344
+ });
2343
2345
  }
2344
- // 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
2345
2347
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2346
- try options.chance.commit(player, .hit);
2347
2348
  }
2348
2349
 
2349
2350
  // Sleep Clause Mod
@@ -2355,19 +2356,20 @@ pub const Effects = struct {
2355
2356
  foe.active.volatiles.Recharging = false;
2356
2357
 
2357
2358
  foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2359
+ options.chance.observe(.sleep, player.foe(), .started);
2360
+
2358
2361
  const last = battle.side(player).last_selected_move;
2359
- try options.log.statusFrom(foe_ident, foe_stored.status, last);
2362
+ try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
2360
2363
  }
2361
2364
 
2362
2365
  fn splash(battle: anytype, player: Player, options: anytype) !void {
2363
- try options.log.activate(battle.active(player), .Splash);
2366
+ try options.log.activate(.{ battle.active(player), .Splash });
2364
2367
  }
2365
2368
 
2366
2369
  fn substitute(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
2367
2370
  var side = battle.side(player);
2368
2371
  if (side.active.volatiles.Substitute) {
2369
- try options.log.fail(battle.active(player), .Substitute);
2370
- return;
2372
+ return options.log.fail(.{ battle.active(player), .Substitute });
2371
2373
  }
2372
2374
 
2373
2375
  assert(side.stored().stats.hp <= 1023);
@@ -2379,28 +2381,30 @@ pub const Effects = struct {
2379
2381
  // avoid having to convert to floating point)
2380
2382
  const required_hp = if (showdown) @divFloor(side.stored().stats.hp - 1, 4) + 1 else hp;
2381
2383
  if (side.stored().hp < required_hp) {
2382
- try options.log.fail(battle.active(player), .Weak);
2383
- return;
2384
+ return options.log.fail(.{ battle.active(player), .Weak });
2384
2385
  }
2385
2386
 
2386
2387
  // GLITCH: can leave the user with 0 HP (faints later) because didn't check '<=' above
2387
2388
  side.stored().hp -= hp;
2388
2389
  side.active.volatiles.substitute = hp + 1;
2389
2390
  side.active.volatiles.Substitute = true;
2390
- try options.log.start(battle.active(player), .Substitute);
2391
+ try options.log.start(.{ battle.active(player), .Substitute });
2391
2392
  if (hp > 0) {
2392
- try options.log.damage(battle.active(player), side.stored(), .None);
2393
+ try options.log.damage(.{ battle.active(player), side.stored(), .None });
2393
2394
  if (showdown and side.stored().hp == 0) residual.* = false;
2394
2395
  }
2395
2396
  }
2396
2397
 
2397
2398
  fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
2398
- if (!showdown or battle.side(player).last_selected_move == .Teleport) return;
2399
-
2400
2399
  // Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
2401
- if (try checkHit(battle, player, move, options)) try options.chance.commit(player, .hit);
2402
-
2403
- battle.last_damage = 0;
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
+ }
2404
2408
  }
2405
2409
 
2406
2410
  fn thrashing(battle: anytype, player: Player, options: anytype) void {
@@ -2410,6 +2414,8 @@ pub const Effects = struct {
2410
2414
 
2411
2415
  volatiles.Thrashing = true;
2412
2416
  volatiles.attacks = Rolls.attackingDuration(battle, player, options);
2417
+
2418
+ options.chance.observe(.attacking, player, .started);
2413
2419
  }
2414
2420
 
2415
2421
  fn transform(battle: anytype, player: Player, options: anytype) !void {
@@ -2438,7 +2444,7 @@ pub const Effects = struct {
2438
2444
  side.active.moves[i].pp = if (m.id != .None) 5 else 0;
2439
2445
  }
2440
2446
 
2441
- try options.log.transform(battle.active(player), foe_ident);
2447
+ try options.log.transform(.{ battle.active(player), foe_ident });
2442
2448
  }
2443
2449
 
2444
2450
  fn binding(battle: anytype, player: Player, rewrap: bool, options: anytype) !void {
@@ -2455,6 +2461,7 @@ pub const Effects = struct {
2455
2461
 
2456
2462
  side.active.volatiles.attacks =
2457
2463
  try Rolls.distribution(battle, .Binding, player, options) - 1;
2464
+ options.chance.observe(.binding, player, .started);
2458
2465
  }
2459
2466
 
2460
2467
  fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2468,87 +2475,87 @@ pub const Effects = struct {
2468
2475
  switch (move.effect) {
2469
2476
  .AttackUp1, .AttackUp2, .Rage => {
2470
2477
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2471
- if (boosts.atk == 6) return try log.fail(ident, .None);
2478
+ if (boosts.atk == 6) return log.fail(.{ ident, .None });
2472
2479
  const n: u2 = if (move.effect == .AttackUp2) 2 else 1;
2473
2480
  boosts.atk = @as(i4, @intCast(@min(6, @as(i8, boosts.atk) + n)));
2474
- const reason = if (move.effect == .Rage) Boost.Rage else Boost.Attack;
2481
+ const reason: Boost = if (move.effect == .Rage) .Rage else .Attack;
2475
2482
  if (stats.atk == MAX_STAT_VALUE) {
2476
2483
  boosts.atk -= 1;
2477
2484
  if (showdown) {
2478
- try log.boost(ident, reason, n);
2479
- try log.boost(ident, Boost.Attack, -1);
2485
+ try log.boost(.{ ident, reason, n });
2486
+ try log.boost(.{ ident, .Attack, -1 });
2480
2487
  if (move.effect == .Rage) return;
2481
2488
  }
2482
- return try log.fail(ident, .None);
2489
+ return log.fail(.{ ident, .None });
2483
2490
  }
2484
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2491
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2485
2492
  const stat = unmodifiedStats(battle, side).atk;
2486
2493
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2487
- try log.boost(ident, reason, n);
2494
+ try log.boost(.{ ident, reason, n });
2488
2495
  // Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
2489
2496
  if (showdown and move.effect == .Rage) return;
2490
2497
  },
2491
2498
  .DefenseUp1, .DefenseUp2 => {
2492
2499
  assert(boosts.def >= -6 and boosts.def <= 6);
2493
- if (boosts.def == 6) return try log.fail(ident, .None);
2500
+ if (boosts.def == 6) return log.fail(.{ ident, .None });
2494
2501
  const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
2495
2502
  boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
2496
2503
  if (stats.def == MAX_STAT_VALUE) {
2497
2504
  boosts.def -= 1;
2498
2505
  if (showdown) {
2499
- try log.boost(ident, .Defense, n);
2500
- try log.boost(ident, .Defense, -1);
2506
+ try log.boost(.{ ident, .Defense, n });
2507
+ try log.boost(.{ ident, .Defense, -1 });
2501
2508
  }
2502
- return try log.fail(ident, .None);
2509
+ return log.fail(.{ ident, .None });
2503
2510
  }
2504
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2511
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2505
2512
  const stat = unmodifiedStats(battle, side).def;
2506
2513
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2507
- try log.boost(ident, .Defense, n);
2514
+ try log.boost(.{ ident, .Defense, n });
2508
2515
  },
2509
2516
  .SpeedUp2 => {
2510
2517
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2511
- if (boosts.spe == 6) return try log.fail(ident, .None);
2518
+ if (boosts.spe == 6) return log.fail(.{ ident, .None });
2512
2519
  boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
2513
2520
  if (stats.spe == MAX_STAT_VALUE) {
2514
2521
  boosts.spe -= 1;
2515
2522
  if (showdown) {
2516
- try log.boost(ident, .Speed, 2);
2517
- try log.boost(ident, .Speed, -1);
2523
+ try log.boost(.{ ident, .Speed, 2 });
2524
+ try log.boost(.{ ident, .Speed, -1 });
2518
2525
  }
2519
- return try log.fail(ident, .None);
2526
+ return log.fail(.{ ident, .None });
2520
2527
  }
2521
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2528
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2522
2529
  const stat = unmodifiedStats(battle, side).spe;
2523
2530
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2524
- try log.boost(ident, .Speed, 2);
2531
+ try log.boost(.{ ident, .Speed, 2 });
2525
2532
  },
2526
2533
  .SpecialUp1, .SpecialUp2 => {
2527
2534
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2528
- if (boosts.spc == 6) return try log.fail(ident, .None);
2535
+ if (boosts.spc == 6) return log.fail(.{ ident, .None });
2529
2536
  const n: u2 = if (move.effect == .SpecialUp2) 2 else 1;
2530
2537
  boosts.spc = @intCast(@min(6, @as(i8, boosts.spc) + n));
2531
2538
  if (stats.spc == MAX_STAT_VALUE) {
2532
2539
  boosts.spc -= 1;
2533
2540
  if (showdown) {
2534
- try log.boost(ident, .SpecialAttack, n);
2535
- try log.boost(ident, .SpecialAttack, -1);
2536
- try log.boost(ident, .SpecialDefense, n);
2537
- try log.boost(ident, .SpecialDefense, -1);
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 });
2538
2545
  }
2539
- return try log.fail(ident, .None);
2546
+ return log.fail(.{ ident, .None });
2540
2547
  }
2541
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2548
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2542
2549
  const stat = unmodifiedStats(battle, side).spc;
2543
2550
  stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2544
- try log.boost(ident, .SpecialAttack, n);
2545
- try log.boost(ident, .SpecialDefense, n);
2551
+ try log.boost(.{ ident, .SpecialAttack, n });
2552
+ try log.boost(.{ ident, .SpecialDefense, n });
2546
2553
  },
2547
2554
  .EvasionUp1 => {
2548
2555
  assert(boosts.evasion >= -6 and boosts.evasion <= 6);
2549
- if (boosts.evasion == 6) return try log.fail(ident, .None);
2556
+ if (boosts.evasion == 6) return log.fail(.{ ident, .None });
2550
2557
  boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
2551
- try log.boost(ident, .Evasion, 1);
2558
+ try log.boost(.{ ident, .Evasion, 1 });
2552
2559
  },
2553
2560
  else => unreachable,
2554
2561
  }
@@ -2563,7 +2570,7 @@ pub const Effects = struct {
2563
2570
  const foe_ident = battle.active(player.foe());
2564
2571
  const secondary = move.effect.isStatDownChance();
2565
2572
 
2566
- 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 });
2567
2574
 
2568
2575
  if (secondary) {
2569
2576
  const proc = try Rolls.unboost(battle, player, options);
@@ -2571,7 +2578,6 @@ pub const Effects = struct {
2571
2578
  } else {
2572
2579
  // checkHit already checks for Invulnerable
2573
2580
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2574
- try options.chance.commit(player, .hit);
2575
2581
  }
2576
2582
 
2577
2583
  var stats = &foe.active.stats;
@@ -2581,82 +2587,82 @@ pub const Effects = struct {
2581
2587
  switch (move.effect) {
2582
2588
  .AttackDown1, .AttackDownChance => {
2583
2589
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2584
- 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 });
2585
2591
  boosts.atk = @intCast(@max(-6, @as(i8, boosts.atk) - 1));
2586
2592
  if (stats.atk == 1) {
2587
2593
  boosts.atk += 1;
2588
2594
  if (showdown) {
2589
- try log.boost(foe_ident, .Attack, -1);
2590
- try log.boost(foe_ident, .Attack, 1);
2595
+ try log.boost(.{ foe_ident, .Attack, -1 });
2596
+ try log.boost(.{ foe_ident, .Attack, 1 });
2591
2597
  }
2592
- return try log.fail(foe_ident, .None);
2598
+ return log.fail(.{ foe_ident, .None });
2593
2599
  }
2594
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2600
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2595
2601
  const stat = unmodifiedStats(battle, foe).atk;
2596
2602
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2597
- try log.boost(foe_ident, .Attack, -1);
2603
+ try log.boost(.{ foe_ident, .Attack, -1 });
2598
2604
  },
2599
2605
  .DefenseDown1, .DefenseDown2, .DefenseDownChance => {
2600
2606
  assert(boosts.def >= -6 and boosts.def <= 6);
2601
- 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 });
2602
2608
  const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
2603
2609
  boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
2604
2610
  if (stats.def == 1) {
2605
2611
  boosts.def += 1;
2606
2612
  if (showdown) {
2607
- try log.boost(foe_ident, .Defense, -@as(i8, n));
2608
- 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 });
2609
2615
  }
2610
- return try log.fail(foe_ident, .None);
2616
+ return log.fail(.{ foe_ident, .None });
2611
2617
  }
2612
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2618
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2613
2619
  const stat = unmodifiedStats(battle, foe).def;
2614
2620
  stats.def = @max(1, stat * mod[0] / mod[1]);
2615
- try log.boost(foe_ident, .Defense, -@as(i8, n));
2621
+ try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
2616
2622
  },
2617
2623
  .SpeedDown1, .SpeedDownChance => {
2618
2624
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2619
- 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 });
2620
2626
  boosts.spe = @intCast(@max(-6, @as(i8, boosts.spe) - 1));
2621
2627
  if (stats.spe == 1) {
2622
2628
  boosts.spe += 1;
2623
2629
  if (showdown) {
2624
- try log.boost(foe_ident, .Speed, -1);
2625
- try log.boost(foe_ident, .Speed, 1);
2630
+ try log.boost(.{ foe_ident, .Speed, -1 });
2631
+ try log.boost(.{ foe_ident, .Speed, 1 });
2626
2632
  }
2627
- return try log.fail(foe_ident, .None);
2633
+ return log.fail(.{ foe_ident, .None });
2628
2634
  }
2629
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2635
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2630
2636
  const stat = unmodifiedStats(battle, foe).spe;
2631
2637
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2632
- try log.boost(foe_ident, .Speed, -1);
2638
+ try log.boost(.{ foe_ident, .Speed, -1 });
2633
2639
  assert(boosts.spe >= -6);
2634
2640
  },
2635
2641
  .SpecialDownChance => {
2636
2642
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2637
- 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 });
2638
2644
  boosts.spc = @intCast(@max(-6, @as(i8, boosts.spc) - 1));
2639
2645
  if (stats.spc == 1) {
2640
2646
  boosts.spc += 1;
2641
2647
  if (showdown) {
2642
- try log.boost(foe_ident, .SpecialAttack, -1);
2643
- try log.boost(foe_ident, .SpecialAttack, 1);
2644
- try log.boost(foe_ident, .SpecialDefense, -1);
2645
- try log.boost(foe_ident, .SpecialDefense, 1);
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 });
2646
2652
  }
2647
- return try log.fail(foe_ident, .None);
2653
+ return log.fail(.{ foe_ident, .None });
2648
2654
  }
2649
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2655
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2650
2656
  const stat = unmodifiedStats(battle, foe).spc;
2651
2657
  stats.spc = @max(1, stat * mod[0] / mod[1]);
2652
- try log.boost(foe_ident, .SpecialAttack, -1);
2653
- try log.boost(foe_ident, .SpecialDefense, -1);
2658
+ try log.boost(.{ foe_ident, .SpecialAttack, -1 });
2659
+ try log.boost(.{ foe_ident, .SpecialDefense, -1 });
2654
2660
  },
2655
2661
  .AccuracyDown1 => {
2656
2662
  assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
2657
- 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 });
2658
2664
  boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
2659
- try log.boost(foe_ident, .Accuracy, -1);
2665
+ try log.boost(.{ foe_ident, .Accuracy, -1 });
2660
2666
  },
2661
2667
  else => unreachable,
2662
2668
  }
@@ -2680,40 +2686,40 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
2680
2686
  }
2681
2687
  }
2682
2688
 
2683
- inline fn isForced(active: anytype) bool {
2689
+ fn isForced(active: anytype) bool {
2684
2690
  return active.volatiles.Recharging or active.volatiles.Rage or
2685
2691
  active.volatiles.Thrashing or active.volatiles.Charging;
2686
2692
  }
2687
2693
 
2688
- fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !void {
2694
+ fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
2689
2695
  var log = options.log;
2690
2696
  var side = battle.side(who);
2691
2697
  var volatiles = &side.active.volatiles;
2692
2698
  const ident = battle.active(who);
2693
2699
 
2694
- options.chance.clearDurations(who, clear);
2695
-
2696
- if (volatiles.disabled_move != 0) {
2697
- volatiles.disabled_move = 0;
2698
- volatiles.disabled_duration = 0;
2699
- try log.end(ident, .DisableSilent);
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 });
2700
2705
  }
2701
2706
  if (volatiles.Confusion) {
2702
2707
  // volatiles.confusion is left unchanged
2703
2708
  volatiles.Confusion = false;
2704
- try log.end(ident, .ConfusionSilent);
2709
+ options.chance.observe(.confusion, who, .None);
2710
+ try log.end(.{ ident, .ConfusionSilent });
2705
2711
  }
2706
2712
  if (volatiles.Mist) {
2707
2713
  volatiles.Mist = false;
2708
- try log.end(ident, .Mist);
2714
+ try log.end(.{ ident, .MistSilent });
2709
2715
  }
2710
2716
  if (volatiles.FocusEnergy) {
2711
2717
  volatiles.FocusEnergy = false;
2712
- try log.end(ident, .FocusEnergy);
2718
+ try log.end(.{ ident, .FocusEnergySilent });
2713
2719
  }
2714
2720
  if (volatiles.LeechSeed) {
2715
2721
  volatiles.LeechSeed = false;
2716
- try log.end(ident, .LeechSeed);
2722
+ try log.end(.{ ident, .LeechSeedSilent });
2717
2723
  }
2718
2724
  if (!showdown and volatiles.Toxic) {
2719
2725
  volatiles.Toxic = false;
@@ -2721,24 +2727,39 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2721
2727
  }
2722
2728
  if (volatiles.LightScreen) {
2723
2729
  volatiles.LightScreen = false;
2724
- try log.end(ident, .LightScreen);
2730
+ try log.end(.{ ident, .LightScreenSilent });
2725
2731
  }
2726
2732
  if (volatiles.Reflect) {
2727
2733
  volatiles.Reflect = false;
2728
- try log.end(ident, .Reflect);
2734
+ try log.end(.{ ident, .ReflectSilent });
2729
2735
  }
2730
2736
  if (showdown and volatiles.Toxic) {
2731
2737
  volatiles.Toxic = false;
2732
2738
  // Pokémon Showdown erroneously clears the toxic counter
2733
2739
  volatiles.toxic = 0;
2734
- try log.end(ident, .Toxic);
2740
+ try log.end(.{ ident, .ToxicSilent });
2735
2741
  }
2736
2742
  }
2737
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
+
2738
2757
  pub const Rolls = struct {
2739
- inline fn speedTie(battle: anytype, options: anytype) !bool {
2758
+ fn speedTie(battle: anytype, options: anytype) !bool {
2740
2759
  const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
2741
2760
  player == .P1
2761
+ else if (@hasDecl(@TypeOf(battle.rng), "speedTie"))
2762
+ battle.rng.speedTie()
2742
2763
  else if (showdown)
2743
2764
  battle.rng.range(u8, 0, 2) == 0
2744
2765
  else
@@ -2748,21 +2769,31 @@ pub const Rolls = struct {
2748
2769
  return p1;
2749
2770
  }
2750
2771
 
2751
- 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 {
2752
2779
  const crit = if (options.calc.overridden(player, .critical_hit)) |val|
2753
2780
  val == .true
2781
+ else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
2782
+ battle.rng.criticalHit(player, rate)
2754
2783
  else if (showdown)
2755
2784
  battle.rng.chance(u8, rate, 256)
2756
2785
  else
2757
2786
  std.math.rotl(u8, battle.rng.next(), 3) < rate;
2758
2787
 
2759
- try options.chance.criticalHit(player, crit, rate);
2788
+ if (!spurious) try options.chance.criticalHit(player, crit, rate);
2760
2789
  return crit;
2761
2790
  }
2762
2791
 
2763
- inline fn damage(battle: anytype, player: anytype, options: anytype) !u8 {
2792
+ fn damage(battle: anytype, player: Player, options: anytype) !u8 {
2764
2793
  const roll = if (options.calc.overridden(player, .damage)) |val|
2765
2794
  val
2795
+ else if (@hasDecl(@TypeOf(battle.rng), "damage"))
2796
+ battle.rng.damage(player)
2766
2797
  else roll: {
2767
2798
  if (showdown) break :roll battle.rng.range(u8, 217, 256);
2768
2799
  while (true) {
@@ -2776,21 +2807,27 @@ pub const Rolls = struct {
2776
2807
  return roll;
2777
2808
  }
2778
2809
 
2779
- 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;
2780
2813
  const ok = if (options.calc.overridden(player, .hit)) |val|
2781
2814
  val == .true
2815
+ else if (@hasDecl(@TypeOf(battle.rng), "hit"))
2816
+ battle.rng.hit(player, acc)
2782
2817
  else if (showdown)
2783
- battle.rng.chance(u8, accuracy, 256)
2818
+ battle.rng.chance(uN, acc, 256)
2784
2819
  else
2785
- battle.rng.next() < accuracy;
2820
+ battle.rng.next() < acc;
2786
2821
 
2787
- options.chance.hit(ok, accuracy);
2822
+ try options.chance.hit(player, ok, acc);
2788
2823
  return ok;
2789
2824
  }
2790
2825
 
2791
- inline fn confused(battle: anytype, player: Player, options: anytype) !bool {
2826
+ fn confused(battle: anytype, player: Player, options: anytype) !bool {
2792
2827
  const cfz = if (options.calc.overridden(player, .confused)) |val|
2793
2828
  val == .true
2829
+ else if (@hasDecl(@TypeOf(battle.rng), "confused"))
2830
+ battle.rng.confused(player)
2794
2831
  else if (showdown)
2795
2832
  !battle.rng.chance(u8, 128, 256)
2796
2833
  else
@@ -2800,9 +2837,11 @@ pub const Rolls = struct {
2800
2837
  return cfz;
2801
2838
  }
2802
2839
 
2803
- inline fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2840
+ fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2804
2841
  const par = if (options.calc.overridden(player, .paralyzed)) |val|
2805
2842
  val == .true
2843
+ else if (@hasDecl(@TypeOf(battle.rng), "paralyzed"))
2844
+ battle.rng.paralyzed(player)
2806
2845
  else if (showdown)
2807
2846
  battle.rng.chance(u8, 63, 256)
2808
2847
  else
@@ -2812,9 +2851,11 @@ pub const Rolls = struct {
2812
2851
  return par;
2813
2852
  }
2814
2853
 
2815
- inline fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2854
+ fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2816
2855
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2817
2856
  val == .true
2857
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2858
+ battle.rng.secondaryChance(player, 25)
2818
2859
  else if (showdown)
2819
2860
  battle.rng.chance(u8, 25, 256)
2820
2861
  else
@@ -2824,11 +2865,13 @@ pub const Rolls = struct {
2824
2865
  return proc;
2825
2866
  }
2826
2867
 
2827
- inline fn secondaryChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2868
+ fn secondaryChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2828
2869
  const rate: u8 = if (low) 26 else 77;
2829
2870
 
2830
2871
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2831
2872
  val == .true
2873
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2874
+ battle.rng.secondaryChance(player, rate)
2832
2875
  else if (showdown)
2833
2876
  battle.rng.chance(u8, rate, 256)
2834
2877
  else
@@ -2838,11 +2881,13 @@ pub const Rolls = struct {
2838
2881
  return proc;
2839
2882
  }
2840
2883
 
2841
- inline fn poisonChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2884
+ fn poisonChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2842
2885
  const rate: u8 = if (low) 52 else 103;
2843
2886
 
2844
2887
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2845
2888
  val == .true
2889
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2890
+ battle.rng.secondaryChance(player, rate)
2846
2891
  else if (showdown)
2847
2892
  battle.rng.chance(u8, rate, 256)
2848
2893
  else
@@ -2852,9 +2897,11 @@ pub const Rolls = struct {
2852
2897
  return proc;
2853
2898
  }
2854
2899
 
2855
- inline fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2900
+ fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2856
2901
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2857
2902
  val == .true
2903
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2904
+ battle.rng.secondaryChance(player, 85)
2858
2905
  else if (showdown)
2859
2906
  battle.rng.chance(u8, 85, 256)
2860
2907
  else
@@ -2864,14 +2911,14 @@ pub const Rolls = struct {
2864
2911
  return proc;
2865
2912
  }
2866
2913
 
2867
- inline fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2914
+ fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2868
2915
  const move: Move = if (options.calc.overridden(player, .metronome)) |val|
2869
2916
  val
2870
- else if (showdown) move: {
2871
- const r = battle.rng.range(u8, 0, @intFromEnum(Move.Struggle) - 2);
2872
- const mod = @as(u2, (if (r < @intFromEnum(Move.Metronome) - 1) 1 else 2));
2873
- break :move @enumFromInt(r + mod);
2874
- } else move: {
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: {
2875
2922
  while (true) {
2876
2923
  const r = battle.rng.next();
2877
2924
  if (r == 0 or r == @intFromEnum(Move.Metronome)) continue;
@@ -2884,9 +2931,11 @@ pub const Rolls = struct {
2884
2931
  return move;
2885
2932
  }
2886
2933
 
2887
- inline fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2934
+ fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2888
2935
  const power = if (options.calc.overridden(player, .psywave)) |val|
2889
2936
  val - 1
2937
+ else if (@hasDecl(@TypeOf(battle.rng), "psywave"))
2938
+ battle.rng.psywave(player)
2890
2939
  else if (showdown)
2891
2940
  battle.rng.range(u8, 0, max)
2892
2941
  else power: {
@@ -2901,11 +2950,13 @@ pub const Rolls = struct {
2901
2950
  return power;
2902
2951
  }
2903
2952
 
2904
- inline fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2953
+ fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2905
2954
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2906
2955
  @intCast(val)
2956
+ else if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
2957
+ battle.rng.sleepDuration(player)
2907
2958
  else if (showdown)
2908
- @intCast(battle.rng.range(u8, 1, 8))
2959
+ battle.rng.range(u3, 1, 8)
2909
2960
  else duration: {
2910
2961
  while (true) {
2911
2962
  const r = battle.rng.next() & 7;
@@ -2914,50 +2965,58 @@ pub const Rolls = struct {
2914
2965
  };
2915
2966
 
2916
2967
  assert(duration >= 1 and duration <= 7);
2917
- options.chance.duration(.sleep, player, player.foe(), duration);
2968
+ options.chance.duration(player, duration);
2918
2969
  return duration;
2919
2970
  }
2920
2971
 
2921
- inline fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2972
+ fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2922
2973
  const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
2923
2974
  val
2975
+ else if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
2976
+ battle.rng.disableDuration(player)
2924
2977
  else if (showdown)
2925
- @intCast(battle.rng.range(u8, 1, 9))
2978
+ battle.rng.range(u4, 1, 9)
2926
2979
  else
2927
2980
  @intCast((battle.rng.next() & 7) + 1);
2928
2981
 
2929
2982
  assert(duration >= 1 and duration <= 8);
2930
- options.chance.duration(.disable, player, player.foe(), duration);
2983
+ options.chance.duration(player, duration);
2931
2984
  return duration;
2932
2985
  }
2933
2986
 
2934
- inline fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
2987
+ fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
2935
2988
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2936
2989
  @intCast(val)
2990
+ else if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
2991
+ battle.rng.confusionDuration(player)
2937
2992
  else if (showdown)
2938
- @intCast(battle.rng.range(u8, 2, 6))
2993
+ battle.rng.range(u3, 2, 6)
2939
2994
  else
2940
2995
  @intCast((battle.rng.next() & 3) + 2);
2941
2996
 
2942
2997
  assert(duration >= 2 and duration <= 5);
2943
- options.chance.duration(.confusion, player, if (self) player else player.foe(), duration);
2998
+ options.chance.duration(player, duration);
2944
2999
  return duration;
2945
3000
  }
2946
3001
 
2947
- inline fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
3002
+ fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2948
3003
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2949
3004
  @intCast(val)
3005
+ else if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
3006
+ battle.rng.attackingDuration(player)
2950
3007
  else if (showdown)
2951
- @intCast(battle.rng.range(u4, 2, 4))
3008
+ battle.rng.range(u3, 2, 4)
2952
3009
  else
2953
3010
  @intCast((battle.rng.next() & 1) + 2);
2954
3011
 
2955
3012
  assert(duration >= 2 and duration <= 3);
2956
- options.chance.duration(.attacking, player, player, duration);
3013
+ options.chance.duration(player, duration);
2957
3014
  return duration;
2958
3015
  }
2959
3016
 
2960
- inline fn distribution(
3017
+ const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
3018
+
3019
+ fn distribution(
2961
3020
  battle: anytype,
2962
3021
  comptime effect: Move.Effect,
2963
3022
  player: Player,
@@ -2966,8 +3025,10 @@ pub const Rolls = struct {
2966
3025
  const roll = if (effect == .Binding) .duration else .multi_hit;
2967
3026
  const n: u3 = if (options.calc.overridden(player, roll)) |val|
2968
3027
  @intCast(val)
3028
+ else if (@hasDecl(@TypeOf(battle.rng), "distribution"))
3029
+ battle.rng.distribution(player)
2969
3030
  else if (showdown)
2970
- DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)]
3031
+ DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
2971
3032
  else n: {
2972
3033
  const r = (battle.rng.next() & 3);
2973
3034
  break :n @intCast((if (r < 2) r else battle.rng.next() & 3) + 2);
@@ -2975,7 +3036,7 @@ pub const Rolls = struct {
2975
3036
 
2976
3037
  assert(n >= 2 and n <= 5);
2977
3038
  if (effect == .Binding) {
2978
- options.chance.duration(.binding, player, player, n);
3039
+ options.chance.duration(player, n);
2979
3040
  } else {
2980
3041
  try options.chance.multiHit(player, n);
2981
3042
  }
@@ -2998,7 +3059,9 @@ pub const Rolls = struct {
2998
3059
  else
2999
3060
  null;
3000
3061
 
3001
- 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: {
3002
3065
  if (showdown) {
3003
3066
  if (check_pp == 0) {
3004
3067
  var i: usize = moves.len;
@@ -3014,10 +3077,9 @@ pub const Rolls = struct {
3014
3077
  while (i < moves.len and r > 0) : (i += 1) {
3015
3078
  if (moves[i].pp > 0) {
3016
3079
  r -= 1;
3017
- if (r == 0) break;
3080
+ if (r == 0) break :slot @intCast(i + 1);
3018
3081
  }
3019
3082
  }
3020
- break :slot @intCast(i + 1);
3021
3083
  }
3022
3084
  }
3023
3085
 
@@ -3039,11 +3101,11 @@ test "RNG agreement" {
3039
3101
  expected[i] = @intCast(i * 0x1000000);
3040
3102
  }
3041
3103
 
3042
- var spe = rng.FixedRNG(1, expected.len){ .rolls = expected };
3043
- var cfz = rng.FixedRNG(1, expected.len){ .rolls = expected };
3044
- var par = rng.FixedRNG(1, expected.len){ .rolls = expected };
3045
- var brn = rng.FixedRNG(1, expected.len){ .rolls = expected };
3046
- var eff = rng.FixedRNG(1, expected.len){ .rolls = expected };
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 };
3047
3109
 
3048
3110
  for (0..expected.len) |i| {
3049
3111
  try expectEqual(spe.range(u8, 0, 2) == 0, i < Gen12.percent(50) + 1);
@@ -3079,11 +3141,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3079
3141
  const side = battle.side(player);
3080
3142
  const foe = battle.foe(player);
3081
3143
 
3082
- var active = &side.active;
3144
+ const active = &side.active;
3083
3145
  const stored = side.stored();
3084
3146
 
3085
3147
  // While players are not given any input options on the cartridge in these cases,
3086
- // Pokémon Showdown instead produces a list with a single move that must be chosen.
3148
+ // Pokémon Showdown instead produces a list with a single move that must be chosen
3087
3149
  //
3088
3150
  // Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
3089
3151
  // here when not in compatibility mode, but the engine needs to be able to differentiate
@@ -3091,7 +3153,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3091
3153
  // passing due to being forced into using a move. Instead of introducing another option
3092
3154
  // we simply repurpose Move with no move slot, even though pedantically this is not
3093
3155
  // strictly correct as the player would not have been presented the option to move or
3094
- // switch at all.
3156
+ // switch at all
3095
3157
  if (isForced(active)) {
3096
3158
  out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
3097
3159
  n += 1;
@@ -3110,7 +3172,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3110
3172
  // On the cartridge, all of these happen after "FIGHT" (indicating you are not
3111
3173
  // switching) but before you are allowed to select a move. Pokémon Showdown instead
3112
3174
  // either disables all other moves in the case of limited or requires you to select a
3113
- // move normally if sleeping/frozen/bound.
3175
+ // move normally if sleeping/frozen/bound
3114
3176
  if (!showdown and (limited or foe.active.volatiles.Binding or
3115
3177
  Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
3116
3178
  {
@@ -3123,7 +3185,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3123
3185
  // Pokémon Showdown handles Bide and Binding moves by checking if the move in question
3124
3186
  // is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
3125
3187
  // Move will not result in forcing the subsequent use unless the user also had the
3126
- // proc-ed move in their moveset) and disabling all other moves.
3188
+ // proc-ed move in their moveset) and disabling all other moves
3127
3189
  if (limited) {
3128
3190
  assert(showdown);
3129
3191
  assert(side.last_selected_move != .None);
@@ -3133,7 +3195,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3133
3195
  if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
3134
3196
  // Pokémon Showdown displays Struggle if limited to Bide but unable to pick
3135
3197
  const struggle =
3136
- 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);
3137
3199
  const s = if (struggle) 0 else slot;
3138
3200
  out[n] = .{ .type = .Move, .data = s };
3139
3201
  n += 1;
@@ -3148,7 +3210,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3148
3210
  const m = active.moves[slot - 1];
3149
3211
  if (m.id == .None) break;
3150
3212
  if (m.pp == 0) continue;
3151
- if (active.volatiles.disabled_move == slot) continue;
3213
+ if (active.volatiles.disable_move == slot) continue;
3152
3214
  out[n] = .{ .type = .Move, .data = slot };
3153
3215
  n += 1;
3154
3216
  }