@pkmn/engine 0.1.0-dev.5570b5c5 → 0.1.0-dev.565de616
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.
- package/README.md +12 -8
- package/build/pkg/addon/browser.d.ts +3 -0
- package/build/pkg/addon/browser.js +44 -0
- package/build/pkg/addon/browser.js.map +1 -0
- package/build/pkg/addon/node.d.ts +3 -0
- package/build/pkg/addon/node.js +97 -0
- package/build/pkg/addon/node.js.map +1 -0
- package/build/pkg/addon/wasm.d.ts +2 -0
- package/build/pkg/addon/wasm.js +21 -0
- package/build/pkg/addon/wasm.js.map +1 -0
- package/build/pkg/addon.d.ts +22 -0
- package/build/pkg/addon.js +29 -51
- package/build/pkg/addon.js.map +1 -1
- package/build/pkg/data/layout.json +15 -2
- package/build/pkg/data/protocol.json +23 -9
- package/build/pkg/data.d.ts +23 -1
- package/build/pkg/data.js.map +1 -1
- package/build/pkg/gen1.d.ts +10 -13
- package/build/pkg/gen1.js +1 -1
- package/build/pkg/gen1.js.map +1 -1
- package/build/pkg/index.d.ts +53 -12
- package/build/pkg/index.js +15 -10
- package/build/pkg/index.js.map +1 -1
- package/build/pkg/protocol.d.ts +20 -1
- package/build/pkg/protocol.js +105 -103
- package/build/pkg/protocol.js.map +1 -1
- package/build/tools/debug.js +35 -68
- package/build/tools/debug.js.map +1 -1
- package/build/tools/display/debug.jsx +141 -0
- package/build/tools/display/debug.jsx.map +1 -0
- package/build/tools/display/index.js +141 -0
- package/build/tools/display/index.js.map +1 -0
- package/build/tools/display/ui.jsx +349 -0
- package/build/tools/display/ui.jsx.map +1 -0
- package/build/tools/display/util.js +18 -0
- package/build/tools/display/util.js.map +1 -0
- package/build.zig +230 -74
- package/package.json +51 -31
- package/src/bin/install-pkmn-engine +8 -6
- package/src/bin/pkmn-debug +1 -1
- package/src/lib/common/array.zig +77 -0
- package/src/lib/common/data.zig +8 -8
- package/src/lib/common/debug.zig +39 -8
- package/src/lib/common/js.zig +4 -2
- package/src/lib/common/optional.zig +13 -9
- package/src/lib/common/options.zig +6 -0
- package/src/lib/common/protocol.zig +45 -44
- package/src/lib/common/rational.zig +121 -29
- package/src/lib/common/rng.zig +5 -7
- package/src/lib/common/util.zig +50 -1
- package/src/lib/gen1/calc.zig +683 -156
- package/src/lib/gen1/chance.zig +415 -291
- package/src/lib/gen1/data/moves.zig +2 -4
- package/src/lib/gen1/data/species.zig +1 -3
- package/src/lib/gen1/data/types.zig +1 -2
- package/src/lib/gen1/data.zig +38 -41
- package/src/lib/gen1/helpers.zig +92 -548
- package/src/lib/gen1/mechanics.zig +325 -235
- package/src/lib/gen2/calc.zig +37 -27
- package/src/lib/gen2/chance.zig +90 -503
- package/src/lib/gen2/data/items.zig +1 -3
- package/src/lib/gen2/data/moves.zig +2 -4
- package/src/lib/gen2/data/species.zig +1 -3
- package/src/lib/gen2/data/types.zig +1 -2
- package/src/lib/gen2/data.zig +26 -29
- package/src/lib/gen2/generated.zig +18 -23
- package/src/lib/gen2/helpers.zig +46 -25
- package/src/lib/gen2/mechanics.zig +69 -107
- package/src/lib/node.zig +2 -4
- package/src/lib/pkmn.zig +14 -16
- package/src/lib/wasm.zig +1 -2
- package/src/tools/display/dom.ts +96 -0
- package/src/tools/display/ui.css +354 -0
- package/build/test/display.js +0 -400
- package/build/test/display.js.map +0 -1
- package/src/test/display/pkmn.html.tmpl +0 -368
|
@@ -1,37 +1,28 @@
|
|
|
1
|
-
const
|
|
2
|
-
|
|
3
|
-
const pkmn = @import("../pkmn.zig");
|
|
1
|
+
const DEBUG = @import("../common/debug.zig").print;
|
|
4
2
|
|
|
3
|
+
const chance = @import("chance.zig");
|
|
5
4
|
const common = @import("../common/data.zig");
|
|
6
|
-
const
|
|
5
|
+
const data = @import("data.zig");
|
|
6
|
+
const pkmn = @import("../pkmn.zig");
|
|
7
7
|
const protocol = @import("../common/protocol.zig");
|
|
8
8
|
const rng = @import("../common/rng.zig");
|
|
9
|
-
|
|
10
|
-
const data = @import("data.zig");
|
|
9
|
+
const std = @import("std");
|
|
11
10
|
|
|
12
11
|
const assert = std.debug.assert;
|
|
13
|
-
|
|
14
|
-
const expectEqual = std.testing.expectEqual;
|
|
15
|
-
|
|
16
|
-
const showdown = pkmn.options.showdown;
|
|
17
|
-
|
|
18
|
-
const Choice = common.Choice;
|
|
19
|
-
const ID = common.ID;
|
|
20
|
-
const Player = common.Player;
|
|
21
|
-
const Result = common.Result;
|
|
22
|
-
|
|
23
12
|
const Boost = protocol.Boost;
|
|
13
|
+
const Choice = common.Choice;
|
|
24
14
|
const Damage = protocol.Damage;
|
|
15
|
+
const Effectiveness = data.Effectiveness;
|
|
16
|
+
const expectEqual = std.testing.expectEqual;
|
|
25
17
|
const Fail = protocol.Fail;
|
|
26
|
-
const Heal = protocol.Heal;
|
|
27
|
-
|
|
28
18
|
const Gen12 = rng.Gen12;
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
const Effectiveness = data.Effectiveness;
|
|
19
|
+
const Heal = protocol.Heal;
|
|
20
|
+
const ID = common.ID;
|
|
32
21
|
const Move = data.Move;
|
|
33
22
|
const MoveSlot = data.MoveSlot;
|
|
34
|
-
const
|
|
23
|
+
const Player = common.Player;
|
|
24
|
+
const Result = common.Result;
|
|
25
|
+
const showdown = pkmn.options.showdown;
|
|
35
26
|
const Side = data.Side;
|
|
36
27
|
const Species = data.Species;
|
|
37
28
|
const Stats = data.Stats;
|
|
@@ -166,7 +157,7 @@ fn selectMove(
|
|
|
166
157
|
// to satisfy the conditions of the Desync Clause Mod. However, because Binding is still
|
|
167
158
|
// set the selected move will not actually be used, it will just be reported as having
|
|
168
159
|
// been used (this differs from how Pokémon Showdown works, but its impossible to
|
|
169
|
-
// replicate the incorrect behavior with the correct mechanisms)
|
|
160
|
+
// replicate the incorrect behavior with the correct mechanisms)
|
|
170
161
|
saveMove(battle, player, choice);
|
|
171
162
|
} else {
|
|
172
163
|
assert(choice.data == 0);
|
|
@@ -174,7 +165,10 @@ fn selectMove(
|
|
|
174
165
|
if (foe_choice.type == .Switch) {
|
|
175
166
|
const last = side.active.move(battle.lastMove(player).index);
|
|
176
167
|
if (last.id == .Metronome) side.last_selected_move = last.id;
|
|
177
|
-
if (last.id == .MirrorMove)
|
|
168
|
+
if (last.id == .MirrorMove) {
|
|
169
|
+
if (!pkmn.options.mod) return Result.Error;
|
|
170
|
+
side.last_selected_move = last.id;
|
|
171
|
+
}
|
|
178
172
|
}
|
|
179
173
|
}
|
|
180
174
|
return null;
|
|
@@ -253,11 +247,11 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: a
|
|
|
253
247
|
active.moves = incoming.moves;
|
|
254
248
|
|
|
255
249
|
statusModify(incoming.status, &active.stats);
|
|
250
|
+
options.chance.switched(player, slot);
|
|
256
251
|
|
|
257
252
|
foe.active.volatiles.Binding = false;
|
|
258
253
|
|
|
259
254
|
try options.log.switched(.{ battle.active(player), incoming });
|
|
260
|
-
options.chance.switched(player, side.order[0], out);
|
|
261
255
|
|
|
262
256
|
if (showdown and incoming.status == Status.TOX) {
|
|
263
257
|
incoming.status = Status.init(.PSN);
|
|
@@ -347,13 +341,13 @@ fn doTurn(
|
|
|
347
341
|
)) |r| return r;
|
|
348
342
|
if (!replace) {
|
|
349
343
|
if (player_choice.type != .Switch) {
|
|
350
|
-
if (try checkFaint(battle, foe_player, options)) |r| return r;
|
|
344
|
+
if (try checkFaint(battle, foe_player, true, options)) |r| return r;
|
|
351
345
|
}
|
|
352
346
|
if (residual) try handleResidual(battle, player, options);
|
|
353
|
-
if (try checkFaint(battle, player, options)) |r| return r;
|
|
347
|
+
if (try checkFaint(battle, player, false, options)) |r| return r;
|
|
354
348
|
} else if (foe_choice.type == .Pass) return null;
|
|
355
349
|
|
|
356
|
-
options.chance.clearPending();
|
|
350
|
+
if (!showdown) options.chance.clearPending();
|
|
357
351
|
|
|
358
352
|
residual = true;
|
|
359
353
|
replace = battle.side(foe_player).stored().hp == 0;
|
|
@@ -369,10 +363,10 @@ fn doTurn(
|
|
|
369
363
|
)) |r| return r;
|
|
370
364
|
if (!replace) {
|
|
371
365
|
if (!calc and foe_choice.type != .Switch) {
|
|
372
|
-
if (try checkFaint(battle, player, options)) |r| return r;
|
|
366
|
+
if (try checkFaint(battle, player, true, options)) |r| return r;
|
|
373
367
|
}
|
|
374
368
|
if (residual) try handleResidual(battle, foe_player, options);
|
|
375
|
-
if (try checkFaint(battle, foe_player, options)) |r| return r;
|
|
369
|
+
if (try checkFaint(battle, foe_player, false, options)) |r| return r;
|
|
376
370
|
}
|
|
377
371
|
|
|
378
372
|
// Flinch is bugged on Pokémon Showdown because it gets implemented with a duration which causes
|
|
@@ -425,13 +419,13 @@ fn executeMove(
|
|
|
425
419
|
mslot = @intCast(battle.lastMove(player).index);
|
|
426
420
|
const stored = side.stored();
|
|
427
421
|
// GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
|
|
428
|
-
auto = isForced(&side.active) or
|
|
422
|
+
auto = pkmn.options.mod or isForced(&side.active) or
|
|
429
423
|
side.active.volatiles.Binding or side.active.volatiles.Bide or
|
|
430
424
|
side.last_selected_move == .HyperBeam or
|
|
431
425
|
Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
|
|
432
426
|
// If it wasn't Hyper Beam or the continuation of a move effect then we must have just
|
|
433
427
|
// 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
|
|
428
|
+
// index 1 and the current Pokémon has the same move in its first slot
|
|
435
429
|
if (!auto) {
|
|
436
430
|
// side.active.moves(slot) is safe to check even though the slot in question might not
|
|
437
431
|
// technically be from this Pokémon because it must be exactly 1 to not desync and
|
|
@@ -458,6 +452,25 @@ fn executeMove(
|
|
|
458
452
|
.err => return @as(?Result, Result.Error),
|
|
459
453
|
}
|
|
460
454
|
|
|
455
|
+
// Pokémon Showdown incorrectly implements PP deduction when handling the Hyper Beam automatic
|
|
456
|
+
// selection glitch - if the move was proced due to Metronome / Mirror Move it either subtracts
|
|
457
|
+
// from the Hyper Beam slot if present or skips PP deduction entirely...
|
|
458
|
+
if (showdown and !skip_pp and side.last_selected_move == .HyperBeam and
|
|
459
|
+
side.last_selected_move != side.active.move(battle.lastMove(player).index).id)
|
|
460
|
+
{
|
|
461
|
+
assert(mslot == 1);
|
|
462
|
+
const has_beam = has_beam: {
|
|
463
|
+
for (side.active.moves, 0..) |m, i| {
|
|
464
|
+
if (m.id == .HyperBeam) {
|
|
465
|
+
mslot = @intCast(i + 1);
|
|
466
|
+
break :has_beam true;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
break :has_beam false;
|
|
470
|
+
};
|
|
471
|
+
if (!has_beam) skip_pp = true;
|
|
472
|
+
}
|
|
473
|
+
|
|
461
474
|
const can = skip_can or
|
|
462
475
|
try canMove(battle, player, mslot, auto, skip_pp, .None, residual, options);
|
|
463
476
|
if (!can) return null;
|
|
@@ -484,17 +497,22 @@ fn beforeMove(
|
|
|
484
497
|
|
|
485
498
|
if (Status.is(stored.status, .SLP)) {
|
|
486
499
|
const before = stored.status;
|
|
500
|
+
const slf = Status.is(stored.status, .EXT);
|
|
487
501
|
// Even if the EXT bit is set this will still correctly modify the sleep duration
|
|
488
|
-
if (options.calc.
|
|
489
|
-
|
|
502
|
+
if (options.calc.overridden(player, .sleep)) |obs| switch (obs) {
|
|
503
|
+
.started, .ended => stored.status = 0,
|
|
504
|
+
.continuing => if (Status.duration(stored.status) > 1) {
|
|
505
|
+
stored.status -= 1;
|
|
506
|
+
},
|
|
507
|
+
else => unreachable,
|
|
490
508
|
} else {
|
|
491
509
|
stored.status -= 1;
|
|
492
510
|
}
|
|
493
511
|
|
|
494
512
|
const duration = Status.duration(stored.status);
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
513
|
+
try options.chance.sleep(player, if (slf)
|
|
514
|
+
.None
|
|
515
|
+
else if (duration == 0) .ended else .continuing);
|
|
498
516
|
|
|
499
517
|
if (duration == 0) {
|
|
500
518
|
try log.curestatus(.{ ident, before, .Message });
|
|
@@ -532,12 +550,12 @@ fn beforeMove(
|
|
|
532
550
|
}
|
|
533
551
|
|
|
534
552
|
if (volatiles.disable_duration > 0) {
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
553
|
+
volatiles.disable_duration =
|
|
554
|
+
decrement(.disable, player, options, volatiles.disable_duration);
|
|
555
|
+
try options.chance.disable(
|
|
556
|
+
player,
|
|
557
|
+
if (volatiles.disable_duration == 0) .ended else .continuing,
|
|
558
|
+
);
|
|
541
559
|
|
|
542
560
|
if (volatiles.disable_duration == 0) {
|
|
543
561
|
volatiles.disable_move = 0;
|
|
@@ -550,19 +568,24 @@ fn beforeMove(
|
|
|
550
568
|
// This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
|
|
551
569
|
// before the side had a selected a move - we simply need to assume this leads to a desync
|
|
552
570
|
if (side.last_selected_move == .None) {
|
|
553
|
-
assert(!
|
|
571
|
+
assert(!pkmn.options.mod);
|
|
554
572
|
return .err;
|
|
555
573
|
}
|
|
556
574
|
|
|
557
575
|
if (volatiles.Confusion) {
|
|
558
576
|
assert(volatiles.confusion > 0);
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
if (
|
|
577
|
+
if (options.calc.overridden(player, .confusion)) |obs| switch (obs) {
|
|
578
|
+
.started, .ended => volatiles.confusion = 0,
|
|
579
|
+
.continuing => if (volatiles.confusion > 1) {
|
|
580
|
+
volatiles.confusion -= 1;
|
|
581
|
+
},
|
|
582
|
+
.overwritten => {},
|
|
583
|
+
else => unreachable,
|
|
562
584
|
} else {
|
|
563
585
|
volatiles.confusion -= 1;
|
|
564
586
|
}
|
|
565
|
-
|
|
587
|
+
|
|
588
|
+
try options.chance.confusion(player, if (volatiles.confusion == 0) .ended else .continuing);
|
|
566
589
|
|
|
567
590
|
if (volatiles.confusion == 0) {
|
|
568
591
|
volatiles.Confusion = false;
|
|
@@ -580,7 +603,9 @@ fn beforeMove(
|
|
|
580
603
|
volatiles.Charging = false;
|
|
581
604
|
volatiles.Binding = false;
|
|
582
605
|
volatiles.Invulnerable = false;
|
|
583
|
-
|
|
606
|
+
|
|
607
|
+
options.chance.observe(.attacking, player, .None);
|
|
608
|
+
options.chance.observe(.binding, player, .None);
|
|
584
609
|
{
|
|
585
610
|
// This feels (and is) disgusting but the cartridge literally just overwrites
|
|
586
611
|
// the opponent's defense with the user's defense and resets it after. As a
|
|
@@ -588,7 +613,9 @@ fn beforeMove(
|
|
|
588
613
|
const def = foe.active.stats.def;
|
|
589
614
|
foe.active.stats.def = active.stats.def;
|
|
590
615
|
defer foe.active.stats.def = def;
|
|
591
|
-
if (!calcDamage(battle, player, player.foe(), null, false))
|
|
616
|
+
if (!calcDamage(battle, player, player.foe(), null, false, options)) {
|
|
617
|
+
return .err;
|
|
618
|
+
}
|
|
592
619
|
}
|
|
593
620
|
// Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
|
|
594
621
|
// on the targeting behavior of the confused Pokémon's selected move which results
|
|
@@ -617,7 +644,10 @@ fn beforeMove(
|
|
|
617
644
|
volatiles.Thrashing = false;
|
|
618
645
|
volatiles.Charging = false;
|
|
619
646
|
volatiles.Binding = false;
|
|
620
|
-
|
|
647
|
+
|
|
648
|
+
options.chance.observe(.attacking, player, .None);
|
|
649
|
+
options.chance.observe(.binding, player, .None);
|
|
650
|
+
|
|
621
651
|
// GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
|
|
622
652
|
try log.cant(.{ ident, .Paralysis });
|
|
623
653
|
return .done;
|
|
@@ -635,12 +665,8 @@ fn beforeMove(
|
|
|
635
665
|
volatiles.state +%= battle.last_damage;
|
|
636
666
|
}
|
|
637
667
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
} else {
|
|
641
|
-
volatiles.attacks -= 1;
|
|
642
|
-
}
|
|
643
|
-
try options.chance.attacking(player, volatiles.attacks);
|
|
668
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
669
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
644
670
|
|
|
645
671
|
if (volatiles.attacks != 0) {
|
|
646
672
|
try log.activate(.{ ident, .Bide });
|
|
@@ -674,35 +700,40 @@ fn beforeMove(
|
|
|
674
700
|
}
|
|
675
701
|
|
|
676
702
|
if (volatiles.Thrashing) {
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
}
|
|
682
|
-
try options.chance.attacking(player, volatiles.attacks);
|
|
703
|
+
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
704
|
+
|
|
705
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
706
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
683
707
|
|
|
684
|
-
if (
|
|
708
|
+
if (volatiles.attacks == 0) {
|
|
709
|
+
const overwritten = volatiles.Confusion;
|
|
710
|
+
|
|
711
|
+
volatiles.Thrashing = false;
|
|
712
|
+
volatiles.Confusion = true;
|
|
713
|
+
volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
714
|
+
|
|
715
|
+
// On Pokémon Showdown this could leak information to the opponent that thrashing has
|
|
716
|
+
// ended (the engine's protocol is always from the omniscient perspective and clients
|
|
717
|
+
// can be expected to recognize this pattern and pass the information only to the
|
|
718
|
+
// correct player)
|
|
685
719
|
try log.start(.{ battle.active(player), .ConfusionSilent });
|
|
720
|
+
options.chance.observe(.confusion, player, if (overwritten)
|
|
721
|
+
if (pkmn.options.overwrite) .overwritten else .continuing
|
|
722
|
+
else
|
|
723
|
+
.started);
|
|
724
|
+
if (!overwritten) options.calc.confusion(player);
|
|
686
725
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
if (handleThrashing(battle, active, player, options)) {
|
|
693
|
-
try log.start(.{ battle.active(player), .ConfusionSilent });
|
|
694
|
-
}
|
|
695
|
-
}
|
|
726
|
+
|
|
727
|
+
// This shouldn't actually set last_used_move, but Pokémon Showdown sets last
|
|
728
|
+
// used in useMove and doesn't have the notion of skipping canMove semantics
|
|
729
|
+
if (showdown) side.last_used_move = side.last_selected_move;
|
|
730
|
+
|
|
696
731
|
return .skip_can;
|
|
697
732
|
}
|
|
698
733
|
|
|
699
734
|
if (volatiles.Binding) {
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
} else {
|
|
703
|
-
volatiles.attacks -= 1;
|
|
704
|
-
}
|
|
705
|
-
try options.chance.binding(player, volatiles.attacks);
|
|
735
|
+
volatiles.attacks = decrement(.binding, player, options, volatiles.attacks);
|
|
736
|
+
try options.chance.binding(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
706
737
|
|
|
707
738
|
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
708
739
|
if (showdown or battle.last_damage != 0) {
|
|
@@ -756,9 +787,14 @@ fn canMove(
|
|
|
756
787
|
|
|
757
788
|
if (move.effect == .Thrashing) {
|
|
758
789
|
Effects.thrashing(battle, player, options);
|
|
759
|
-
} else if (
|
|
760
|
-
// Pokémon Showdown handles this after hit/miss checks and damage calculation
|
|
761
|
-
|
|
790
|
+
} else if (move.effect == .Binding) {
|
|
791
|
+
// Pokémon Showdown handles this after hit/miss checks and damage calculation, though clears
|
|
792
|
+
// Recharging in onTryMove (required for the Hyper Beam automatic selection glitch)
|
|
793
|
+
if (showdown) {
|
|
794
|
+
battle.foe(player).active.volatiles.Recharging = false;
|
|
795
|
+
} else {
|
|
796
|
+
try Effects.binding(battle, player, false, options);
|
|
797
|
+
}
|
|
762
798
|
}
|
|
763
799
|
|
|
764
800
|
return true;
|
|
@@ -795,7 +831,7 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
|
|
|
795
831
|
assert(active.move(mslot).pp == side.stored().move(mslot).pp);
|
|
796
832
|
}
|
|
797
833
|
|
|
798
|
-
fn incrementPP(side: *Side, mslot: u4) void {
|
|
834
|
+
fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
|
|
799
835
|
var active = &side.active;
|
|
800
836
|
const volatiles = &active.volatiles;
|
|
801
837
|
|
|
@@ -803,8 +839,11 @@ fn incrementPP(side: *Side, mslot: u4) void {
|
|
|
803
839
|
// GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
|
|
804
840
|
if (showdown and volatiles.Transform) return;
|
|
805
841
|
|
|
842
|
+
const n = options.calc.overridden(player, .pp) orelse 1;
|
|
843
|
+
if (volatiles.Transform and !options.chance.pp(player, n)) return;
|
|
844
|
+
|
|
806
845
|
assert(mslot > 0 and mslot <= 4);
|
|
807
|
-
side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +%
|
|
846
|
+
side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% n;
|
|
808
847
|
}
|
|
809
848
|
|
|
810
849
|
// Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
|
|
@@ -852,13 +891,13 @@ fn doMove(
|
|
|
852
891
|
break :miss true;
|
|
853
892
|
}
|
|
854
893
|
break :miss (move.target != .Self and
|
|
855
|
-
!moveHit(battle, player, move, &immune, &mist, options));
|
|
894
|
+
!try moveHit(battle, player, move, &immune, &mist, options));
|
|
856
895
|
};
|
|
857
896
|
assert(!immune or miss or (showdown and move.effect == .Binding));
|
|
858
897
|
|
|
859
898
|
var late = showdown and move.effect != .Explode;
|
|
860
899
|
const skip = status or immune;
|
|
861
|
-
if ((!showdown or (!skip or counter)) and !miss)
|
|
900
|
+
if ((!showdown or (!skip or counter)) and !miss) damage: {
|
|
862
901
|
if (showdown and move.effect.isMulti()) {
|
|
863
902
|
try Effects.multiHit(battle, player, move, options);
|
|
864
903
|
hits = side.active.volatiles.attacks;
|
|
@@ -880,8 +919,9 @@ fn doMove(
|
|
|
880
919
|
ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
|
|
881
920
|
// This can overflow after adjustDamage, but will still be sufficient to OHKO
|
|
882
921
|
battle.last_damage = if (ohko) 65535 else 0;
|
|
883
|
-
if (showdown) break :
|
|
884
|
-
} else if (!calcDamage(battle, player, player.foe(), move, crit)) {
|
|
922
|
+
if (showdown) break :damage; // skip adjustDamage / randomizeDamage
|
|
923
|
+
} else if (!calcDamage(battle, player, player.foe(), move, crit, options)) {
|
|
924
|
+
if (!showdown) try options.chance.commit(player, .err);
|
|
885
925
|
return @as(?Result, Result.Error);
|
|
886
926
|
}
|
|
887
927
|
if (battle.last_damage == 0) {
|
|
@@ -895,11 +935,12 @@ fn doMove(
|
|
|
895
935
|
}
|
|
896
936
|
}
|
|
897
937
|
|
|
938
|
+
var missed: bool = undefined;
|
|
898
939
|
const zero = battle.last_damage == 0;
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
940
|
+
if (!showdown and !skip) {
|
|
941
|
+
missed = !try moveHit(battle, player, move, &immune, &mist, options);
|
|
942
|
+
miss = missed or zero;
|
|
943
|
+
}
|
|
903
944
|
|
|
904
945
|
assert(showdown or miss or battle.last_damage > 0 or skip);
|
|
905
946
|
assert((!showdown and miss) or !(ohko and immune));
|
|
@@ -926,11 +967,32 @@ fn doMove(
|
|
|
926
967
|
try log.immune(.{ foe_ident, .OHKO });
|
|
927
968
|
} else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
|
|
928
969
|
try log.immune(.{ foe_ident, .None });
|
|
970
|
+
if (!showdown) {
|
|
971
|
+
if (move.effect == .Binding) {
|
|
972
|
+
// Rather obnoxiously, the cartridge sets the Binding volatile in canMove and
|
|
973
|
+
// then simply clears it later on if the move happens to miss in moveHit.
|
|
974
|
+
// However, moveHit can return true and we'll still end up in the outer miss
|
|
975
|
+
// handling block because attacking into an immune Pokemon does zero damage
|
|
976
|
+
// (considered missing)
|
|
977
|
+
assert(zero);
|
|
978
|
+
// Thus we still need to actually save the hit result (but not the crit roll).
|
|
979
|
+
// Using .miss for this commit would accomplish what we want, but is somewhat
|
|
980
|
+
// misleading as its entirely possible the move actually did hit (and the target
|
|
981
|
+
// is now trapped) so we use the .binding value instead which is effectively
|
|
982
|
+
// identical, just doesn't claim to know about the actual results of the hit
|
|
983
|
+
// roll)
|
|
984
|
+
try options.chance.commit(player, .binding);
|
|
985
|
+
} else {
|
|
986
|
+
// The .glitch option allows for conditionally committing if we are in a
|
|
987
|
+
// division-by-zero freeze alternative timeline scenario
|
|
988
|
+
try options.chance.commit(player, .glitch);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
929
991
|
} else if (mist) {
|
|
930
992
|
if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
|
|
931
993
|
try log.fail(.{ foe_ident, .None });
|
|
932
994
|
} else {
|
|
933
|
-
if (showdown
|
|
995
|
+
if (!showdown) try options.chance.commit(player, if (zero and !missed) .hit else .miss);
|
|
934
996
|
try log.lastmiss(.{});
|
|
935
997
|
try log.miss(.{battle.active(player)});
|
|
936
998
|
}
|
|
@@ -945,17 +1007,15 @@ fn doMove(
|
|
|
945
1007
|
try buildRage(battle, player.foe(), options);
|
|
946
1008
|
} else if (showdown and move.effect == .Disable) {
|
|
947
1009
|
try buildRage(battle, player.foe(), options);
|
|
948
|
-
} else if (!showdown and immune and move.effect == .Binding) {
|
|
949
|
-
try options.chance.commit(player, .binding);
|
|
950
1010
|
}
|
|
951
1011
|
return null;
|
|
952
|
-
} else {
|
|
1012
|
+
} else if (!showdown) {
|
|
953
1013
|
try options.chance.commit(player, .hit);
|
|
954
1014
|
}
|
|
955
1015
|
|
|
956
1016
|
// On the cartridge MultiHit doesn't get set up until after damage has been applied for the
|
|
957
1017
|
// first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
|
|
958
|
-
// it up above before damage calculation)
|
|
1018
|
+
// it up above before damage calculation)
|
|
959
1019
|
if (!showdown and move.effect.isMulti()) {
|
|
960
1020
|
try Effects.multiHit(battle, player, move, options);
|
|
961
1021
|
hits = side.active.volatiles.attacks;
|
|
@@ -994,32 +1054,30 @@ fn doMove(
|
|
|
994
1054
|
try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
995
1055
|
}
|
|
996
1056
|
try log.hitcount(.{ battle.active(player.foe()), hit });
|
|
997
|
-
} else if (showdown) {
|
|
1057
|
+
} else if (showdown and move.effect == .Binding) {
|
|
998
1058
|
// This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
try buildRage(battle, player.foe(), options);
|
|
1010
|
-
}
|
|
1011
|
-
return null;
|
|
1059
|
+
try Effects.binding(battle, player, rewrap, options);
|
|
1060
|
+
if (immune) {
|
|
1061
|
+
battle.last_damage = 0;
|
|
1062
|
+
assert(foe.stored().hp > 0);
|
|
1063
|
+
// Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
|
|
1064
|
+
if (sub) {
|
|
1065
|
+
try log.activate(.{ battle.active(player.foe()), .Substitute });
|
|
1066
|
+
} else {
|
|
1067
|
+
try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
|
|
1068
|
+
try buildRage(battle, player.foe(), options);
|
|
1012
1069
|
}
|
|
1070
|
+
return null;
|
|
1013
1071
|
}
|
|
1014
1072
|
}
|
|
1015
1073
|
|
|
1016
1074
|
// 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
|
|
1075
|
+
// if an effect was intended to "always happen" it will still get skipped
|
|
1018
1076
|
if (nullified) return null;
|
|
1019
1077
|
|
|
1020
1078
|
// On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
|
|
1021
1079
|
// 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
|
|
1080
|
+
// setup before the loop means we can avoid having to waste time doing no-op handler searches
|
|
1023
1081
|
if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
|
|
1024
1082
|
|
|
1025
1083
|
if (foe.stored().hp == 0) return null;
|
|
@@ -1034,7 +1092,7 @@ fn doMove(
|
|
|
1034
1092
|
// below). For Twineedle we change the data to that of one with PoisonChance1 given its
|
|
1035
1093
|
// MultiHit behavior is complete after the loop above, though Pokémon Showdown handles the
|
|
1036
1094
|
// Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
|
|
1037
|
-
// puts the |-status| message before |-hitcount| instead of after
|
|
1095
|
+
// puts the |-status| message before |-hitcount| instead of after
|
|
1038
1096
|
if (move.effect == .Twineedle) {
|
|
1039
1097
|
if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
1040
1098
|
} else if (move.effect == .Disable) {
|
|
@@ -1058,7 +1116,9 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
|
|
|
1058
1116
|
rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
|
|
1059
1117
|
rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
|
|
1060
1118
|
|
|
1061
|
-
|
|
1119
|
+
const spurious =
|
|
1120
|
+
!showdown and ((move.bp == 0 and move.effect != .OHKO) or move.target == .Depends);
|
|
1121
|
+
return Rolls.criticalHit(battle, player, @intCast(rate), spurious, options);
|
|
1062
1122
|
}
|
|
1063
1123
|
|
|
1064
1124
|
fn calcDamage(
|
|
@@ -1067,6 +1127,7 @@ fn calcDamage(
|
|
|
1067
1127
|
target_player: Player,
|
|
1068
1128
|
m: ?Move.Data,
|
|
1069
1129
|
crit: bool,
|
|
1130
|
+
options: anytype,
|
|
1070
1131
|
) bool {
|
|
1071
1132
|
// Confusion (indicated when m == null) just needs a 40 BP physical move
|
|
1072
1133
|
const cfz = m == null;
|
|
@@ -1078,28 +1139,8 @@ fn calcDamage(
|
|
|
1078
1139
|
|
|
1079
1140
|
const special = move.type.special();
|
|
1080
1141
|
|
|
1081
|
-
|
|
1082
|
-
var
|
|
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
|
|
1142
|
+
var atk = attack(side, crit, special);
|
|
1143
|
+
var def = defense(target, crit, special, cfz);
|
|
1103
1144
|
|
|
1104
1145
|
// Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
|
|
1105
1146
|
// will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
|
|
@@ -1107,7 +1148,7 @@ fn calcDamage(
|
|
|
1107
1148
|
if ((!showdown or !cfz) and (atk > 255 or def > 255)) {
|
|
1108
1149
|
atk = @max((atk / 4) & 255, 1);
|
|
1109
1150
|
// GLITCH: not adjusted to be a min of 1 on cartridge (can lead to division-by-zero freeze)
|
|
1110
|
-
def = @max((def / 4) & 255, if (
|
|
1151
|
+
def = @max((def / 4) & 255, if (pkmn.options.mod) 1 else 0);
|
|
1111
1152
|
}
|
|
1112
1153
|
|
|
1113
1154
|
const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
|
|
@@ -1124,11 +1165,41 @@ fn calcDamage(
|
|
|
1124
1165
|
d = @min(997, d);
|
|
1125
1166
|
d += 2;
|
|
1126
1167
|
|
|
1168
|
+
// Whether or not a critical hit occurred can change whether a division-by-zero freeze is
|
|
1169
|
+
// encountered so we need to make sure that in the event of a miss we commit the status of the
|
|
1170
|
+
// crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
|
|
1171
|
+
// that results in counter intuitive probabilities in the common case
|
|
1172
|
+
if (pkmn.options.chance and !pkmn.options.mod) {
|
|
1173
|
+
atk = attack(side, !crit, special);
|
|
1174
|
+
def = defense(target, !crit, special, false);
|
|
1175
|
+
if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1127
1178
|
battle.last_damage = @intCast(d);
|
|
1128
1179
|
|
|
1129
1180
|
return true;
|
|
1130
1181
|
}
|
|
1131
1182
|
|
|
1183
|
+
fn attack(side: *Side, crit: bool, special: bool) u32 {
|
|
1184
|
+
return if (crit)
|
|
1185
|
+
if (special) side.stored().stats.spc else side.stored().stats.atk
|
|
1186
|
+
else if (special) side.active.stats.spc else side.active.stats.atk;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
fn defense(target: *Side, crit: bool, special: bool, cfz: bool) u32 {
|
|
1190
|
+
return if (crit)
|
|
1191
|
+
if (special) target.stored().stats.spc else target.stored().stats.def
|
|
1192
|
+
else
|
|
1193
|
+
// GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
|
|
1194
|
+
if (special)
|
|
1195
|
+
target.active.stats.spc *
|
|
1196
|
+
@as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
|
|
1197
|
+
else
|
|
1198
|
+
// Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
|
|
1199
|
+
target.active.stats.def *
|
|
1200
|
+
@as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1132
1203
|
fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
1133
1204
|
const side = battle.side(player);
|
|
1134
1205
|
const foe = battle.foe(player);
|
|
@@ -1173,7 +1244,6 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1173
1244
|
const foe = battle.foe(player);
|
|
1174
1245
|
|
|
1175
1246
|
if (!try checkHit(battle, player, move, options)) return null;
|
|
1176
|
-
try options.chance.commit(player, .hit);
|
|
1177
1247
|
|
|
1178
1248
|
battle.last_damage = switch (side.last_selected_move) {
|
|
1179
1249
|
.SuperFang => @max(foe.stored().hp / 2, 1),
|
|
@@ -1184,13 +1254,13 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1184
1254
|
.Psywave => power: {
|
|
1185
1255
|
const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
|
|
1186
1256
|
// GLITCH: Psywave infinite glitch loop
|
|
1187
|
-
if (!
|
|
1257
|
+
if (!pkmn.options.mod and max <= 1) return Result.Error;
|
|
1188
1258
|
break :power try Rolls.psywave(battle, player, max, options);
|
|
1189
1259
|
},
|
|
1190
1260
|
else => unreachable,
|
|
1191
1261
|
};
|
|
1192
1262
|
|
|
1193
|
-
if (battle.last_damage == 0) return if (
|
|
1263
|
+
if (battle.last_damage == 0) return if (pkmn.options.mod) null else Result.Error;
|
|
1194
1264
|
|
|
1195
1265
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1196
1266
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
@@ -1227,16 +1297,15 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1227
1297
|
|
|
1228
1298
|
if (!used or !selected) {
|
|
1229
1299
|
// GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
|
|
1230
|
-
if (!
|
|
1300
|
+
if (!pkmn.options.mod) return Result.Error;
|
|
1231
1301
|
try options.log.fail(.{ battle.active(player), .None });
|
|
1232
1302
|
return null;
|
|
1233
1303
|
}
|
|
1234
1304
|
|
|
1235
1305
|
battle.last_damage *|= 2;
|
|
1236
1306
|
|
|
1237
|
-
// Pokémon Showdown calls
|
|
1307
|
+
// Pokémon Showdown calls moveHit before Counter
|
|
1238
1308
|
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
1239
|
-
try options.chance.commit(player, .hit);
|
|
1240
1309
|
|
|
1241
1310
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1242
1311
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
@@ -1265,7 +1334,7 @@ fn applyDamage(
|
|
|
1265
1334
|
subbed.active.volatiles.Substitute = false;
|
|
1266
1335
|
// battle.last_damage is not updated with the amount of HP the Substitute had
|
|
1267
1336
|
try options.log.end(.{ battle.active(sub_player), .Substitute });
|
|
1268
|
-
options.calc.capped(
|
|
1337
|
+
if (reason != .Confusion) options.calc.capped(sub_player);
|
|
1269
1338
|
return true;
|
|
1270
1339
|
} else {
|
|
1271
1340
|
// Safe to truncate since less than subbed.volatiles.substitute which is a u8
|
|
@@ -1299,7 +1368,7 @@ fn mirrorMove(
|
|
|
1299
1368
|
try options.log.fail(.{ battle.active(player), .None });
|
|
1300
1369
|
return null;
|
|
1301
1370
|
} else if (!showdown or foe.last_used_move != .Struggle) {
|
|
1302
|
-
incrementPP(side, mslot);
|
|
1371
|
+
incrementPP(side, player, mslot, options);
|
|
1303
1372
|
}
|
|
1304
1373
|
|
|
1305
1374
|
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
|
|
@@ -1320,7 +1389,7 @@ fn metronome(
|
|
|
1320
1389
|
var side = battle.side(player);
|
|
1321
1390
|
|
|
1322
1391
|
side.last_selected_move = try Rolls.metronome(battle, player, options);
|
|
1323
|
-
incrementPP(side, mslot);
|
|
1392
|
+
incrementPP(side, player, mslot, options);
|
|
1324
1393
|
|
|
1325
1394
|
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
|
|
1326
1395
|
return null;
|
|
@@ -1332,7 +1401,10 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
|
|
|
1332
1401
|
var immune = false;
|
|
1333
1402
|
var mist = false;
|
|
1334
1403
|
|
|
1335
|
-
if (moveHit(battle, player, move, &immune, &mist, options))
|
|
1404
|
+
if (try moveHit(battle, player, move, &immune, &mist, options)) {
|
|
1405
|
+
if (!showdown) try options.chance.commit(player, .hit);
|
|
1406
|
+
return true;
|
|
1407
|
+
}
|
|
1336
1408
|
|
|
1337
1409
|
assert(!immune);
|
|
1338
1410
|
if (mist) {
|
|
@@ -1341,11 +1413,11 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
|
|
|
1341
1413
|
try options.log.activate(.{ foe_ident, .Mist });
|
|
1342
1414
|
try options.log.fail(.{ foe_ident, .None });
|
|
1343
1415
|
} else {
|
|
1344
|
-
try options.chance.commit(player, .miss);
|
|
1345
1416
|
try options.log.lastmiss(.{});
|
|
1346
1417
|
try options.log.miss(.{battle.active(player)});
|
|
1347
1418
|
}
|
|
1348
1419
|
|
|
1420
|
+
if (!showdown) try options.chance.commit(player, .miss);
|
|
1349
1421
|
return false;
|
|
1350
1422
|
}
|
|
1351
1423
|
|
|
@@ -1356,7 +1428,7 @@ fn moveHit(
|
|
|
1356
1428
|
immune: *bool,
|
|
1357
1429
|
mist: *bool,
|
|
1358
1430
|
options: anytype,
|
|
1359
|
-
) bool {
|
|
1431
|
+
) !bool {
|
|
1360
1432
|
var side = battle.side(player);
|
|
1361
1433
|
const foe = battle.foe(player);
|
|
1362
1434
|
|
|
@@ -1407,7 +1479,7 @@ fn moveHit(
|
|
|
1407
1479
|
if (overwrite) side.active.volatiles.state = accuracy;
|
|
1408
1480
|
|
|
1409
1481
|
// GLITCH: max accuracy is 255 so 1/256 chance of miss
|
|
1410
|
-
break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
|
|
1482
|
+
break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
|
|
1411
1483
|
};
|
|
1412
1484
|
|
|
1413
1485
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1418,13 +1490,17 @@ fn moveHit(
|
|
|
1418
1490
|
|
|
1419
1491
|
if (!miss) return true;
|
|
1420
1492
|
if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
|
|
1493
|
+
|
|
1421
1494
|
side.active.volatiles.Binding = false;
|
|
1495
|
+
options.chance.observe(.binding, player, .None);
|
|
1496
|
+
|
|
1422
1497
|
return false;
|
|
1423
1498
|
}
|
|
1424
1499
|
|
|
1425
1500
|
fn checkFaint(
|
|
1426
1501
|
battle: anytype,
|
|
1427
1502
|
player: Player,
|
|
1503
|
+
cap: bool,
|
|
1428
1504
|
options: anytype,
|
|
1429
1505
|
) @TypeOf(options.log).Error!?Result {
|
|
1430
1506
|
const side = battle.side(player);
|
|
@@ -1437,8 +1513,8 @@ fn checkFaint(
|
|
|
1437
1513
|
const foe_out = foe_fainted and findFirstAlive(foe) == 0;
|
|
1438
1514
|
const more = player_out or foe_out;
|
|
1439
1515
|
|
|
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;
|
|
1516
|
+
if (try faint(battle, player, !(more or foe_fainted), cap, options)) |r| return r;
|
|
1517
|
+
if (foe_fainted) if (try faint(battle, player.foe(), !more, false, options)) |r| return r;
|
|
1442
1518
|
|
|
1443
1519
|
assert(!side.active.volatiles.MultiHit);
|
|
1444
1520
|
assert(!foe.active.volatiles.MultiHit);
|
|
@@ -1459,7 +1535,7 @@ fn checkFaint(
|
|
|
1459
1535
|
return .{ .p1 = foe_choice, .p2 = .Switch };
|
|
1460
1536
|
}
|
|
1461
1537
|
|
|
1462
|
-
fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
|
|
1538
|
+
fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytype) !?Result {
|
|
1463
1539
|
var side = battle.side(player);
|
|
1464
1540
|
var foe = battle.foe(player);
|
|
1465
1541
|
assert(side.stored().hp == 0);
|
|
@@ -1468,7 +1544,7 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
|
|
|
1468
1544
|
assert(!foe_volatiles.MultiHit);
|
|
1469
1545
|
if (foe_volatiles.Bide) {
|
|
1470
1546
|
assert(!foe_volatiles.Thrashing and !foe_volatiles.Rage);
|
|
1471
|
-
foe_volatiles.state = if (
|
|
1547
|
+
foe_volatiles.state = if (pkmn.options.mod) 0 else foe_volatiles.state & 255;
|
|
1472
1548
|
if (foe_volatiles.state != 0) return Result.Error;
|
|
1473
1549
|
}
|
|
1474
1550
|
|
|
@@ -1493,7 +1569,9 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
|
|
|
1493
1569
|
}
|
|
1494
1570
|
|
|
1495
1571
|
try options.log.faint(.{ battle.active(player), done });
|
|
1496
|
-
|
|
1572
|
+
// We can only safely set capped if the faint occured as the direct result of an opponent's
|
|
1573
|
+
// move (as opposed to due to residual damage, recoil, Explosion, etc)
|
|
1574
|
+
if (cap) options.calc.capped(player);
|
|
1497
1575
|
return null;
|
|
1498
1576
|
}
|
|
1499
1577
|
|
|
@@ -1557,12 +1635,12 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
|
|
|
1557
1635
|
|
|
1558
1636
|
battle.turn += 1;
|
|
1559
1637
|
|
|
1560
|
-
if (
|
|
1638
|
+
if (pkmn.options.mod and pkmn.options.ebc and checkEBC(battle)) {
|
|
1561
1639
|
try options.log.tie(.{});
|
|
1562
1640
|
return Result.Tie;
|
|
1563
1641
|
}
|
|
1564
1642
|
|
|
1565
|
-
if (
|
|
1643
|
+
if (pkmn.options.mod and battle.turn >= 1000) {
|
|
1566
1644
|
try options.log.tie(.{});
|
|
1567
1645
|
return Result.Tie;
|
|
1568
1646
|
} else if (battle.turn >= 65535) {
|
|
@@ -1653,17 +1731,6 @@ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
|
|
|
1653
1731
|
}
|
|
1654
1732
|
}
|
|
1655
1733
|
|
|
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
1734
|
fn onBegin(
|
|
1668
1735
|
battle: anytype,
|
|
1669
1736
|
player: Player,
|
|
@@ -1750,8 +1817,10 @@ pub const Effects = struct {
|
|
|
1750
1817
|
|
|
1751
1818
|
side.active.volatiles.Bide = true;
|
|
1752
1819
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1820
|
+
|
|
1753
1821
|
side.active.volatiles.state = 0;
|
|
1754
1822
|
side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
1823
|
+
options.chance.observe(.attacking, player, .started);
|
|
1755
1824
|
|
|
1756
1825
|
try options.log.start(.{ battle.active(player), .Bide });
|
|
1757
1826
|
}
|
|
@@ -1813,12 +1882,14 @@ pub const Effects = struct {
|
|
|
1813
1882
|
if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
1814
1883
|
if (!try checkHit(battle, player, move, options)) return;
|
|
1815
1884
|
}
|
|
1816
|
-
try options.chance.commit(player, .hit);
|
|
1817
1885
|
}
|
|
1818
1886
|
|
|
1819
1887
|
if (foe.active.volatiles.Confusion) return;
|
|
1820
1888
|
foe.active.volatiles.Confusion = true;
|
|
1821
|
-
|
|
1889
|
+
|
|
1890
|
+
foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
1891
|
+
options.chance.observe(.confusion, player.foe(), .started);
|
|
1892
|
+
options.calc.confusion(player.foe());
|
|
1822
1893
|
|
|
1823
1894
|
try options.log.start(.{ battle.active(player.foe()), .Confusion });
|
|
1824
1895
|
}
|
|
@@ -1846,10 +1917,7 @@ pub const Effects = struct {
|
|
|
1846
1917
|
const foe_ident = battle.active(player.foe());
|
|
1847
1918
|
|
|
1848
1919
|
// 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
|
-
}
|
|
1920
|
+
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
1853
1921
|
|
|
1854
1922
|
if (volatiles.disable_move != 0) {
|
|
1855
1923
|
try options.log.fail(.{ foe_ident, .None });
|
|
@@ -1867,18 +1935,18 @@ pub const Effects = struct {
|
|
|
1867
1935
|
|
|
1868
1936
|
// Technically this is still considered simply a "miss" on the cartridge,
|
|
1869
1937
|
// but diverging from Pokémon Showdown here would mostly just be pedantic
|
|
1870
|
-
if (n == 0) {
|
|
1938
|
+
if (n == 0 or (pkmn.options.mod and err)) {
|
|
1871
1939
|
try options.log.fail(.{ foe_ident, .None });
|
|
1872
1940
|
return null;
|
|
1873
1941
|
} else if (err) {
|
|
1874
1942
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
1875
|
-
assert(!showdown);
|
|
1876
1943
|
return Result.Error;
|
|
1877
1944
|
}
|
|
1878
1945
|
|
|
1879
1946
|
volatiles.disable_move =
|
|
1880
1947
|
@intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
|
|
1881
1948
|
volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
|
|
1949
|
+
options.chance.observe(.disable, player.foe(), .started);
|
|
1882
1950
|
|
|
1883
1951
|
const id = foe.active.move(volatiles.disable_move).id;
|
|
1884
1952
|
try options.log.start(.{ foe_ident, .Disable, id });
|
|
@@ -1949,7 +2017,7 @@ pub const Effects = struct {
|
|
|
1949
2017
|
if (foe.active.types.includes(move.type)) return;
|
|
1950
2018
|
if (!try Rolls.secondaryChance(battle, player, true, options)) return;
|
|
1951
2019
|
// Freeze Clause Mod
|
|
1952
|
-
if (
|
|
2020
|
+
if (pkmn.options.mod) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
1953
2021
|
|
|
1954
2022
|
foe_stored.status = Status.init(.FRZ);
|
|
1955
2023
|
// GLITCH: Hyper Beam recharging status is not cleared
|
|
@@ -1985,22 +2053,24 @@ pub const Effects = struct {
|
|
|
1985
2053
|
if (p != player and Status.any(s.stored().status)) {
|
|
1986
2054
|
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
1987
2055
|
s.stored().status = 0;
|
|
2056
|
+
options.chance.observe(.sleep, p, .None);
|
|
1988
2057
|
} else if (showdown and s.stored().status == Status.TOX) {
|
|
1989
2058
|
s.stored().status = Status.init(.PSN);
|
|
1990
2059
|
try log.status(.{ battle.active(p), s.stored().status, .None });
|
|
1991
2060
|
}
|
|
1992
|
-
try clearVolatiles(battle, p,
|
|
2061
|
+
try clearVolatiles(battle, p, options);
|
|
1993
2062
|
}
|
|
1994
2063
|
} else {
|
|
1995
2064
|
if (Status.any(foe_stored.status)) {
|
|
1996
2065
|
if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
|
|
1997
2066
|
foe.last_selected_move = .SKIP_TURN;
|
|
2067
|
+
options.chance.observe(.sleep, player.foe(), .None);
|
|
1998
2068
|
}
|
|
1999
2069
|
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
2000
2070
|
foe_stored.status = 0;
|
|
2001
2071
|
}
|
|
2002
|
-
try clearVolatiles(battle, player,
|
|
2003
|
-
try clearVolatiles(battle, player.foe(),
|
|
2072
|
+
try clearVolatiles(battle, player, options);
|
|
2073
|
+
try clearVolatiles(battle, player.foe(), options);
|
|
2004
2074
|
}
|
|
2005
2075
|
}
|
|
2006
2076
|
|
|
@@ -2051,7 +2121,6 @@ pub const Effects = struct {
|
|
|
2051
2121
|
}
|
|
2052
2122
|
}
|
|
2053
2123
|
|
|
2054
|
-
try options.chance.commit(player, .hit);
|
|
2055
2124
|
foe.active.volatiles.LeechSeed = true;
|
|
2056
2125
|
|
|
2057
2126
|
try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
|
|
@@ -2092,12 +2161,10 @@ pub const Effects = struct {
|
|
|
2092
2161
|
if (!has_mimic) {
|
|
2093
2162
|
// Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
|
|
2094
2163
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2095
|
-
try options.chance.commit(player, .hit);
|
|
2096
2164
|
return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
2097
2165
|
}
|
|
2098
2166
|
}
|
|
2099
2167
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2100
|
-
try options.chance.commit(player, .hit);
|
|
2101
2168
|
|
|
2102
2169
|
const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
|
|
2103
2170
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
@@ -2151,7 +2218,6 @@ pub const Effects = struct {
|
|
|
2151
2218
|
if (immune) return log.immune(.{ foe_ident, .None });
|
|
2152
2219
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2153
2220
|
}
|
|
2154
|
-
try options.chance.commit(player, .hit);
|
|
2155
2221
|
|
|
2156
2222
|
foe_stored.status = Status.init(.PAR);
|
|
2157
2223
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
@@ -2219,7 +2285,6 @@ pub const Effects = struct {
|
|
|
2219
2285
|
|
|
2220
2286
|
if (move.effect == .Poison) {
|
|
2221
2287
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2222
|
-
try options.chance.commit(player, .hit);
|
|
2223
2288
|
} else {
|
|
2224
2289
|
if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
|
|
2225
2290
|
return;
|
|
@@ -2288,13 +2353,12 @@ pub const Effects = struct {
|
|
|
2288
2353
|
if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
|
|
2289
2354
|
});
|
|
2290
2355
|
}
|
|
2291
|
-
// If
|
|
2356
|
+
// If moveHit in doMove didn't return true Pokémon Showdown wouldn't be in here
|
|
2292
2357
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2293
|
-
try options.chance.commit(player, .hit);
|
|
2294
2358
|
}
|
|
2295
2359
|
|
|
2296
2360
|
// Sleep Clause Mod
|
|
2297
|
-
if (
|
|
2361
|
+
if (pkmn.options.mod) {
|
|
2298
2362
|
for (foe.pokemon) |p| {
|
|
2299
2363
|
if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
|
|
2300
2364
|
}
|
|
@@ -2302,6 +2366,8 @@ pub const Effects = struct {
|
|
|
2302
2366
|
foe.active.volatiles.Recharging = false;
|
|
2303
2367
|
|
|
2304
2368
|
foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
|
|
2369
|
+
options.chance.observe(.sleep, player.foe(), .started);
|
|
2370
|
+
|
|
2305
2371
|
const last = battle.side(player).last_selected_move;
|
|
2306
2372
|
try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
|
|
2307
2373
|
}
|
|
@@ -2343,9 +2409,7 @@ pub const Effects = struct {
|
|
|
2343
2409
|
// Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
|
|
2344
2410
|
if (showdown) {
|
|
2345
2411
|
if (battle.side(player).last_selected_move == .Teleport) return;
|
|
2346
|
-
|
|
2347
|
-
try options.chance.commit(player, .hit);
|
|
2348
|
-
}
|
|
2412
|
+
_ = try checkHit(battle, player, move, options);
|
|
2349
2413
|
battle.last_damage = 0;
|
|
2350
2414
|
} else {
|
|
2351
2415
|
try options.log.fail(.{ battle.active(player), .None });
|
|
@@ -2360,6 +2424,8 @@ pub const Effects = struct {
|
|
|
2360
2424
|
|
|
2361
2425
|
volatiles.Thrashing = true;
|
|
2362
2426
|
volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
2427
|
+
|
|
2428
|
+
options.chance.observe(.attacking, player, .started);
|
|
2363
2429
|
}
|
|
2364
2430
|
|
|
2365
2431
|
fn transform(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -2398,13 +2464,14 @@ pub const Effects = struct {
|
|
|
2398
2464
|
if (side.active.volatiles.Binding) return;
|
|
2399
2465
|
side.active.volatiles.Binding = true;
|
|
2400
2466
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2401
|
-
// (Pokémon Showdown
|
|
2467
|
+
// (Pokémon Showdown clears Recharging in onTryMove instead)
|
|
2402
2468
|
if (!showdown) {
|
|
2403
2469
|
foe.active.volatiles.Recharging = false;
|
|
2404
2470
|
} else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
|
|
2405
2471
|
|
|
2406
2472
|
side.active.volatiles.attacks =
|
|
2407
2473
|
try Rolls.distribution(battle, .Binding, player, options) - 1;
|
|
2474
|
+
options.chance.observe(.binding, player, .started);
|
|
2408
2475
|
}
|
|
2409
2476
|
|
|
2410
2477
|
fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2521,7 +2588,6 @@ pub const Effects = struct {
|
|
|
2521
2588
|
} else {
|
|
2522
2589
|
// checkHit already checks for Invulnerable
|
|
2523
2590
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2524
|
-
try options.chance.commit(player, .hit);
|
|
2525
2591
|
}
|
|
2526
2592
|
|
|
2527
2593
|
var stats = &foe.active.stats;
|
|
@@ -2635,22 +2701,22 @@ fn isForced(active: anytype) bool {
|
|
|
2635
2701
|
active.volatiles.Thrashing or active.volatiles.Charging;
|
|
2636
2702
|
}
|
|
2637
2703
|
|
|
2638
|
-
fn clearVolatiles(battle: anytype, who: Player,
|
|
2704
|
+
fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
|
|
2639
2705
|
var log = options.log;
|
|
2640
2706
|
var side = battle.side(who);
|
|
2641
2707
|
var volatiles = &side.active.volatiles;
|
|
2642
2708
|
const ident = battle.active(who);
|
|
2643
2709
|
|
|
2644
|
-
options.chance.clearDurations(who, clear);
|
|
2645
|
-
|
|
2646
2710
|
if (volatiles.disable_move != 0) {
|
|
2647
2711
|
volatiles.disable_move = 0;
|
|
2648
2712
|
volatiles.disable_duration = 0;
|
|
2713
|
+
options.chance.observe(.disable, who, .None);
|
|
2649
2714
|
try log.end(.{ ident, .DisableSilent });
|
|
2650
2715
|
}
|
|
2651
2716
|
if (volatiles.Confusion) {
|
|
2652
2717
|
// volatiles.confusion is left unchanged
|
|
2653
2718
|
volatiles.Confusion = false;
|
|
2719
|
+
options.chance.observe(.confusion, who, .None);
|
|
2654
2720
|
try log.end(.{ ident, .ConfusionSilent });
|
|
2655
2721
|
}
|
|
2656
2722
|
if (volatiles.Mist) {
|
|
@@ -2685,6 +2751,19 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
|
|
|
2685
2751
|
}
|
|
2686
2752
|
}
|
|
2687
2753
|
|
|
2754
|
+
fn decrement(
|
|
2755
|
+
comptime field: chance.Action.Field,
|
|
2756
|
+
player: Player,
|
|
2757
|
+
options: anytype,
|
|
2758
|
+
n: anytype,
|
|
2759
|
+
) @TypeOf(n) {
|
|
2760
|
+
return if (options.calc.overridden(player, field)) |obs| switch (obs) {
|
|
2761
|
+
.started, .ended => 0,
|
|
2762
|
+
.continuing => if (n > 1) n - 1 else n,
|
|
2763
|
+
else => unreachable,
|
|
2764
|
+
} else n - 1;
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2688
2767
|
pub const Rolls = struct {
|
|
2689
2768
|
fn speedTie(battle: anytype, options: anytype) !bool {
|
|
2690
2769
|
const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
|
|
@@ -2700,7 +2779,13 @@ pub const Rolls = struct {
|
|
|
2700
2779
|
return p1;
|
|
2701
2780
|
}
|
|
2702
2781
|
|
|
2703
|
-
fn criticalHit(
|
|
2782
|
+
fn criticalHit(
|
|
2783
|
+
battle: anytype,
|
|
2784
|
+
player: Player,
|
|
2785
|
+
rate: u8,
|
|
2786
|
+
spurious: bool,
|
|
2787
|
+
options: anytype,
|
|
2788
|
+
) !bool {
|
|
2704
2789
|
const crit = if (options.calc.overridden(player, .critical_hit)) |val|
|
|
2705
2790
|
val == .true
|
|
2706
2791
|
else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
|
|
@@ -2710,7 +2795,7 @@ pub const Rolls = struct {
|
|
|
2710
2795
|
else
|
|
2711
2796
|
std.math.rotl(u8, battle.rng.next(), 3) < rate;
|
|
2712
2797
|
|
|
2713
|
-
try options.chance.criticalHit(player, crit, rate);
|
|
2798
|
+
if (!spurious) try options.chance.criticalHit(player, crit, rate);
|
|
2714
2799
|
return crit;
|
|
2715
2800
|
}
|
|
2716
2801
|
|
|
@@ -2732,17 +2817,19 @@ pub const Rolls = struct {
|
|
|
2732
2817
|
return roll;
|
|
2733
2818
|
}
|
|
2734
2819
|
|
|
2735
|
-
|
|
2820
|
+
const uN = if (pkmn.options.miss) u8 else u9;
|
|
2821
|
+
fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) !bool {
|
|
2822
|
+
const acc: uN = if (pkmn.options.miss) accuracy else @as(uN, accuracy) + 1;
|
|
2736
2823
|
const ok = if (options.calc.overridden(player, .hit)) |val|
|
|
2737
2824
|
val == .true
|
|
2738
2825
|
else if (@hasDecl(@TypeOf(battle.rng), "hit"))
|
|
2739
|
-
battle.rng.hit(player,
|
|
2826
|
+
battle.rng.hit(player, acc)
|
|
2740
2827
|
else if (showdown)
|
|
2741
|
-
battle.rng.chance(
|
|
2828
|
+
battle.rng.chance(uN, acc, 256)
|
|
2742
2829
|
else
|
|
2743
|
-
battle.rng.next() <
|
|
2830
|
+
battle.rng.next() < acc;
|
|
2744
2831
|
|
|
2745
|
-
options.chance.hit(ok,
|
|
2832
|
+
try options.chance.hit(player, ok, acc);
|
|
2746
2833
|
return ok;
|
|
2747
2834
|
}
|
|
2748
2835
|
|
|
@@ -2874,9 +2961,10 @@ pub const Rolls = struct {
|
|
|
2874
2961
|
}
|
|
2875
2962
|
|
|
2876
2963
|
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
|
|
2964
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2965
|
+
if (val >= 1 and val <= 7) @as(u3, @intCast(val)) else null
|
|
2966
|
+
else
|
|
2967
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
|
|
2880
2968
|
battle.rng.sleepDuration(player)
|
|
2881
2969
|
else if (showdown)
|
|
2882
2970
|
battle.rng.range(u3, 1, 8)
|
|
@@ -2888,14 +2976,15 @@ pub const Rolls = struct {
|
|
|
2888
2976
|
};
|
|
2889
2977
|
|
|
2890
2978
|
assert(duration >= 1 and duration <= 7);
|
|
2891
|
-
options.chance.duration(
|
|
2979
|
+
options.chance.duration(player, duration);
|
|
2892
2980
|
return duration;
|
|
2893
2981
|
}
|
|
2894
2982
|
|
|
2895
2983
|
fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
|
|
2896
|
-
const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
|
|
2897
|
-
val
|
|
2898
|
-
else
|
|
2984
|
+
const duration: u4 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2985
|
+
if (val >= 1 and val <= 8) val else null
|
|
2986
|
+
else
|
|
2987
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
|
|
2899
2988
|
battle.rng.disableDuration(player)
|
|
2900
2989
|
else if (showdown)
|
|
2901
2990
|
battle.rng.range(u4, 1, 9)
|
|
@@ -2903,29 +2992,31 @@ pub const Rolls = struct {
|
|
|
2903
2992
|
@intCast((battle.rng.next() & 7) + 1);
|
|
2904
2993
|
|
|
2905
2994
|
assert(duration >= 1 and duration <= 8);
|
|
2906
|
-
options.chance.duration(
|
|
2995
|
+
options.chance.duration(player, duration);
|
|
2907
2996
|
return duration;
|
|
2908
2997
|
}
|
|
2909
2998
|
|
|
2910
|
-
fn confusionDuration(battle: anytype, player: Player,
|
|
2911
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2912
|
-
@intCast(val)
|
|
2913
|
-
else
|
|
2914
|
-
battle.rng
|
|
2999
|
+
fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
3000
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
3001
|
+
if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
|
|
3002
|
+
else
|
|
3003
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
|
|
3004
|
+
battle.rng.confusionDuration(player)
|
|
2915
3005
|
else if (showdown)
|
|
2916
3006
|
battle.rng.range(u3, 2, 6)
|
|
2917
3007
|
else
|
|
2918
3008
|
@intCast((battle.rng.next() & 3) + 2);
|
|
2919
3009
|
|
|
2920
3010
|
assert(duration >= 2 and duration <= 5);
|
|
2921
|
-
options.chance.duration(
|
|
3011
|
+
options.chance.duration(player, duration);
|
|
2922
3012
|
return duration;
|
|
2923
3013
|
}
|
|
2924
3014
|
|
|
2925
3015
|
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
|
|
3016
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
3017
|
+
if (val >= 2 and val <= 3) @as(u3, @intCast(val)) else null
|
|
3018
|
+
else
|
|
3019
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
|
|
2929
3020
|
battle.rng.attackingDuration(player)
|
|
2930
3021
|
else if (showdown)
|
|
2931
3022
|
battle.rng.range(u3, 2, 4)
|
|
@@ -2933,7 +3024,7 @@ pub const Rolls = struct {
|
|
|
2933
3024
|
@intCast((battle.rng.next() & 1) + 2);
|
|
2934
3025
|
|
|
2935
3026
|
assert(duration >= 2 and duration <= 3);
|
|
2936
|
-
options.chance.duration(
|
|
3027
|
+
options.chance.duration(player, duration);
|
|
2937
3028
|
return duration;
|
|
2938
3029
|
}
|
|
2939
3030
|
|
|
@@ -2946,9 +3037,10 @@ pub const Rolls = struct {
|
|
|
2946
3037
|
options: anytype,
|
|
2947
3038
|
) !u3 {
|
|
2948
3039
|
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
|
|
3040
|
+
const n: u3 = (if (options.calc.overridden(player, roll)) |val|
|
|
3041
|
+
if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
|
|
3042
|
+
else
|
|
3043
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "distribution"))
|
|
2952
3044
|
battle.rng.distribution(player)
|
|
2953
3045
|
else if (showdown)
|
|
2954
3046
|
DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
|
|
@@ -2959,7 +3051,7 @@ pub const Rolls = struct {
|
|
|
2959
3051
|
|
|
2960
3052
|
assert(n >= 2 and n <= 5);
|
|
2961
3053
|
if (effect == .Binding) {
|
|
2962
|
-
options.chance.duration(
|
|
3054
|
+
options.chance.duration(player, n);
|
|
2963
3055
|
} else {
|
|
2964
3056
|
try options.chance.multiHit(player, n);
|
|
2965
3057
|
}
|
|
@@ -2974,15 +3066,13 @@ pub const Rolls = struct {
|
|
|
2974
3066
|
options: anytype,
|
|
2975
3067
|
) !u4 {
|
|
2976
3068
|
// TODO: consider throwing error instead of rerolling?
|
|
2977
|
-
const
|
|
3069
|
+
const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
|
|
2978
3070
|
if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
|
|
2979
3071
|
val
|
|
2980
3072
|
else
|
|
2981
3073
|
null
|
|
2982
3074
|
else
|
|
2983
|
-
null
|
|
2984
|
-
|
|
2985
|
-
const slot: u4 = overridden orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
|
|
3075
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
|
|
2986
3076
|
battle.rng.moveSlot(player, moves, check_pp)
|
|
2987
3077
|
else slot: {
|
|
2988
3078
|
if (showdown) {
|
|
@@ -3068,7 +3158,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3068
3158
|
const stored = side.stored();
|
|
3069
3159
|
|
|
3070
3160
|
// 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
|
|
3161
|
+
// Pokémon Showdown instead produces a list with a single move that must be chosen
|
|
3072
3162
|
//
|
|
3073
3163
|
// Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
|
|
3074
3164
|
// here when not in compatibility mode, but the engine needs to be able to differentiate
|
|
@@ -3076,7 +3166,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3076
3166
|
// passing due to being forced into using a move. Instead of introducing another option
|
|
3077
3167
|
// we simply repurpose Move with no move slot, even though pedantically this is not
|
|
3078
3168
|
// strictly correct as the player would not have been presented the option to move or
|
|
3079
|
-
// switch at all
|
|
3169
|
+
// switch at all
|
|
3080
3170
|
if (isForced(active)) {
|
|
3081
3171
|
out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
|
|
3082
3172
|
n += 1;
|
|
@@ -3095,7 +3185,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3095
3185
|
// On the cartridge, all of these happen after "FIGHT" (indicating you are not
|
|
3096
3186
|
// switching) but before you are allowed to select a move. Pokémon Showdown instead
|
|
3097
3187
|
// either disables all other moves in the case of limited or requires you to select a
|
|
3098
|
-
// move normally if sleeping/frozen/bound
|
|
3188
|
+
// move normally if sleeping/frozen/bound
|
|
3099
3189
|
if (!showdown and (limited or foe.active.volatiles.Binding or
|
|
3100
3190
|
Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
|
|
3101
3191
|
{
|
|
@@ -3108,7 +3198,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3108
3198
|
// Pokémon Showdown handles Bide and Binding moves by checking if the move in question
|
|
3109
3199
|
// is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
|
|
3110
3200
|
// 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
|
|
3201
|
+
// proc-ed move in their moveset) and disabling all other moves
|
|
3112
3202
|
if (limited) {
|
|
3113
3203
|
assert(showdown);
|
|
3114
3204
|
assert(side.last_selected_move != .None);
|
|
@@ -3140,7 +3230,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3140
3230
|
// Struggle (Pokémon Showdown would use 'move 1' here)
|
|
3141
3231
|
if (n == before) {
|
|
3142
3232
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
3143
|
-
if (!
|
|
3233
|
+
if (!pkmn.options.mod) {
|
|
3144
3234
|
while (slot <= 4) : (slot += 1) {
|
|
3145
3235
|
if (active.moves[slot - 1].pp != 0) return n;
|
|
3146
3236
|
}
|