@pkmn/engine 0.1.0-dev.32adfe77 → 0.1.0-dev.3393edb3

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 (62) hide show
  1. package/README.md +15 -11
  2. package/build/pkg/addon/browser.d.ts +3 -0
  3. package/build/pkg/addon/browser.js +43 -0
  4. package/build/pkg/addon/browser.js.map +1 -0
  5. package/build/pkg/addon/node.d.ts +3 -0
  6. package/build/pkg/addon/node.js +96 -0
  7. package/build/pkg/addon/node.js.map +1 -0
  8. package/build/pkg/addon.d.ts +2 -0
  9. package/build/pkg/addon.js +35 -58
  10. package/build/pkg/addon.js.map +1 -1
  11. package/build/pkg/data.d.ts +25 -4
  12. package/build/pkg/data.js.map +1 -1
  13. package/build/pkg/gen1.js +1 -1
  14. package/build/pkg/gen1.js.map +1 -1
  15. package/build/pkg/index.d.ts +22 -9
  16. package/build/pkg/index.js +15 -10
  17. package/build/pkg/index.js.map +1 -1
  18. package/build/pkg/protocol.d.ts +20 -1
  19. package/build/pkg/protocol.js +109 -107
  20. package/build/pkg/protocol.js.map +1 -1
  21. package/build/tools/debug.js +36 -70
  22. package/build/tools/debug.js.map +1 -1
  23. package/build/tools/display/debug.jsx +141 -0
  24. package/build/tools/display/debug.jsx.map +1 -0
  25. package/build/tools/display/index.js +132 -0
  26. package/build/tools/display/index.js.map +1 -0
  27. package/build/tools/display/ui.jsx +349 -0
  28. package/build/tools/display/ui.jsx.map +1 -0
  29. package/build/tools/display/util.js +18 -0
  30. package/build/tools/display/util.js.map +1 -0
  31. package/build.zig +186 -65
  32. package/package.json +46 -29
  33. package/src/bin/install-pkmn-engine +17 -7
  34. package/src/bin/pkmn-debug +1 -1
  35. package/src/lib/common/array.zig +77 -0
  36. package/src/lib/common/data.zig +8 -8
  37. package/src/lib/common/debug.zig +32 -6
  38. package/src/lib/common/js.zig +4 -2
  39. package/src/lib/common/optional.zig +13 -9
  40. package/src/lib/common/options.zig +4 -0
  41. package/src/lib/common/protocol.zig +46 -36
  42. package/src/lib/common/rational.zig +114 -24
  43. package/src/lib/common/rng.zig +4 -4
  44. package/src/lib/common/util.zig +50 -1
  45. package/src/lib/gen1/calc.zig +663 -130
  46. package/src/lib/gen1/chance.zig +384 -275
  47. package/src/lib/gen1/data.zig +31 -30
  48. package/src/lib/gen1/helpers.zig +89 -538
  49. package/src/lib/gen1/mechanics.zig +275 -197
  50. package/src/lib/gen2/calc.zig +34 -16
  51. package/src/lib/gen2/chance.zig +90 -502
  52. package/src/lib/gen2/data.zig +15 -14
  53. package/src/lib/gen2/helpers.zig +41 -13
  54. package/src/lib/gen2/mechanics.zig +158 -103
  55. package/src/lib/pkmn.zig +10 -16
  56. package/src/tools/display/dom.ts +96 -0
  57. package/src/tools/display/ui.css +351 -0
  58. package/build/test/display.js +0 -400
  59. package/build/test/display.js.map +0 -1
  60. package/src/lib/gen1/README.md +0 -894
  61. package/src/lib/gen2/README.md +0 -329
  62. package/src/test/display/pkmn.html.tmpl +0 -368
@@ -8,6 +8,7 @@ const protocol = @import("../common/protocol.zig");
8
8
  const rng = @import("../common/rng.zig");
9
9
 
10
10
  const data = @import("data.zig");
11
+ const chance = @import("chance.zig");
11
12
 
12
13
  const assert = std.debug.assert;
13
14
 
@@ -27,11 +28,9 @@ const Heal = protocol.Heal;
27
28
 
28
29
  const Gen12 = rng.Gen12;
29
30
 
30
- const ActivePokemon = data.ActivePokemon;
31
31
  const Effectiveness = data.Effectiveness;
32
32
  const Move = data.Move;
33
33
  const MoveSlot = data.MoveSlot;
34
- const Pokemon = data.Pokemon;
35
34
  const Side = data.Side;
36
35
  const Species = data.Species;
37
36
  const Stats = data.Stats;
@@ -166,7 +165,7 @@ fn selectMove(
166
165
  // to satisfy the conditions of the Desync Clause Mod. However, because Binding is still
167
166
  // set the selected move will not actually be used, it will just be reported as having
168
167
  // been used (this differs from how Pokémon Showdown works, but its impossible to
169
- // replicate the incorrect behavior with the correct mechanisms).
168
+ // replicate the incorrect behavior with the correct mechanisms)
170
169
  saveMove(battle, player, choice);
171
170
  } else {
172
171
  assert(choice.data == 0);
@@ -253,11 +252,11 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: a
253
252
  active.moves = incoming.moves;
254
253
 
255
254
  statusModify(incoming.status, &active.stats);
255
+ options.chance.switched(player, slot);
256
256
 
257
257
  foe.active.volatiles.Binding = false;
258
258
 
259
259
  try options.log.switched(.{ battle.active(player), incoming });
260
- options.chance.switched(player, side.order[0], out);
261
260
 
262
261
  if (showdown and incoming.status == Status.TOX) {
263
262
  incoming.status = Status.init(.PSN);
@@ -342,18 +341,19 @@ fn doTurn(
342
341
  player_choice,
343
342
  player_rewrap,
344
343
  player_skip,
344
+ true,
345
345
  &residual,
346
346
  options,
347
347
  )) |r| return r;
348
348
  if (!replace) {
349
349
  if (player_choice.type != .Switch) {
350
- if (try checkFaint(battle, foe_player, options)) |r| return r;
350
+ if (try checkFaint(battle, foe_player, true, options)) |r| return r;
351
351
  }
352
352
  if (residual) try handleResidual(battle, player, options);
353
- if (try checkFaint(battle, player, options)) |r| return r;
353
+ if (try checkFaint(battle, player, false, options)) |r| return r;
354
354
  } else if (foe_choice.type == .Pass) return null;
355
355
 
356
- options.chance.clearPending();
356
+ if (!showdown) options.chance.clearPending();
357
357
 
358
358
  residual = true;
359
359
  replace = battle.side(foe_player).stored().hp == 0;
@@ -364,15 +364,16 @@ fn doTurn(
364
364
  foe_choice,
365
365
  foe_rewrap,
366
366
  foe_skip,
367
+ false,
367
368
  &residual,
368
369
  options,
369
370
  )) |r| return r;
370
371
  if (!replace) {
371
372
  if (!calc and foe_choice.type != .Switch) {
372
- if (try checkFaint(battle, player, options)) |r| return r;
373
+ if (try checkFaint(battle, player, true, options)) |r| return r;
373
374
  }
374
375
  if (residual) try handleResidual(battle, foe_player, options);
375
- if (try checkFaint(battle, foe_player, options)) |r| return r;
376
+ if (try checkFaint(battle, foe_player, false, options)) |r| return r;
376
377
  }
377
378
 
378
379
  // Flinch is bugged on Pokémon Showdown because it gets implemented with a duration which causes
@@ -391,6 +392,7 @@ fn executeMove(
391
392
  choice: Choice,
392
393
  rewrap: bool,
393
394
  skip: bool,
395
+ first: bool,
394
396
  residual: *bool,
395
397
  options: anytype,
396
398
  ) !?Result {
@@ -431,7 +433,7 @@ fn executeMove(
431
433
  Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
432
434
  // If it wasn't Hyper Beam or the continuation of a move effect then we must have just
433
435
  // thawed, in which case we will desync unless the last_selected_move happened to be at
434
- // 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
435
437
  if (!auto) {
436
438
  // side.active.moves(slot) is safe to check even though the slot in question might not
437
439
  // technically be from this Pokémon because it must be exactly 1 to not desync and
@@ -450,7 +452,7 @@ fn executeMove(
450
452
 
451
453
  var skip_can = false;
452
454
  var skip_pp = false;
453
- switch (try beforeMove(battle, player, skip, residual, options)) {
455
+ switch (try beforeMove(battle, player, skip, first, residual, options)) {
454
456
  .done => return null,
455
457
  .skip_can => skip_can = true,
456
458
  .skip_pp => skip_pp = true,
@@ -471,6 +473,7 @@ fn beforeMove(
471
473
  battle: anytype,
472
474
  player: Player,
473
475
  skip: bool,
476
+ first: bool,
474
477
  residual: *bool,
475
478
  options: anytype,
476
479
  ) !BeforeMove {
@@ -484,17 +487,22 @@ fn beforeMove(
484
487
 
485
488
  if (Status.is(stored.status, .SLP)) {
486
489
  const before = stored.status;
490
+ const slf = Status.is(stored.status, .EXT);
487
491
  // Even if the EXT bit is set this will still correctly modify the sleep duration
488
- if (options.calc.modify(player, .sleep)) |extend| {
489
- 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,
490
498
  } else {
491
499
  stored.status -= 1;
492
500
  }
493
501
 
494
502
  const duration = Status.duration(stored.status);
495
- if (!Status.is(stored.status, .EXT)) {
496
- try options.chance.sleep(player, duration);
497
- }
503
+ try options.chance.sleep(player, if (slf)
504
+ .None
505
+ else if (duration == 0) .ended else .continuing);
498
506
 
499
507
  if (duration == 0) {
500
508
  try log.curestatus(.{ ident, before, .Message });
@@ -532,12 +540,12 @@ fn beforeMove(
532
540
  }
533
541
 
534
542
  if (volatiles.disable_duration > 0) {
535
- if (options.calc.modify(player, .disable)) |extend| {
536
- if (!extend) volatiles.disable_duration = 0;
537
- } else {
538
- volatiles.disable_duration -= 1;
539
- }
540
- try options.chance.disable(player, volatiles.disable_duration);
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
+ );
541
549
 
542
550
  if (volatiles.disable_duration == 0) {
543
551
  volatiles.disable_move = 0;
@@ -556,13 +564,25 @@ fn beforeMove(
556
564
 
557
565
  if (volatiles.Confusion) {
558
566
  assert(volatiles.confusion > 0);
559
-
560
- if (options.calc.modify(player, .confusion)) |extend| {
561
- 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,
562
581
  } else {
563
582
  volatiles.confusion -= 1;
564
583
  }
565
- try options.chance.confusion(player, volatiles.confusion);
584
+
585
+ try options.chance.confusion(player, if (volatiles.confusion == 0) .ended else .continuing);
566
586
 
567
587
  if (volatiles.confusion == 0) {
568
588
  volatiles.Confusion = false;
@@ -580,7 +600,9 @@ fn beforeMove(
580
600
  volatiles.Charging = false;
581
601
  volatiles.Binding = false;
582
602
  volatiles.Invulnerable = false;
583
- options.chance.clearDurations(player, null);
603
+
604
+ options.chance.observe(.attacking, player, .None);
605
+ options.chance.observe(.binding, player, .None);
584
606
  {
585
607
  // This feels (and is) disgusting but the cartridge literally just overwrites
586
608
  // the opponent's defense with the user's defense and resets it after. As a
@@ -588,7 +610,9 @@ fn beforeMove(
588
610
  const def = foe.active.stats.def;
589
611
  foe.active.stats.def = active.stats.def;
590
612
  defer foe.active.stats.def = def;
591
- if (!calcDamage(battle, player, player.foe(), null, false)) return .err;
613
+ if (!calcDamage(battle, player, player.foe(), null, false, options)) {
614
+ return .err;
615
+ }
592
616
  }
593
617
  // Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
594
618
  // on the targeting behavior of the confused Pokémon's selected move which results
@@ -617,7 +641,10 @@ fn beforeMove(
617
641
  volatiles.Thrashing = false;
618
642
  volatiles.Charging = false;
619
643
  volatiles.Binding = false;
620
- options.chance.clearDurations(player, null);
644
+
645
+ options.chance.observe(.attacking, player, .None);
646
+ options.chance.observe(.binding, player, .None);
647
+
621
648
  // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
622
649
  try log.cant(.{ ident, .Paralysis });
623
650
  return .done;
@@ -635,12 +662,8 @@ fn beforeMove(
635
662
  volatiles.state +%= battle.last_damage;
636
663
  }
637
664
 
638
- if (options.calc.modify(player, .attacking)) |extend| {
639
- if (!extend) volatiles.attacks = 0;
640
- } else {
641
- volatiles.attacks -= 1;
642
- }
643
- 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);
644
667
 
645
668
  if (volatiles.attacks != 0) {
646
669
  try log.activate(.{ ident, .Bide });
@@ -674,35 +697,36 @@ fn beforeMove(
674
697
  }
675
698
 
676
699
  if (volatiles.Thrashing) {
677
- if (options.calc.modify(player, .attacking)) |extend| {
678
- if (!extend) volatiles.attacks = 0;
679
- } else {
680
- volatiles.attacks -= 1;
681
- }
682
- try options.chance.attacking(player, volatiles.attacks);
700
+ try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
701
+
702
+ volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
703
+ try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
704
+
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);
683
711
 
684
- if (!showdown and handleThrashing(battle, active, player, options)) {
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)
685
716
  try log.start(.{ battle.active(player), .ConfusionSilent });
717
+ options.chance.observe(.confusion, player, if (overwritten) .overwritten else .started);
686
718
  }
687
- try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
688
- if (showdown) {
689
- // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
690
- // used in useMove and doesn't have the notion of skipping canMove semantics
691
- side.last_used_move = side.last_selected_move;
692
- if (handleThrashing(battle, active, player, options)) {
693
- try log.start(.{ battle.active(player), .ConfusionSilent });
694
- }
695
- }
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
+
696
724
  return .skip_can;
697
725
  }
698
726
 
699
727
  if (volatiles.Binding) {
700
- if (options.calc.modify(player, .binding)) |extend| {
701
- if (!extend) volatiles.attacks = 0;
702
- } else {
703
- volatiles.attacks -= 1;
704
- }
705
- try options.chance.binding(player, volatiles.attacks);
728
+ volatiles.attacks = decrement(.binding, player, options, volatiles.attacks);
729
+ try options.chance.binding(player, if (volatiles.attacks == 0) .ended else .continuing);
706
730
 
707
731
  try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
708
732
  if (showdown or battle.last_damage != 0) {
@@ -795,7 +819,7 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
795
819
  assert(active.move(mslot).pp == side.stored().move(mslot).pp);
796
820
  }
797
821
 
798
- fn incrementPP(side: *Side, mslot: u4) void {
822
+ fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
799
823
  var active = &side.active;
800
824
  const volatiles = &active.volatiles;
801
825
 
@@ -803,8 +827,11 @@ fn incrementPP(side: *Side, mslot: u4) void {
803
827
  // GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
804
828
  if (showdown and volatiles.Transform) return;
805
829
 
830
+ const n = options.calc.overridden(player, .pp) orelse 1;
831
+ if (volatiles.Transform and !options.chance.pp(player, n)) return;
832
+
806
833
  assert(mslot > 0 and mslot <= 4);
807
- 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;
808
835
  }
809
836
 
810
837
  // Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
@@ -852,13 +879,13 @@ fn doMove(
852
879
  break :miss true;
853
880
  }
854
881
  break :miss (move.target != .Self and
855
- !moveHit(battle, player, move, &immune, &mist, options));
882
+ !try moveHit(battle, player, move, &immune, &mist, options));
856
883
  };
857
884
  assert(!immune or miss or (showdown and move.effect == .Binding));
858
885
 
859
886
  var late = showdown and move.effect != .Explode;
860
887
  const skip = status or immune;
861
- if ((!showdown or (!skip or counter)) and !miss) blk: {
888
+ if ((!showdown or (!skip or counter)) and !miss) damage: {
862
889
  if (showdown and move.effect.isMulti()) {
863
890
  try Effects.multiHit(battle, player, move, options);
864
891
  hits = side.active.volatiles.attacks;
@@ -880,8 +907,9 @@ fn doMove(
880
907
  ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
881
908
  // This can overflow after adjustDamage, but will still be sufficient to OHKO
882
909
  battle.last_damage = if (ohko) 65535 else 0;
883
- if (showdown) break :blk; // skip adjustDamage / randomizeDamage
884
- } 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);
885
913
  return @as(?Result, Result.Error);
886
914
  }
887
915
  if (battle.last_damage == 0) {
@@ -895,11 +923,12 @@ fn doMove(
895
923
  }
896
924
  }
897
925
 
926
+ var missed: bool = undefined;
898
927
  const zero = battle.last_damage == 0;
899
- miss = if (showdown or skip)
900
- miss
901
- else
902
- (!moveHit(battle, player, move, &immune, &mist, options) or zero);
928
+ if (!showdown and !skip) {
929
+ missed = !try moveHit(battle, player, move, &immune, &mist, options);
930
+ miss = missed or zero;
931
+ }
903
932
 
904
933
  assert(showdown or miss or battle.last_damage > 0 or skip);
905
934
  assert((!showdown and miss) or !(ohko and immune));
@@ -926,11 +955,32 @@ fn doMove(
926
955
  try log.immune(.{ foe_ident, .OHKO });
927
956
  } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
928
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
+ }
929
979
  } else if (mist) {
930
980
  if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
931
981
  try log.fail(.{ foe_ident, .None });
932
982
  } else {
933
- if (showdown or !zero) try options.chance.commit(player, .miss);
983
+ if (!showdown) try options.chance.commit(player, if (zero and !missed) .hit else .miss);
934
984
  try log.lastmiss(.{});
935
985
  try log.miss(.{battle.active(player)});
936
986
  }
@@ -945,17 +995,15 @@ fn doMove(
945
995
  try buildRage(battle, player.foe(), options);
946
996
  } else if (showdown and move.effect == .Disable) {
947
997
  try buildRage(battle, player.foe(), options);
948
- } else if (!showdown and immune and move.effect == .Binding) {
949
- try options.chance.commit(player, .binding);
950
998
  }
951
999
  return null;
952
- } else {
1000
+ } else if (!showdown) {
953
1001
  try options.chance.commit(player, .hit);
954
1002
  }
955
1003
 
956
1004
  // On the cartridge MultiHit doesn't get set up until after damage has been applied for the
957
1005
  // first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
958
- // it up above before damage calculation).
1006
+ // it up above before damage calculation)
959
1007
  if (!showdown and move.effect.isMulti()) {
960
1008
  try Effects.multiHit(battle, player, move, options);
961
1009
  hits = side.active.volatiles.attacks;
@@ -994,32 +1042,30 @@ fn doMove(
994
1042
  try Effects.poison(battle, player, Move.get(.PoisonSting), options);
995
1043
  }
996
1044
  try log.hitcount(.{ battle.active(player.foe()), hit });
997
- } else if (showdown) {
1045
+ } else if (showdown and move.effect == .Binding) {
998
1046
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
999
- if (move.effect == .Binding) {
1000
- try Effects.binding(battle, player, rewrap, options);
1001
- if (immune) {
1002
- battle.last_damage = 0;
1003
- assert(foe.stored().hp > 0);
1004
- // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
1005
- if (sub) {
1006
- try log.activate(.{ battle.active(player.foe()), .Substitute });
1007
- } else {
1008
- try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
1009
- try buildRage(battle, player.foe(), options);
1010
- }
1011
- return null;
1047
+ try Effects.binding(battle, player, rewrap, options);
1048
+ if (immune) {
1049
+ battle.last_damage = 0;
1050
+ assert(foe.stored().hp > 0);
1051
+ // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
1052
+ if (sub) {
1053
+ try log.activate(.{ battle.active(player.foe()), .Substitute });
1054
+ } else {
1055
+ try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
1056
+ try buildRage(battle, player.foe(), options);
1012
1057
  }
1058
+ return null;
1013
1059
  }
1014
1060
  }
1015
1061
 
1016
1062
  // Substitute being broken nullifies the move's effect completely so even
1017
- // if an effect was intended to "always happen" it will still get skipped.
1063
+ // if an effect was intended to "always happen" it will still get skipped
1018
1064
  if (nullified) return null;
1019
1065
 
1020
1066
  // On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
1021
1067
  // but this is only done to setup the MultiHit looping in the first place. Moving the MultiHit
1022
- // setup before the loop means we can avoid having to waste time doing no-op handler searches.
1068
+ // setup before the loop means we can avoid having to waste time doing no-op handler searches
1023
1069
  if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
1024
1070
 
1025
1071
  if (foe.stored().hp == 0) return null;
@@ -1034,7 +1080,7 @@ fn doMove(
1034
1080
  // below). For Twineedle we change the data to that of one with PoisonChance1 given its
1035
1081
  // MultiHit behavior is complete after the loop above, though Pokémon Showdown handles the
1036
1082
  // Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
1037
- // puts the |-status| message before |-hitcount| instead of after.
1083
+ // puts the |-status| message before |-hitcount| instead of after
1038
1084
  if (move.effect == .Twineedle) {
1039
1085
  if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1040
1086
  } else if (move.effect == .Disable) {
@@ -1058,7 +1104,9 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
1058
1104
  rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
1059
1105
  rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
1060
1106
 
1061
- return Rolls.criticalHit(battle, player, @intCast(rate), options);
1107
+ const spurious =
1108
+ !showdown and ((move.bp == 0 and move.effect != .OHKO) or move.target == .Depends);
1109
+ return Rolls.criticalHit(battle, player, @intCast(rate), spurious, options);
1062
1110
  }
1063
1111
 
1064
1112
  fn calcDamage(
@@ -1067,6 +1115,7 @@ fn calcDamage(
1067
1115
  target_player: Player,
1068
1116
  m: ?Move.Data,
1069
1117
  crit: bool,
1118
+ options: anytype,
1070
1119
  ) bool {
1071
1120
  // Confusion (indicated when m == null) just needs a 40 BP physical move
1072
1121
  const cfz = m == null;
@@ -1078,28 +1127,8 @@ fn calcDamage(
1078
1127
 
1079
1128
  const special = move.type.special();
1080
1129
 
1081
- // zig fmt: off
1082
- var atk: u32 =
1083
- if (crit)
1084
- if (special) side.stored().stats.spc
1085
- else side.stored().stats.atk
1086
- else
1087
- if (special) side.active.stats.spc
1088
- else side.active.stats.atk;
1089
- var def: u32 =
1090
- if (crit)
1091
- if (special) target.stored().stats.spc
1092
- else target.stored().stats.def
1093
- else
1094
- // GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
1095
- if (special)
1096
- target.active.stats.spc *
1097
- @as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
1098
- // Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
1099
- else
1100
- target.active.stats.def *
1101
- @as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
1102
- // zig fmt: on
1130
+ var atk = attack(side, crit, special);
1131
+ var def = defense(target, crit, special, cfz);
1103
1132
 
1104
1133
  // Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
1105
1134
  // will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
@@ -1124,11 +1153,41 @@ fn calcDamage(
1124
1153
  d = @min(997, d);
1125
1154
  d += 2;
1126
1155
 
1156
+ // Whether or not a critical hit occurred can change whether a division-by-zero freeze is
1157
+ // encountered so we need to make sure that in the event of a miss we commit the status of the
1158
+ // crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
1159
+ // that results in counter intuitive probabilities in the common case
1160
+ if (pkmn.options.chance and !showdown) {
1161
+ atk = attack(side, !crit, special);
1162
+ def = defense(target, !crit, special, false);
1163
+ if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
1164
+ }
1165
+
1127
1166
  battle.last_damage = @intCast(d);
1128
1167
 
1129
1168
  return true;
1130
1169
  }
1131
1170
 
1171
+ fn attack(side: *Side, crit: bool, special: bool) u32 {
1172
+ return if (crit)
1173
+ if (special) side.stored().stats.spc else side.stored().stats.atk
1174
+ else if (special) side.active.stats.spc else side.active.stats.atk;
1175
+ }
1176
+
1177
+ fn defense(target: *Side, crit: bool, special: bool, cfz: bool) u32 {
1178
+ return if (crit)
1179
+ if (special) target.stored().stats.spc else target.stored().stats.def
1180
+ else
1181
+ // GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
1182
+ if (special)
1183
+ target.active.stats.spc *
1184
+ @as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
1185
+ else
1186
+ // Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
1187
+ target.active.stats.def *
1188
+ @as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
1189
+ }
1190
+
1132
1191
  fn adjustDamage(battle: anytype, player: Player) u16 {
1133
1192
  const side = battle.side(player);
1134
1193
  const foe = battle.foe(player);
@@ -1173,7 +1232,6 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1173
1232
  const foe = battle.foe(player);
1174
1233
 
1175
1234
  if (!try checkHit(battle, player, move, options)) return null;
1176
- try options.chance.commit(player, .hit);
1177
1235
 
1178
1236
  battle.last_damage = switch (side.last_selected_move) {
1179
1237
  .SuperFang => @max(foe.stored().hp / 2, 1),
@@ -1234,9 +1292,8 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1234
1292
 
1235
1293
  battle.last_damage *|= 2;
1236
1294
 
1237
- // Pokémon Showdown calls checkHit before Counter
1295
+ // Pokémon Showdown calls moveHit before Counter
1238
1296
  if (!showdown and !try checkHit(battle, player, move, options)) return null;
1239
- try options.chance.commit(player, .hit);
1240
1297
 
1241
1298
  const sub = showdown and foe.active.volatiles.Substitute;
1242
1299
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -1265,7 +1322,7 @@ fn applyDamage(
1265
1322
  subbed.active.volatiles.Substitute = false;
1266
1323
  // battle.last_damage is not updated with the amount of HP the Substitute had
1267
1324
  try options.log.end(.{ battle.active(sub_player), .Substitute });
1268
- options.calc.capped(target_player);
1325
+ if (reason != .Confusion) options.calc.capped(sub_player);
1269
1326
  return true;
1270
1327
  } else {
1271
1328
  // Safe to truncate since less than subbed.volatiles.substitute which is a u8
@@ -1299,7 +1356,7 @@ fn mirrorMove(
1299
1356
  try options.log.fail(.{ battle.active(player), .None });
1300
1357
  return null;
1301
1358
  } else if (!showdown or foe.last_used_move != .Struggle) {
1302
- incrementPP(side, mslot);
1359
+ incrementPP(side, player, mslot, options);
1303
1360
  }
1304
1361
 
1305
1362
  if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
@@ -1320,7 +1377,7 @@ fn metronome(
1320
1377
  var side = battle.side(player);
1321
1378
 
1322
1379
  side.last_selected_move = try Rolls.metronome(battle, player, options);
1323
- incrementPP(side, mslot);
1380
+ incrementPP(side, player, mslot, options);
1324
1381
 
1325
1382
  if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
1326
1383
  return null;
@@ -1332,7 +1389,10 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
1332
1389
  var immune = false;
1333
1390
  var mist = false;
1334
1391
 
1335
- if (moveHit(battle, player, move, &immune, &mist, options)) return true;
1392
+ if (try moveHit(battle, player, move, &immune, &mist, options)) {
1393
+ if (!showdown) try options.chance.commit(player, .hit);
1394
+ return true;
1395
+ }
1336
1396
 
1337
1397
  assert(!immune);
1338
1398
  if (mist) {
@@ -1341,11 +1401,11 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
1341
1401
  try options.log.activate(.{ foe_ident, .Mist });
1342
1402
  try options.log.fail(.{ foe_ident, .None });
1343
1403
  } else {
1344
- try options.chance.commit(player, .miss);
1345
1404
  try options.log.lastmiss(.{});
1346
1405
  try options.log.miss(.{battle.active(player)});
1347
1406
  }
1348
1407
 
1408
+ if (!showdown) try options.chance.commit(player, .miss);
1349
1409
  return false;
1350
1410
  }
1351
1411
 
@@ -1356,7 +1416,7 @@ fn moveHit(
1356
1416
  immune: *bool,
1357
1417
  mist: *bool,
1358
1418
  options: anytype,
1359
- ) bool {
1419
+ ) !bool {
1360
1420
  var side = battle.side(player);
1361
1421
  const foe = battle.foe(player);
1362
1422
 
@@ -1407,7 +1467,7 @@ fn moveHit(
1407
1467
  if (overwrite) side.active.volatiles.state = accuracy;
1408
1468
 
1409
1469
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1410
- break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
1470
+ break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
1411
1471
  };
1412
1472
 
1413
1473
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1418,13 +1478,17 @@ fn moveHit(
1418
1478
 
1419
1479
  if (!miss) return true;
1420
1480
  if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
1481
+
1421
1482
  side.active.volatiles.Binding = false;
1483
+ options.chance.observe(.binding, player, .None);
1484
+
1422
1485
  return false;
1423
1486
  }
1424
1487
 
1425
1488
  fn checkFaint(
1426
1489
  battle: anytype,
1427
1490
  player: Player,
1491
+ cap: bool,
1428
1492
  options: anytype,
1429
1493
  ) @TypeOf(options.log).Error!?Result {
1430
1494
  const side = battle.side(player);
@@ -1437,8 +1501,8 @@ fn checkFaint(
1437
1501
  const foe_out = foe_fainted and findFirstAlive(foe) == 0;
1438
1502
  const more = player_out or foe_out;
1439
1503
 
1440
- if (try faint(battle, player, !(more or foe_fainted), options)) |r| return r;
1441
- if (foe_fainted) if (try faint(battle, player.foe(), !more, options)) |r| return r;
1504
+ if (try faint(battle, player, !(more or foe_fainted), cap, options)) |r| return r;
1505
+ if (foe_fainted) if (try faint(battle, player.foe(), !more, false, options)) |r| return r;
1442
1506
 
1443
1507
  assert(!side.active.volatiles.MultiHit);
1444
1508
  assert(!foe.active.volatiles.MultiHit);
@@ -1459,7 +1523,7 @@ fn checkFaint(
1459
1523
  return .{ .p1 = foe_choice, .p2 = .Switch };
1460
1524
  }
1461
1525
 
1462
- fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
1526
+ fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytype) !?Result {
1463
1527
  var side = battle.side(player);
1464
1528
  var foe = battle.foe(player);
1465
1529
  assert(side.stored().hp == 0);
@@ -1493,7 +1557,9 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
1493
1557
  }
1494
1558
 
1495
1559
  try options.log.faint(.{ battle.active(player), done });
1496
- options.calc.capped(player);
1560
+ // We can only safely set capped if the faint occured as the direct result of an opponent's
1561
+ // move (as opposed to due to residual damage, recoil, Explosion, etc)
1562
+ if (cap) options.calc.capped(player);
1497
1563
  return null;
1498
1564
  }
1499
1565
 
@@ -1653,17 +1719,6 @@ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1653
1719
  }
1654
1720
  }
1655
1721
 
1656
- fn handleThrashing(battle: anytype, active: *ActivePokemon, player: Player, options: anytype) bool {
1657
- var volatiles = &active.volatiles;
1658
- assert(volatiles.Thrashing);
1659
- if (volatiles.attacks > 0) return false;
1660
-
1661
- volatiles.Thrashing = false;
1662
- volatiles.Confusion = true;
1663
- volatiles.confusion = Rolls.confusionDuration(battle, player, true, options);
1664
- return true;
1665
- }
1666
-
1667
1722
  fn onBegin(
1668
1723
  battle: anytype,
1669
1724
  player: Player,
@@ -1750,8 +1805,10 @@ pub const Effects = struct {
1750
1805
 
1751
1806
  side.active.volatiles.Bide = true;
1752
1807
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1808
+
1753
1809
  side.active.volatiles.state = 0;
1754
1810
  side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
1811
+ options.chance.observe(.attacking, player, .started);
1755
1812
 
1756
1813
  try options.log.start(.{ battle.active(player), .Bide });
1757
1814
  }
@@ -1813,12 +1870,13 @@ pub const Effects = struct {
1813
1870
  if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1814
1871
  if (!try checkHit(battle, player, move, options)) return;
1815
1872
  }
1816
- try options.chance.commit(player, .hit);
1817
1873
  }
1818
1874
 
1819
1875
  if (foe.active.volatiles.Confusion) return;
1820
1876
  foe.active.volatiles.Confusion = true;
1821
- foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
1877
+
1878
+ foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
1879
+ options.chance.observe(.confusion, player.foe(), .started);
1822
1880
 
1823
1881
  try options.log.start(.{ battle.active(player.foe()), .Confusion });
1824
1882
  }
@@ -1846,10 +1904,7 @@ pub const Effects = struct {
1846
1904
  const foe_ident = battle.active(player.foe());
1847
1905
 
1848
1906
  // Pokémon Showdown handles hit/miss earlier in doMove
1849
- if (!showdown) {
1850
- if (!try checkHit(battle, player, move, options)) return null;
1851
- try options.chance.commit(player, .hit);
1852
- }
1907
+ if (!showdown and !try checkHit(battle, player, move, options)) return null;
1853
1908
 
1854
1909
  if (volatiles.disable_move != 0) {
1855
1910
  try options.log.fail(.{ foe_ident, .None });
@@ -1879,6 +1934,7 @@ pub const Effects = struct {
1879
1934
  volatiles.disable_move =
1880
1935
  @intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
1881
1936
  volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
1937
+ options.chance.observe(.disable, player.foe(), .started);
1882
1938
 
1883
1939
  const id = foe.active.move(volatiles.disable_move).id;
1884
1940
  try options.log.start(.{ foe_ident, .Disable, id });
@@ -1985,22 +2041,24 @@ pub const Effects = struct {
1985
2041
  if (p != player and Status.any(s.stored().status)) {
1986
2042
  try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
1987
2043
  s.stored().status = 0;
2044
+ options.chance.observe(.sleep, p, .None);
1988
2045
  } else if (showdown and s.stored().status == Status.TOX) {
1989
2046
  s.stored().status = Status.init(.PSN);
1990
2047
  try log.status(.{ battle.active(p), s.stored().status, .None });
1991
2048
  }
1992
- try clearVolatiles(battle, p, p != player, options);
2049
+ try clearVolatiles(battle, p, options);
1993
2050
  }
1994
2051
  } else {
1995
2052
  if (Status.any(foe_stored.status)) {
1996
2053
  if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
1997
2054
  foe.last_selected_move = .SKIP_TURN;
2055
+ options.chance.observe(.sleep, player.foe(), .None);
1998
2056
  }
1999
2057
  try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2000
2058
  foe_stored.status = 0;
2001
2059
  }
2002
- try clearVolatiles(battle, player, false, options);
2003
- try clearVolatiles(battle, player.foe(), true, options);
2060
+ try clearVolatiles(battle, player, options);
2061
+ try clearVolatiles(battle, player.foe(), options);
2004
2062
  }
2005
2063
  }
2006
2064
 
@@ -2051,7 +2109,6 @@ pub const Effects = struct {
2051
2109
  }
2052
2110
  }
2053
2111
 
2054
- try options.chance.commit(player, .hit);
2055
2112
  foe.active.volatiles.LeechSeed = true;
2056
2113
 
2057
2114
  try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
@@ -2092,12 +2149,10 @@ pub const Effects = struct {
2092
2149
  if (!has_mimic) {
2093
2150
  // Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
2094
2151
  if (!try checkHit(battle, player, move, options)) return;
2095
- try options.chance.commit(player, .hit);
2096
2152
  return options.log.fail(.{ battle.active(player.foe()), .None });
2097
2153
  }
2098
2154
  }
2099
2155
  if (!try checkHit(battle, player, move, options)) return;
2100
- try options.chance.commit(player, .hit);
2101
2156
 
2102
2157
  const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
2103
2158
  side.active.move(oslot).id = foe.active.move(rslot).id;
@@ -2151,7 +2206,6 @@ pub const Effects = struct {
2151
2206
  if (immune) return log.immune(.{ foe_ident, .None });
2152
2207
  if (!try checkHit(battle, player, move, options)) return;
2153
2208
  }
2154
- try options.chance.commit(player, .hit);
2155
2209
 
2156
2210
  foe_stored.status = Status.init(.PAR);
2157
2211
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
@@ -2219,7 +2273,6 @@ pub const Effects = struct {
2219
2273
 
2220
2274
  if (move.effect == .Poison) {
2221
2275
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2222
- try options.chance.commit(player, .hit);
2223
2276
  } else {
2224
2277
  if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
2225
2278
  return;
@@ -2288,9 +2341,8 @@ pub const Effects = struct {
2288
2341
  if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
2289
2342
  });
2290
2343
  }
2291
- // If checkHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2344
+ // If moveHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2292
2345
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2293
- try options.chance.commit(player, .hit);
2294
2346
  }
2295
2347
 
2296
2348
  // Sleep Clause Mod
@@ -2302,6 +2354,8 @@ pub const Effects = struct {
2302
2354
  foe.active.volatiles.Recharging = false;
2303
2355
 
2304
2356
  foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2357
+ options.chance.observe(.sleep, player.foe(), .started);
2358
+
2305
2359
  const last = battle.side(player).last_selected_move;
2306
2360
  try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
2307
2361
  }
@@ -2343,9 +2397,7 @@ pub const Effects = struct {
2343
2397
  // Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
2344
2398
  if (showdown) {
2345
2399
  if (battle.side(player).last_selected_move == .Teleport) return;
2346
- if (try checkHit(battle, player, move, options)) {
2347
- try options.chance.commit(player, .hit);
2348
- }
2400
+ _ = try checkHit(battle, player, move, options);
2349
2401
  battle.last_damage = 0;
2350
2402
  } else {
2351
2403
  try options.log.fail(.{ battle.active(player), .None });
@@ -2360,6 +2412,8 @@ pub const Effects = struct {
2360
2412
 
2361
2413
  volatiles.Thrashing = true;
2362
2414
  volatiles.attacks = Rolls.attackingDuration(battle, player, options);
2415
+
2416
+ options.chance.observe(.attacking, player, .started);
2363
2417
  }
2364
2418
 
2365
2419
  fn transform(battle: anytype, player: Player, options: anytype) !void {
@@ -2405,6 +2459,7 @@ pub const Effects = struct {
2405
2459
 
2406
2460
  side.active.volatiles.attacks =
2407
2461
  try Rolls.distribution(battle, .Binding, player, options) - 1;
2462
+ options.chance.observe(.binding, player, .started);
2408
2463
  }
2409
2464
 
2410
2465
  fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2521,7 +2576,6 @@ pub const Effects = struct {
2521
2576
  } else {
2522
2577
  // checkHit already checks for Invulnerable
2523
2578
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2524
- try options.chance.commit(player, .hit);
2525
2579
  }
2526
2580
 
2527
2581
  var stats = &foe.active.stats;
@@ -2635,22 +2689,22 @@ fn isForced(active: anytype) bool {
2635
2689
  active.volatiles.Thrashing or active.volatiles.Charging;
2636
2690
  }
2637
2691
 
2638
- fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !void {
2692
+ fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
2639
2693
  var log = options.log;
2640
2694
  var side = battle.side(who);
2641
2695
  var volatiles = &side.active.volatiles;
2642
2696
  const ident = battle.active(who);
2643
2697
 
2644
- options.chance.clearDurations(who, clear);
2645
-
2646
2698
  if (volatiles.disable_move != 0) {
2647
2699
  volatiles.disable_move = 0;
2648
2700
  volatiles.disable_duration = 0;
2701
+ options.chance.observe(.disable, who, .None);
2649
2702
  try log.end(.{ ident, .DisableSilent });
2650
2703
  }
2651
2704
  if (volatiles.Confusion) {
2652
2705
  // volatiles.confusion is left unchanged
2653
2706
  volatiles.Confusion = false;
2707
+ options.chance.observe(.confusion, who, .None);
2654
2708
  try log.end(.{ ident, .ConfusionSilent });
2655
2709
  }
2656
2710
  if (volatiles.Mist) {
@@ -2685,6 +2739,19 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2685
2739
  }
2686
2740
  }
2687
2741
 
2742
+ fn decrement(
2743
+ comptime field: chance.Action.Field,
2744
+ player: Player,
2745
+ options: anytype,
2746
+ n: anytype,
2747
+ ) @TypeOf(n) {
2748
+ return if (options.calc.overridden(player, field)) |obs| switch (obs) {
2749
+ .started, .ended => 0,
2750
+ .continuing => if (n > 1) n - 1 else n,
2751
+ else => unreachable,
2752
+ } else n - 1;
2753
+ }
2754
+
2688
2755
  pub const Rolls = struct {
2689
2756
  fn speedTie(battle: anytype, options: anytype) !bool {
2690
2757
  const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
@@ -2700,7 +2767,13 @@ pub const Rolls = struct {
2700
2767
  return p1;
2701
2768
  }
2702
2769
 
2703
- fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2770
+ fn criticalHit(
2771
+ battle: anytype,
2772
+ player: Player,
2773
+ rate: u8,
2774
+ spurious: bool,
2775
+ options: anytype,
2776
+ ) !bool {
2704
2777
  const crit = if (options.calc.overridden(player, .critical_hit)) |val|
2705
2778
  val == .true
2706
2779
  else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
@@ -2710,7 +2783,7 @@ pub const Rolls = struct {
2710
2783
  else
2711
2784
  std.math.rotl(u8, battle.rng.next(), 3) < rate;
2712
2785
 
2713
- try options.chance.criticalHit(player, crit, rate);
2786
+ if (!spurious) try options.chance.criticalHit(player, crit, rate);
2714
2787
  return crit;
2715
2788
  }
2716
2789
 
@@ -2732,17 +2805,19 @@ pub const Rolls = struct {
2732
2805
  return roll;
2733
2806
  }
2734
2807
 
2735
- fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) bool {
2808
+ const uN = if (pkmn.options.miss) u8 else u9;
2809
+ fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) !bool {
2810
+ const acc: uN = if (pkmn.options.miss) accuracy else @as(uN, accuracy) + 1;
2736
2811
  const ok = if (options.calc.overridden(player, .hit)) |val|
2737
2812
  val == .true
2738
2813
  else if (@hasDecl(@TypeOf(battle.rng), "hit"))
2739
- battle.rng.hit(player, accuracy)
2814
+ battle.rng.hit(player, acc)
2740
2815
  else if (showdown)
2741
- battle.rng.chance(u8, accuracy, 256)
2816
+ battle.rng.chance(uN, acc, 256)
2742
2817
  else
2743
- battle.rng.next() < accuracy;
2818
+ battle.rng.next() < acc;
2744
2819
 
2745
- options.chance.hit(ok, accuracy);
2820
+ try options.chance.hit(player, ok, acc);
2746
2821
  return ok;
2747
2822
  }
2748
2823
 
@@ -2874,9 +2949,10 @@ pub const Rolls = struct {
2874
2949
  }
2875
2950
 
2876
2951
  fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2877
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2878
- @intCast(val)
2879
- else if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
2952
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
2953
+ if (val >= 1 and val <= 7) @as(u3, @intCast(val)) else null
2954
+ else
2955
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
2880
2956
  battle.rng.sleepDuration(player)
2881
2957
  else if (showdown)
2882
2958
  battle.rng.range(u3, 1, 8)
@@ -2888,14 +2964,15 @@ pub const Rolls = struct {
2888
2964
  };
2889
2965
 
2890
2966
  assert(duration >= 1 and duration <= 7);
2891
- options.chance.duration(.sleep, player, player.foe(), duration);
2967
+ options.chance.duration(player, duration);
2892
2968
  return duration;
2893
2969
  }
2894
2970
 
2895
2971
  fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2896
- const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
2897
- val
2898
- else if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
2972
+ const duration: u4 = (if (options.calc.overridden(player, .duration)) |val|
2973
+ if (val >= 1 and val <= 8) val else null
2974
+ else
2975
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
2899
2976
  battle.rng.disableDuration(player)
2900
2977
  else if (showdown)
2901
2978
  battle.rng.range(u4, 1, 9)
@@ -2903,29 +2980,31 @@ pub const Rolls = struct {
2903
2980
  @intCast((battle.rng.next() & 7) + 1);
2904
2981
 
2905
2982
  assert(duration >= 1 and duration <= 8);
2906
- options.chance.duration(.disable, player, player.foe(), duration);
2983
+ options.chance.duration(player, duration);
2907
2984
  return duration;
2908
2985
  }
2909
2986
 
2910
- fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
2911
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2912
- @intCast(val)
2913
- else if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
2914
- battle.rng.confusionDuration(player, self)
2987
+ fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
2988
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
2989
+ if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
2990
+ else
2991
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
2992
+ battle.rng.confusionDuration(player)
2915
2993
  else if (showdown)
2916
2994
  battle.rng.range(u3, 2, 6)
2917
2995
  else
2918
2996
  @intCast((battle.rng.next() & 3) + 2);
2919
2997
 
2920
2998
  assert(duration >= 2 and duration <= 5);
2921
- options.chance.duration(.confusion, player, if (self) player else player.foe(), duration);
2999
+ options.chance.duration(player, duration);
2922
3000
  return duration;
2923
3001
  }
2924
3002
 
2925
3003
  fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2926
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2927
- @intCast(val)
2928
- else if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
3004
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
3005
+ if (val >= 2 and val <= 3) @as(u3, @intCast(val)) else null
3006
+ else
3007
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
2929
3008
  battle.rng.attackingDuration(player)
2930
3009
  else if (showdown)
2931
3010
  battle.rng.range(u3, 2, 4)
@@ -2933,7 +3012,7 @@ pub const Rolls = struct {
2933
3012
  @intCast((battle.rng.next() & 1) + 2);
2934
3013
 
2935
3014
  assert(duration >= 2 and duration <= 3);
2936
- options.chance.duration(.attacking, player, player, duration);
3015
+ options.chance.duration(player, duration);
2937
3016
  return duration;
2938
3017
  }
2939
3018
 
@@ -2946,9 +3025,10 @@ pub const Rolls = struct {
2946
3025
  options: anytype,
2947
3026
  ) !u3 {
2948
3027
  const roll = if (effect == .Binding) .duration else .multi_hit;
2949
- const n: u3 = if (options.calc.overridden(player, roll)) |val|
2950
- @intCast(val)
2951
- else if (@hasDecl(@TypeOf(battle.rng), "distribution"))
3028
+ const n: u3 = (if (options.calc.overridden(player, roll)) |val|
3029
+ if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
3030
+ else
3031
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "distribution"))
2952
3032
  battle.rng.distribution(player)
2953
3033
  else if (showdown)
2954
3034
  DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
@@ -2959,7 +3039,7 @@ pub const Rolls = struct {
2959
3039
 
2960
3040
  assert(n >= 2 and n <= 5);
2961
3041
  if (effect == .Binding) {
2962
- options.chance.duration(.binding, player, player, n);
3042
+ options.chance.duration(player, n);
2963
3043
  } else {
2964
3044
  try options.chance.multiHit(player, n);
2965
3045
  }
@@ -2974,15 +3054,13 @@ pub const Rolls = struct {
2974
3054
  options: anytype,
2975
3055
  ) !u4 {
2976
3056
  // TODO: consider throwing error instead of rerolling?
2977
- const overridden = if (options.calc.overridden(player, .move_slot)) |val|
3057
+ const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
2978
3058
  if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
2979
3059
  val
2980
3060
  else
2981
3061
  null
2982
3062
  else
2983
- null;
2984
-
2985
- const slot: u4 = overridden orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
3063
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
2986
3064
  battle.rng.moveSlot(player, moves, check_pp)
2987
3065
  else slot: {
2988
3066
  if (showdown) {
@@ -3068,7 +3146,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3068
3146
  const stored = side.stored();
3069
3147
 
3070
3148
  // While players are not given any input options on the cartridge in these cases,
3071
- // Pokémon Showdown instead produces a list with a single move that must be chosen.
3149
+ // Pokémon Showdown instead produces a list with a single move that must be chosen
3072
3150
  //
3073
3151
  // Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
3074
3152
  // here when not in compatibility mode, but the engine needs to be able to differentiate
@@ -3076,7 +3154,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3076
3154
  // passing due to being forced into using a move. Instead of introducing another option
3077
3155
  // we simply repurpose Move with no move slot, even though pedantically this is not
3078
3156
  // strictly correct as the player would not have been presented the option to move or
3079
- // switch at all.
3157
+ // switch at all
3080
3158
  if (isForced(active)) {
3081
3159
  out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
3082
3160
  n += 1;
@@ -3095,7 +3173,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3095
3173
  // On the cartridge, all of these happen after "FIGHT" (indicating you are not
3096
3174
  // switching) but before you are allowed to select a move. Pokémon Showdown instead
3097
3175
  // either disables all other moves in the case of limited or requires you to select a
3098
- // move normally if sleeping/frozen/bound.
3176
+ // move normally if sleeping/frozen/bound
3099
3177
  if (!showdown and (limited or foe.active.volatiles.Binding or
3100
3178
  Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
3101
3179
  {
@@ -3108,7 +3186,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3108
3186
  // Pokémon Showdown handles Bide and Binding moves by checking if the move in question
3109
3187
  // is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
3110
3188
  // Move will not result in forcing the subsequent use unless the user also had the
3111
- // proc-ed move in their moveset) and disabling all other moves.
3189
+ // proc-ed move in their moveset) and disabling all other moves
3112
3190
  if (limited) {
3113
3191
  assert(showdown);
3114
3192
  assert(side.last_selected_move != .None);