@pkmn/engine 0.1.0-dev.8b192003 → 0.1.0-dev.8c935a85

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 (79) hide show
  1. package/README.md +9 -6
  2. package/build/pkg/addon/browser.d.ts +3 -0
  3. package/build/pkg/addon/browser.js +47 -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 +110 -0
  7. package/build/pkg/addon/node.js.map +1 -0
  8. package/build/pkg/addon/wasm.d.ts +2 -0
  9. package/build/pkg/addon/wasm.js +53 -0
  10. package/build/pkg/addon/wasm.js.map +1 -0
  11. package/build/pkg/addon.d.ts +25 -3
  12. package/build/pkg/addon.js +34 -65
  13. package/build/pkg/addon.js.map +1 -1
  14. package/build/pkg/data/layout.json +15 -2
  15. package/build/pkg/data/protocol.json +23 -9
  16. package/build/pkg/data.d.ts +23 -1
  17. package/build/pkg/data.js.map +1 -1
  18. package/build/pkg/gen1.d.ts +15 -18
  19. package/build/pkg/gen1.js +28 -19
  20. package/build/pkg/gen1.js.map +1 -1
  21. package/build/pkg/index.d.ts +40 -90
  22. package/build/pkg/index.js +23 -70
  23. package/build/pkg/index.js.map +1 -1
  24. package/build/pkg/protocol.d.ts +20 -1
  25. package/build/pkg/protocol.js +105 -103
  26. package/build/pkg/protocol.js.map +1 -1
  27. package/build/tools/debug.js +52 -76
  28. package/build/tools/debug.js.map +1 -1
  29. package/build/tools/display/debug.jsx +152 -0
  30. package/build/tools/display/debug.jsx.map +1 -0
  31. package/build/tools/display/index.js +152 -0
  32. package/build/tools/display/index.js.map +1 -0
  33. package/build/tools/display/ui.jsx +359 -0
  34. package/build/tools/display/ui.jsx.map +1 -0
  35. package/build/tools/display/util.js +33 -0
  36. package/build/tools/display/util.js.map +1 -0
  37. package/build.zig +330 -85
  38. package/package.json +54 -135
  39. package/src/bin/install-pkmn-engine +10 -7
  40. package/src/bin/pkmn-debug +1 -1
  41. package/src/lib/bindings/c.zig +255 -0
  42. package/src/lib/bindings/node.zig +121 -0
  43. package/src/lib/bindings/wasm.zig +59 -0
  44. package/src/lib/common/array.zig +77 -0
  45. package/src/lib/common/data.zig +8 -8
  46. package/src/lib/common/debug.zig +39 -8
  47. package/src/lib/common/js.zig +4 -2
  48. package/src/lib/common/optional.zig +13 -9
  49. package/src/lib/common/options.zig +4 -0
  50. package/src/lib/common/protocol.zig +45 -44
  51. package/src/lib/common/rational.zig +256 -181
  52. package/src/lib/common/rng.zig +5 -7
  53. package/src/lib/common/util.zig +60 -1
  54. package/src/lib/gen1/calc.zig +314 -164
  55. package/src/lib/gen1/chance.zig +477 -119
  56. package/src/lib/gen1/data/moves.zig +2 -4
  57. package/src/lib/gen1/data/species.zig +1 -3
  58. package/src/lib/gen1/data/types.zig +1 -2
  59. package/src/lib/gen1/data.zig +35 -38
  60. package/src/lib/gen1/helpers.zig +17 -27
  61. package/src/lib/gen1/mechanics.zig +249 -156
  62. package/src/lib/gen2/calc.zig +37 -27
  63. package/src/lib/gen2/chance.zig +87 -67
  64. package/src/lib/gen2/data/items.zig +1 -3
  65. package/src/lib/gen2/data/moves.zig +2 -4
  66. package/src/lib/gen2/data/species.zig +1 -3
  67. package/src/lib/gen2/data/types.zig +1 -2
  68. package/src/lib/gen2/data.zig +26 -29
  69. package/src/lib/gen2/generated.zig +18 -23
  70. package/src/lib/gen2/helpers.zig +9 -18
  71. package/src/lib/gen2/mechanics.zig +81 -81
  72. package/src/lib/node.zig +3 -116
  73. package/src/lib/pkmn.zig +14 -7
  74. package/src/lib/wasm.zig +13 -9
  75. package/src/tools/display/dom.ts +93 -0
  76. package/src/tools/display/ui.css +354 -0
  77. package/build/test/display.js +0 -400
  78. package/build/test/display.js.map +0 -1
  79. package/src/test/display/pkmn.html.tmpl +0 -368
@@ -1,39 +1,29 @@
1
- const std = @import("std");
2
-
3
- const pkmn = @import("../pkmn.zig");
1
+ const DEBUG = @import("../common/debug.zig").print;
4
2
 
3
+ const builtin = @import("builtin");
4
+ const chance = @import("chance.zig");
5
5
  const common = @import("../common/data.zig");
6
- const DEBUG = @import("../common/debug.zig").print;
7
- const optional = @import("../common/optional.zig");
6
+ const data = @import("data.zig");
7
+ const pkmn = @import("../pkmn.zig");
8
8
  const protocol = @import("../common/protocol.zig");
9
9
  const rng = @import("../common/rng.zig");
10
-
11
- const data = @import("data.zig");
12
- const chance = @import("chance.zig");
10
+ const std = @import("std");
13
11
 
14
12
  const assert = std.debug.assert;
15
-
16
- const expectEqual = std.testing.expectEqual;
17
-
18
- const showdown = pkmn.options.showdown;
19
-
20
- const Choice = common.Choice;
21
- const ID = common.ID;
22
- const Player = common.Player;
23
- const Result = common.Result;
24
-
25
13
  const Boost = protocol.Boost;
14
+ const Choice = common.Choice;
26
15
  const Damage = protocol.Damage;
16
+ const Effectiveness = data.Effectiveness;
17
+ const expectEqual = std.testing.expectEqual;
27
18
  const Fail = protocol.Fail;
28
- const Heal = protocol.Heal;
29
-
30
19
  const Gen12 = rng.Gen12;
31
-
32
- const ActivePokemon = data.ActivePokemon;
33
- const Effectiveness = data.Effectiveness;
20
+ const Heal = protocol.Heal;
21
+ const ID = common.ID;
34
22
  const Move = data.Move;
35
23
  const MoveSlot = data.MoveSlot;
36
- const Pokemon = data.Pokemon;
24
+ const Player = common.Player;
25
+ const Result = common.Result;
26
+ const showdown = pkmn.options.showdown;
37
27
  const Side = data.Side;
38
28
  const Species = data.Species;
39
29
  const Stats = data.Stats;
@@ -176,7 +166,10 @@ fn selectMove(
176
166
  if (foe_choice.type == .Switch) {
177
167
  const last = side.active.move(battle.lastMove(player).index);
178
168
  if (last.id == .Metronome) side.last_selected_move = last.id;
179
- if (last.id == .MirrorMove) return Result.Error;
169
+ if (last.id == .MirrorMove) {
170
+ if (!pkmn.options.mod) return Result.Error;
171
+ side.last_selected_move = last.id;
172
+ }
180
173
  }
181
174
  }
182
175
  return null;
@@ -255,6 +248,7 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: a
255
248
  active.moves = incoming.moves;
256
249
 
257
250
  statusModify(incoming.status, &active.stats);
251
+ options.chance.switched(player, slot);
258
252
 
259
253
  foe.active.volatiles.Binding = false;
260
254
 
@@ -354,7 +348,7 @@ fn doTurn(
354
348
  if (try checkFaint(battle, player, false, options)) |r| return r;
355
349
  } else if (foe_choice.type == .Pass) return null;
356
350
 
357
- options.chance.clearPending();
351
+ if (!showdown) options.chance.clearPending();
358
352
 
359
353
  residual = true;
360
354
  replace = battle.side(foe_player).stored().hp == 0;
@@ -426,7 +420,7 @@ fn executeMove(
426
420
  mslot = @intCast(battle.lastMove(player).index);
427
421
  const stored = side.stored();
428
422
  // GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
429
- auto = isForced(&side.active) or
423
+ auto = pkmn.options.mod or isForced(&side.active) or
430
424
  side.active.volatiles.Binding or side.active.volatiles.Bide or
431
425
  side.last_selected_move == .HyperBeam or
432
426
  Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
@@ -459,6 +453,25 @@ fn executeMove(
459
453
  .err => return @as(?Result, Result.Error),
460
454
  }
461
455
 
456
+ // Pokémon Showdown incorrectly implements PP deduction when handling the Hyper Beam automatic
457
+ // selection glitch - if the move was proced due to Metronome / Mirror Move it either subtracts
458
+ // from the Hyper Beam slot if present or skips PP deduction entirely...
459
+ if (showdown and !skip_pp and side.last_selected_move == .HyperBeam and
460
+ side.last_selected_move != side.active.move(mslot).id)
461
+ {
462
+ assert(mslot == 1);
463
+ const has_beam = has_beam: {
464
+ for (side.active.moves, 0..) |m, i| {
465
+ if (m.id == .HyperBeam) {
466
+ mslot = @intCast(i + 1);
467
+ break :has_beam true;
468
+ }
469
+ }
470
+ break :has_beam false;
471
+ };
472
+ if (!has_beam) skip_pp = true;
473
+ }
474
+
462
475
  const can = skip_can or
463
476
  try canMove(battle, player, mslot, auto, skip_pp, .None, residual, options);
464
477
  if (!can) return null;
@@ -485,11 +498,22 @@ fn beforeMove(
485
498
 
486
499
  if (Status.is(stored.status, .SLP)) {
487
500
  const before = stored.status;
501
+ const slf = Status.is(stored.status, .EXT);
488
502
  // Even if the EXT bit is set this will still correctly modify the sleep duration
489
- stored.status -= 1;
503
+ if (options.calc.overridden(player, .sleep)) |obs| switch (obs) {
504
+ .started, .ended => stored.status = 0,
505
+ .continuing => if (Status.duration(stored.status) > 1) {
506
+ stored.status -= 1;
507
+ },
508
+ else => unreachable,
509
+ } else {
510
+ stored.status -= 1;
511
+ }
490
512
 
491
513
  const duration = Status.duration(stored.status);
492
- options.chance.durations(.sleep, player, observation(duration));
514
+ try options.chance.sleep(player, if (slf)
515
+ .None
516
+ else if (duration == 0) .ended else .continuing);
493
517
 
494
518
  if (duration == 0) {
495
519
  try log.curestatus(.{ ident, before, .Message });
@@ -527,8 +551,12 @@ fn beforeMove(
527
551
  }
528
552
 
529
553
  if (volatiles.disable_duration > 0) {
530
- volatiles.disable_duration -= 1;
531
- options.chance.durations(.disable, player, observation(volatiles.disable_duration));
554
+ volatiles.disable_duration =
555
+ decrement(.disable, player, options, volatiles.disable_duration);
556
+ try options.chance.disable(
557
+ player,
558
+ if (volatiles.disable_duration == 0) .ended else .continuing,
559
+ );
532
560
 
533
561
  if (volatiles.disable_duration == 0) {
534
562
  volatiles.disable_move = 0;
@@ -541,14 +569,24 @@ fn beforeMove(
541
569
  // This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
542
570
  // before the side had a selected a move - we simply need to assume this leads to a desync
543
571
  if (side.last_selected_move == .None) {
544
- assert(!showdown);
572
+ assert(!pkmn.options.mod);
545
573
  return .err;
546
574
  }
547
575
 
548
576
  if (volatiles.Confusion) {
549
577
  assert(volatiles.confusion > 0);
550
- volatiles.confusion -= 1;
551
- options.chance.durations(.confusion, player, observation(volatiles.confusion));
578
+ if (options.calc.overridden(player, .confusion)) |obs| switch (obs) {
579
+ .started, .ended => volatiles.confusion = 0,
580
+ .continuing => if (volatiles.confusion > 1) {
581
+ volatiles.confusion -= 1;
582
+ },
583
+ .overwritten => {},
584
+ else => unreachable,
585
+ } else {
586
+ volatiles.confusion -= 1;
587
+ }
588
+
589
+ try options.chance.confusion(player, if (volatiles.confusion == 0) .ended else .continuing);
552
590
 
553
591
  if (volatiles.confusion == 0) {
554
592
  volatiles.Confusion = false;
@@ -567,9 +605,8 @@ fn beforeMove(
567
605
  volatiles.Binding = false;
568
606
  volatiles.Invulnerable = false;
569
607
 
570
- options.chance.durations(.bide, player, .None);
571
- options.chance.durations(.thrashing, player, .None);
572
- options.chance.durations(.binding, player, .None);
608
+ options.chance.observe(.attacking, player, .None);
609
+ options.chance.observe(.binding, player, .None);
573
610
  {
574
611
  // This feels (and is) disgusting but the cartridge literally just overwrites
575
612
  // the opponent's defense with the user's defense and resets it after. As a
@@ -577,19 +614,14 @@ fn beforeMove(
577
614
  const def = foe.active.stats.def;
578
615
  foe.active.stats.def = active.stats.def;
579
616
  defer foe.active.stats.def = def;
580
- if (!calcDamage(battle, player, player.foe(), null, false)) return .err;
617
+ if (!calcDamage(battle, player, player.foe(), null, false, options)) {
618
+ return .err;
619
+ }
581
620
  }
582
- // Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
583
- // on the targeting behavior of the confused Pokémon's selected move which results
584
- // in the wrong behavior with respect to the Substitute + Confusion glitch
585
- const target = if (showdown and Move.get(side.last_selected_move).target == .Self)
586
- player
587
- else
588
- player.foe();
589
621
 
590
622
  const uncapped = battle.last_damage;
591
623
  // Skipping adjustDamage / randomizeDamage / checkHit
592
- _ = try applyDamage(battle, player, target, .Confusion, options);
624
+ _ = try applyDamage(battle, player, player.foe(), .Confusion, options);
593
625
  // Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
594
626
  if (showdown) battle.last_damage = uncapped;
595
627
 
@@ -607,9 +639,8 @@ fn beforeMove(
607
639
  volatiles.Charging = false;
608
640
  volatiles.Binding = false;
609
641
 
610
- options.chance.durations(.bide, player, .None);
611
- options.chance.durations(.thrashing, player, .None);
612
- options.chance.durations(.binding, player, .None);
642
+ options.chance.observe(.attacking, player, .None);
643
+ options.chance.observe(.binding, player, .None);
613
644
 
614
645
  // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
615
646
  try log.cant(.{ ident, .Paralysis });
@@ -628,8 +659,8 @@ fn beforeMove(
628
659
  volatiles.state +%= battle.last_damage;
629
660
  }
630
661
 
631
- volatiles.attacks -= 1;
632
- options.chance.durations(.bide, player, observation(volatiles.attacks));
662
+ volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
663
+ try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
633
664
 
634
665
  if (volatiles.attacks != 0) {
635
666
  try log.activate(.{ ident, .Bide });
@@ -665,14 +696,12 @@ fn beforeMove(
665
696
  if (volatiles.Thrashing) {
666
697
  try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
667
698
 
668
- volatiles.attacks -= 1;
669
- options.chance.durations(
670
- .thrashing,
671
- player,
672
- if (volatiles.attacks <= 1) .ended else .continuing,
673
- );
699
+ volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
700
+ try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
674
701
 
675
702
  if (volatiles.attacks == 0) {
703
+ const overwritten = volatiles.Confusion;
704
+
676
705
  volatiles.Thrashing = false;
677
706
  volatiles.Confusion = true;
678
707
  volatiles.confusion = Rolls.confusionDuration(battle, player, options);
@@ -682,6 +711,11 @@ fn beforeMove(
682
711
  // can be expected to recognize this pattern and pass the information only to the
683
712
  // correct player)
684
713
  try log.start(.{ battle.active(player), .ConfusionSilent });
714
+ options.chance.observe(.confusion, player, if (overwritten)
715
+ if (pkmn.options.overwrite) .overwritten else .continuing
716
+ else
717
+ .started);
718
+ if (!overwritten) options.calc.confusion(player);
685
719
  }
686
720
 
687
721
  // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
@@ -692,8 +726,8 @@ fn beforeMove(
692
726
  }
693
727
 
694
728
  if (volatiles.Binding) {
695
- volatiles.attacks -= 1;
696
- options.chance.durations(.binding, player, observation(volatiles.attacks));
729
+ volatiles.attacks = decrement(.binding, player, options, volatiles.attacks);
730
+ try options.chance.binding(player, if (volatiles.attacks == 0) .ended else .continuing);
697
731
 
698
732
  try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
699
733
  if (showdown or battle.last_damage != 0) {
@@ -747,9 +781,14 @@ fn canMove(
747
781
 
748
782
  if (move.effect == .Thrashing) {
749
783
  Effects.thrashing(battle, player, options);
750
- } else if (!showdown and move.effect == .Binding) {
751
- // Pokémon Showdown handles this after hit/miss checks and damage calculation
752
- try Effects.binding(battle, player, false, options);
784
+ } else if (move.effect == .Binding) {
785
+ // Pokémon Showdown handles this after hit/miss checks and damage calculation, though clears
786
+ // Recharging in onTryMove (required for the Hyper Beam automatic selection glitch)
787
+ if (showdown) {
788
+ battle.foe(player).active.volatiles.Recharging = false;
789
+ } else {
790
+ try Effects.binding(battle, player, false, options);
791
+ }
753
792
  }
754
793
 
755
794
  return true;
@@ -786,7 +825,7 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
786
825
  assert(active.move(mslot).pp == side.stored().move(mslot).pp);
787
826
  }
788
827
 
789
- fn incrementPP(side: *Side, mslot: u4) void {
828
+ fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
790
829
  var active = &side.active;
791
830
  const volatiles = &active.volatiles;
792
831
 
@@ -794,8 +833,11 @@ fn incrementPP(side: *Side, mslot: u4) void {
794
833
  // GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
795
834
  if (showdown and volatiles.Transform) return;
796
835
 
836
+ const n = options.calc.overridden(player, .pp) orelse 1;
837
+ if (volatiles.Transform and !options.chance.pp(player, n)) return;
838
+
797
839
  assert(mslot > 0 and mslot <= 4);
798
- side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% 1;
840
+ side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% n;
799
841
  }
800
842
 
801
843
  // Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
@@ -849,7 +891,7 @@ fn doMove(
849
891
 
850
892
  var late = showdown and move.effect != .Explode;
851
893
  const skip = status or immune;
852
- if ((!showdown or (!skip or counter)) and !miss) blk: {
894
+ if ((!showdown or (!skip or counter)) and !miss) damage: {
853
895
  if (showdown and move.effect.isMulti()) {
854
896
  try Effects.multiHit(battle, player, move, options);
855
897
  hits = side.active.volatiles.attacks;
@@ -871,8 +913,9 @@ fn doMove(
871
913
  ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
872
914
  // This can overflow after adjustDamage, but will still be sufficient to OHKO
873
915
  battle.last_damage = if (ohko) 65535 else 0;
874
- if (showdown) break :blk; // skip adjustDamage / randomizeDamage
875
- } else if (!calcDamage(battle, player, player.foe(), move, crit)) {
916
+ if (showdown) break :damage; // skip adjustDamage / randomizeDamage
917
+ } else if (!calcDamage(battle, player, player.foe(), move, crit, options)) {
918
+ if (!showdown) try options.chance.commit(player, .err);
876
919
  return @as(?Result, Result.Error);
877
920
  }
878
921
  if (battle.last_damage == 0) {
@@ -886,11 +929,12 @@ fn doMove(
886
929
  }
887
930
  }
888
931
 
932
+ var missed: bool = undefined;
889
933
  const zero = battle.last_damage == 0;
890
- miss = if (showdown or skip)
891
- miss
892
- else
893
- (!try moveHit(battle, player, move, &immune, &mist, options) or zero);
934
+ if (!showdown and !skip) {
935
+ missed = !try moveHit(battle, player, move, &immune, &mist, options);
936
+ miss = missed or zero;
937
+ }
894
938
 
895
939
  assert(showdown or miss or battle.last_damage > 0 or skip);
896
940
  assert((!showdown and miss) or !(ohko and immune));
@@ -917,11 +961,32 @@ fn doMove(
917
961
  try log.immune(.{ foe_ident, .OHKO });
918
962
  } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
919
963
  try log.immune(.{ foe_ident, .None });
964
+ if (!showdown) {
965
+ if (move.effect == .Binding) {
966
+ // Rather obnoxiously, the cartridge sets the Binding volatile in canMove and
967
+ // then simply clears it later on if the move happens to miss in moveHit.
968
+ // However, moveHit can return true and we'll still end up in the outer miss
969
+ // handling block because attacking into an immune Pokemon does zero damage
970
+ // (considered missing)
971
+ assert(zero);
972
+ // Thus we still need to actually save the hit result (but not the crit roll).
973
+ // Using .miss for this commit would accomplish what we want, but is somewhat
974
+ // misleading as its entirely possible the move actually did hit (and the target
975
+ // is now trapped) so we use the .binding value instead which is effectively
976
+ // identical, just doesn't claim to know about the actual results of the hit
977
+ // roll)
978
+ try options.chance.commit(player, .binding);
979
+ } else {
980
+ // The .glitch option allows for conditionally committing if we are in a
981
+ // division-by-zero freeze alternative timeline scenario
982
+ try options.chance.commit(player, .glitch);
983
+ }
984
+ }
920
985
  } else if (mist) {
921
986
  if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
922
987
  try log.fail(.{ foe_ident, .None });
923
988
  } else {
924
- if (!showdown and !zero) try options.chance.commit(player, .miss);
989
+ if (!showdown) try options.chance.commit(player, if (zero and !missed) .hit else .miss);
925
990
  try log.lastmiss(.{});
926
991
  try log.miss(.{battle.active(player)});
927
992
  }
@@ -983,22 +1048,20 @@ fn doMove(
983
1048
  try Effects.poison(battle, player, Move.get(.PoisonSting), options);
984
1049
  }
985
1050
  try log.hitcount(.{ battle.active(player.foe()), hit });
986
- } else if (showdown) {
1051
+ } else if (showdown and move.effect == .Binding) {
987
1052
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
988
- if (move.effect == .Binding) {
989
- try Effects.binding(battle, player, rewrap, options);
990
- if (immune) {
991
- battle.last_damage = 0;
992
- assert(foe.stored().hp > 0);
993
- // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
994
- if (sub) {
995
- try log.activate(.{ battle.active(player.foe()), .Substitute });
996
- } else {
997
- try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
998
- try buildRage(battle, player.foe(), options);
999
- }
1000
- return null;
1053
+ try Effects.binding(battle, player, rewrap, options);
1054
+ if (immune) {
1055
+ battle.last_damage = 0;
1056
+ assert(foe.stored().hp > 0);
1057
+ // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
1058
+ if (sub) {
1059
+ try log.activate(.{ battle.active(player.foe()), .Substitute });
1060
+ } else {
1061
+ try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
1062
+ try buildRage(battle, player.foe(), options);
1001
1063
  }
1064
+ return null;
1002
1065
  }
1003
1066
  }
1004
1067
 
@@ -1058,6 +1121,7 @@ fn calcDamage(
1058
1121
  target_player: Player,
1059
1122
  m: ?Move.Data,
1060
1123
  crit: bool,
1124
+ options: anytype,
1061
1125
  ) bool {
1062
1126
  // Confusion (indicated when m == null) just needs a 40 BP physical move
1063
1127
  const cfz = m == null;
@@ -1069,28 +1133,8 @@ fn calcDamage(
1069
1133
 
1070
1134
  const special = move.type.special();
1071
1135
 
1072
- // zig fmt: off
1073
- var atk: u32 =
1074
- if (crit)
1075
- if (special) side.stored().stats.spc
1076
- else side.stored().stats.atk
1077
- else
1078
- if (special) side.active.stats.spc
1079
- else side.active.stats.atk;
1080
- var def: u32 =
1081
- if (crit)
1082
- if (special) target.stored().stats.spc
1083
- else target.stored().stats.def
1084
- else
1085
- // GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
1086
- if (special)
1087
- target.active.stats.spc *
1088
- @as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
1089
- // Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
1090
- else
1091
- target.active.stats.def *
1092
- @as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
1093
- // zig fmt: on
1136
+ var atk = attack(side, crit, special);
1137
+ var def = defense(target, crit, special, cfz);
1094
1138
 
1095
1139
  // Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
1096
1140
  // will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
@@ -1098,7 +1142,7 @@ fn calcDamage(
1098
1142
  if ((!showdown or !cfz) and (atk > 255 or def > 255)) {
1099
1143
  atk = @max((atk / 4) & 255, 1);
1100
1144
  // GLITCH: not adjusted to be a min of 1 on cartridge (can lead to division-by-zero freeze)
1101
- def = @max((def / 4) & 255, if (showdown) 1 else 0);
1145
+ def = @max((def / 4) & 255, if (pkmn.options.mod) 1 else 0);
1102
1146
  }
1103
1147
 
1104
1148
  const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
@@ -1115,11 +1159,41 @@ fn calcDamage(
1115
1159
  d = @min(997, d);
1116
1160
  d += 2;
1117
1161
 
1162
+ // Whether or not a critical hit occurred can change whether a division-by-zero freeze is
1163
+ // encountered so we need to make sure that in the event of a miss we commit the status of the
1164
+ // crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
1165
+ // that results in counter intuitive probabilities in the common case
1166
+ if (pkmn.options.chance and !pkmn.options.mod and move.effect != .Explode) {
1167
+ atk = attack(side, !crit, special);
1168
+ def = defense(target, !crit, special, false);
1169
+ if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
1170
+ }
1171
+
1118
1172
  battle.last_damage = @intCast(d);
1119
1173
 
1120
1174
  return true;
1121
1175
  }
1122
1176
 
1177
+ fn attack(side: *Side, crit: bool, special: bool) u32 {
1178
+ return if (crit)
1179
+ if (special) side.stored().stats.spc else side.stored().stats.atk
1180
+ else if (special) side.active.stats.spc else side.active.stats.atk;
1181
+ }
1182
+
1183
+ fn defense(target: *Side, crit: bool, special: bool, cfz: bool) u32 {
1184
+ return if (crit)
1185
+ if (special) target.stored().stats.spc else target.stored().stats.def
1186
+ else
1187
+ // GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
1188
+ if (special)
1189
+ target.active.stats.spc *
1190
+ @as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
1191
+ else
1192
+ // Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
1193
+ target.active.stats.def *
1194
+ @as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
1195
+ }
1196
+
1123
1197
  fn adjustDamage(battle: anytype, player: Player) u16 {
1124
1198
  const side = battle.side(player);
1125
1199
  const foe = battle.foe(player);
@@ -1174,13 +1248,13 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1174
1248
  .Psywave => power: {
1175
1249
  const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
1176
1250
  // GLITCH: Psywave infinite glitch loop
1177
- if (!showdown and max <= 1) return Result.Error;
1251
+ if (!pkmn.options.mod and max <= 1) return Result.Error;
1178
1252
  break :power try Rolls.psywave(battle, player, max, options);
1179
1253
  },
1180
1254
  else => unreachable,
1181
1255
  };
1182
1256
 
1183
- if (battle.last_damage == 0) return if (showdown) null else Result.Error;
1257
+ if (battle.last_damage == 0) return if (pkmn.options.mod) null else Result.Error;
1184
1258
 
1185
1259
  const sub = showdown and foe.active.volatiles.Substitute;
1186
1260
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -1217,12 +1291,16 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1217
1291
 
1218
1292
  if (!used or !selected) {
1219
1293
  // GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
1220
- if (!showdown) return Result.Error;
1294
+ if (!pkmn.options.mod) return Result.Error;
1221
1295
  try options.log.fail(.{ battle.active(player), .None });
1222
1296
  return null;
1223
1297
  }
1224
1298
 
1225
- battle.last_damage *|= 2;
1299
+ // NOTE: llvm/llvm-project#58557
1300
+ battle.last_damage = if (comptime builtin.target.isWasm())
1301
+ @max(battle.last_damage *% 2, std.math.maxInt(u16))
1302
+ else
1303
+ battle.last_damage *| 2;
1226
1304
 
1227
1305
  // Pokémon Showdown calls moveHit before Counter
1228
1306
  if (!showdown and !try checkHit(battle, player, move, options)) return null;
@@ -1254,7 +1332,7 @@ fn applyDamage(
1254
1332
  subbed.active.volatiles.Substitute = false;
1255
1333
  // battle.last_damage is not updated with the amount of HP the Substitute had
1256
1334
  try options.log.end(.{ battle.active(sub_player), .Substitute });
1257
- options.calc.capped(sub_player);
1335
+ if (reason != .Confusion) options.calc.capped(sub_player);
1258
1336
  return true;
1259
1337
  } else {
1260
1338
  // Safe to truncate since less than subbed.volatiles.substitute which is a u8
@@ -1288,7 +1366,7 @@ fn mirrorMove(
1288
1366
  try options.log.fail(.{ battle.active(player), .None });
1289
1367
  return null;
1290
1368
  } else if (!showdown or foe.last_used_move != .Struggle) {
1291
- incrementPP(side, mslot);
1369
+ incrementPP(side, player, mslot, options);
1292
1370
  }
1293
1371
 
1294
1372
  if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
@@ -1309,7 +1387,7 @@ fn metronome(
1309
1387
  var side = battle.side(player);
1310
1388
 
1311
1389
  side.last_selected_move = try Rolls.metronome(battle, player, options);
1312
- incrementPP(side, mslot);
1390
+ incrementPP(side, player, mslot, options);
1313
1391
 
1314
1392
  if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
1315
1393
  return null;
@@ -1410,7 +1488,10 @@ fn moveHit(
1410
1488
 
1411
1489
  if (!miss) return true;
1412
1490
  if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
1491
+
1413
1492
  side.active.volatiles.Binding = false;
1493
+ options.chance.observe(.binding, player, .None);
1494
+
1414
1495
  return false;
1415
1496
  }
1416
1497
 
@@ -1461,7 +1542,7 @@ fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytyp
1461
1542
  assert(!foe_volatiles.MultiHit);
1462
1543
  if (foe_volatiles.Bide) {
1463
1544
  assert(!foe_volatiles.Thrashing and !foe_volatiles.Rage);
1464
- foe_volatiles.state = if (showdown) 0 else foe_volatiles.state & 255;
1545
+ foe_volatiles.state = if (pkmn.options.mod) 0 else foe_volatiles.state & 255;
1465
1546
  if (foe_volatiles.state != 0) return Result.Error;
1466
1547
  }
1467
1548
 
@@ -1552,12 +1633,12 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
1552
1633
 
1553
1634
  battle.turn += 1;
1554
1635
 
1555
- if (showdown and pkmn.options.ebc and checkEBC(battle)) {
1636
+ if (pkmn.options.mod and pkmn.options.ebc and checkEBC(battle)) {
1556
1637
  try options.log.tie(.{});
1557
1638
  return Result.Tie;
1558
1639
  }
1559
1640
 
1560
- if (showdown and battle.turn >= 1000) {
1641
+ if (pkmn.options.mod and battle.turn >= 1000) {
1561
1642
  try options.log.tie(.{});
1562
1643
  return Result.Tie;
1563
1644
  } else if (battle.turn >= 65535) {
@@ -1737,7 +1818,7 @@ pub const Effects = struct {
1737
1818
 
1738
1819
  side.active.volatiles.state = 0;
1739
1820
  side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
1740
- options.chance.durations(.bide, player, .started);
1821
+ options.chance.observe(.attacking, player, .started);
1741
1822
 
1742
1823
  try options.log.start(.{ battle.active(player), .Bide });
1743
1824
  }
@@ -1805,7 +1886,8 @@ pub const Effects = struct {
1805
1886
  foe.active.volatiles.Confusion = true;
1806
1887
 
1807
1888
  foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
1808
- options.chance.durations(.confusion, player.foe(), .started);
1889
+ options.chance.observe(.confusion, player.foe(), .started);
1890
+ options.calc.confusion(player.foe());
1809
1891
 
1810
1892
  try options.log.start(.{ battle.active(player.foe()), .Confusion });
1811
1893
  }
@@ -1851,19 +1933,18 @@ pub const Effects = struct {
1851
1933
 
1852
1934
  // Technically this is still considered simply a "miss" on the cartridge,
1853
1935
  // but diverging from Pokémon Showdown here would mostly just be pedantic
1854
- if (n == 0) {
1936
+ if (n == 0 or (pkmn.options.mod and err)) {
1855
1937
  try options.log.fail(.{ foe_ident, .None });
1856
1938
  return null;
1857
1939
  } else if (err) {
1858
1940
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
1859
- assert(!showdown);
1860
1941
  return Result.Error;
1861
1942
  }
1862
1943
 
1863
1944
  volatiles.disable_move =
1864
1945
  @intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
1865
1946
  volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
1866
- options.chance.durations(.disable, player.foe(), .started);
1947
+ options.chance.observe(.disable, player.foe(), .started);
1867
1948
 
1868
1949
  const id = foe.active.move(volatiles.disable_move).id;
1869
1950
  try options.log.start(.{ foe_ident, .Disable, id });
@@ -1934,7 +2015,7 @@ pub const Effects = struct {
1934
2015
  if (foe.active.types.includes(move.type)) return;
1935
2016
  if (!try Rolls.secondaryChance(battle, player, true, options)) return;
1936
2017
  // Freeze Clause Mod
1937
- if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
2018
+ if (pkmn.options.mod) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1938
2019
 
1939
2020
  foe_stored.status = Status.init(.FRZ);
1940
2021
  // GLITCH: Hyper Beam recharging status is not cleared
@@ -1970,7 +2051,7 @@ pub const Effects = struct {
1970
2051
  if (p != player and Status.any(s.stored().status)) {
1971
2052
  try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
1972
2053
  s.stored().status = 0;
1973
- options.chance.durations(.sleep, p, .None);
2054
+ options.chance.observe(.sleep, p, .None);
1974
2055
  } else if (showdown and s.stored().status == Status.TOX) {
1975
2056
  s.stored().status = Status.init(.PSN);
1976
2057
  try log.status(.{ battle.active(p), s.stored().status, .None });
@@ -1981,7 +2062,7 @@ pub const Effects = struct {
1981
2062
  if (Status.any(foe_stored.status)) {
1982
2063
  if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
1983
2064
  foe.last_selected_move = .SKIP_TURN;
1984
- options.chance.durations(.sleep, player.foe(), .None);
2065
+ options.chance.observe(.sleep, player.foe(), .None);
1985
2066
  }
1986
2067
  try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
1987
2068
  foe_stored.status = 0;
@@ -2275,7 +2356,7 @@ pub const Effects = struct {
2275
2356
  }
2276
2357
 
2277
2358
  // Sleep Clause Mod
2278
- if (showdown) {
2359
+ if (pkmn.options.mod) {
2279
2360
  for (foe.pokemon) |p| {
2280
2361
  if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
2281
2362
  }
@@ -2283,7 +2364,7 @@ pub const Effects = struct {
2283
2364
  foe.active.volatiles.Recharging = false;
2284
2365
 
2285
2366
  foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2286
- options.chance.durations(.sleep, player.foe(), .started);
2367
+ options.chance.observe(.sleep, player.foe(), .started);
2287
2368
 
2288
2369
  const last = battle.side(player).last_selected_move;
2289
2370
  try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
@@ -2342,7 +2423,7 @@ pub const Effects = struct {
2342
2423
  volatiles.Thrashing = true;
2343
2424
  volatiles.attacks = Rolls.attackingDuration(battle, player, options);
2344
2425
 
2345
- options.chance.durations(.thrashing, player, .started);
2426
+ options.chance.observe(.attacking, player, .started);
2346
2427
  }
2347
2428
 
2348
2429
  fn transform(battle: anytype, player: Player, options: anytype) !void {
@@ -2381,14 +2462,14 @@ pub const Effects = struct {
2381
2462
  if (side.active.volatiles.Binding) return;
2382
2463
  side.active.volatiles.Binding = true;
2383
2464
  // GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
2384
- // (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
2465
+ // (Pokémon Showdown clears Recharging in onTryMove instead)
2385
2466
  if (!showdown) {
2386
2467
  foe.active.volatiles.Recharging = false;
2387
2468
  } else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
2388
2469
 
2389
2470
  side.active.volatiles.attacks =
2390
2471
  try Rolls.distribution(battle, .Binding, player, options) - 1;
2391
- options.chance.durations(.binding, player, .started);
2472
+ options.chance.observe(.binding, player, .started);
2392
2473
  }
2393
2474
 
2394
2475
  fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2627,13 +2708,13 @@ fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
2627
2708
  if (volatiles.disable_move != 0) {
2628
2709
  volatiles.disable_move = 0;
2629
2710
  volatiles.disable_duration = 0;
2630
- options.chance.durations(.disable, who, .None);
2711
+ options.chance.observe(.disable, who, .None);
2631
2712
  try log.end(.{ ident, .DisableSilent });
2632
2713
  }
2633
2714
  if (volatiles.Confusion) {
2634
2715
  // volatiles.confusion is left unchanged
2635
2716
  volatiles.Confusion = false;
2636
- options.chance.durations(.confusion, who, .None);
2717
+ options.chance.observe(.confusion, who, .None);
2637
2718
  try log.end(.{ ident, .ConfusionSilent });
2638
2719
  }
2639
2720
  if (volatiles.Mist) {
@@ -2668,8 +2749,17 @@ fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
2668
2749
  }
2669
2750
  }
2670
2751
 
2671
- fn observation(n: anytype) optional.Optional(chance.Observation) {
2672
- return if (n == 0) .ended else .continuing;
2752
+ fn decrement(
2753
+ comptime field: chance.Action.Field,
2754
+ player: Player,
2755
+ options: anytype,
2756
+ n: anytype,
2757
+ ) @TypeOf(n) {
2758
+ return if (options.calc.overridden(player, field)) |obs| switch (obs) {
2759
+ .started, .ended => 0,
2760
+ .continuing => if (n > 1) n - 1 else n,
2761
+ else => unreachable,
2762
+ } else n - 1;
2673
2763
  }
2674
2764
 
2675
2765
  pub const Rolls = struct {
@@ -2869,9 +2959,10 @@ pub const Rolls = struct {
2869
2959
  }
2870
2960
 
2871
2961
  fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2872
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2873
- @intCast(val)
2874
- else if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
2962
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
2963
+ if (val >= 1 and val <= 7) @as(u3, @intCast(val)) else null
2964
+ else
2965
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
2875
2966
  battle.rng.sleepDuration(player)
2876
2967
  else if (showdown)
2877
2968
  battle.rng.range(u3, 1, 8)
@@ -2888,9 +2979,10 @@ pub const Rolls = struct {
2888
2979
  }
2889
2980
 
2890
2981
  fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2891
- const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
2892
- val
2893
- else if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
2982
+ const duration: u4 = (if (options.calc.overridden(player, .duration)) |val|
2983
+ if (val >= 1 and val <= 8) val else null
2984
+ else
2985
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
2894
2986
  battle.rng.disableDuration(player)
2895
2987
  else if (showdown)
2896
2988
  battle.rng.range(u4, 1, 9)
@@ -2903,9 +2995,10 @@ pub const Rolls = struct {
2903
2995
  }
2904
2996
 
2905
2997
  fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
2906
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2907
- @intCast(val)
2908
- else if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
2998
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
2999
+ if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
3000
+ else
3001
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
2909
3002
  battle.rng.confusionDuration(player)
2910
3003
  else if (showdown)
2911
3004
  battle.rng.range(u3, 2, 6)
@@ -2918,9 +3011,10 @@ pub const Rolls = struct {
2918
3011
  }
2919
3012
 
2920
3013
  fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2921
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2922
- @intCast(val)
2923
- else if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
3014
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
3015
+ if (val >= 2 and val <= 3) @as(u3, @intCast(val)) else null
3016
+ else
3017
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
2924
3018
  battle.rng.attackingDuration(player)
2925
3019
  else if (showdown)
2926
3020
  battle.rng.range(u3, 2, 4)
@@ -2941,9 +3035,10 @@ pub const Rolls = struct {
2941
3035
  options: anytype,
2942
3036
  ) !u3 {
2943
3037
  const roll = if (effect == .Binding) .duration else .multi_hit;
2944
- const n: u3 = if (options.calc.overridden(player, roll)) |val|
2945
- @intCast(val)
2946
- else if (@hasDecl(@TypeOf(battle.rng), "distribution"))
3038
+ const n: u3 = (if (options.calc.overridden(player, roll)) |val|
3039
+ if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
3040
+ else
3041
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "distribution"))
2947
3042
  battle.rng.distribution(player)
2948
3043
  else if (showdown)
2949
3044
  DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
@@ -2969,15 +3064,13 @@ pub const Rolls = struct {
2969
3064
  options: anytype,
2970
3065
  ) !u4 {
2971
3066
  // TODO: consider throwing error instead of rerolling?
2972
- const overridden = if (options.calc.overridden(player, .move_slot)) |val|
3067
+ const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
2973
3068
  if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
2974
3069
  val
2975
3070
  else
2976
3071
  null
2977
3072
  else
2978
- null;
2979
-
2980
- const slot: u4 = overridden orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
3073
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
2981
3074
  battle.rng.moveSlot(player, moves, check_pp)
2982
3075
  else slot: {
2983
3076
  if (showdown) {
@@ -3135,7 +3228,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3135
3228
  // Struggle (Pokémon Showdown would use 'move 1' here)
3136
3229
  if (n == before) {
3137
3230
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
3138
- if (!showdown) {
3231
+ if (!pkmn.options.mod) {
3139
3232
  while (slot <= 4) : (slot += 1) {
3140
3233
  if (active.moves[slot - 1].pp != 0) return n;
3141
3234
  }