@pkmn/engine 0.1.0-dev.c544e10e → 0.1.0-dev.cb3978dd
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/LICENSE +1 -3
- package/README.md +41 -14
- 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 +35 -58
- 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 +27 -5
- 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 +55 -13
- 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 +109 -107
- package/build/pkg/protocol.js.map +1 -1
- package/build/tools/debug.js +36 -70
- 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 +253 -89
- package/package.json +58 -30
- package/src/bin/install-pkmn-engine +29 -19
- package/src/bin/pkmn-debug +1 -1
- package/src/lib/common/array.zig +77 -0
- package/src/lib/common/data.zig +4 -4
- package/src/lib/common/debug.zig +39 -8
- package/src/lib/common/js.zig +4 -2
- package/src/lib/common/optional.zig +14 -10
- package/src/lib/common/options.zig +6 -0
- package/src/lib/common/protocol.zig +75 -79
- package/src/lib/common/rational.zig +128 -32
- package/src/lib/common/rng.zig +7 -9
- package/src/lib/common/util.zig +50 -1
- package/src/lib/gen1/calc.zig +686 -159
- package/src/lib/gen1/chance.zig +405 -280
- package/src/lib/gen1/data/moves.zig +2 -4
- package/src/lib/gen1/data/species.zig +2 -4
- package/src/lib/gen1/data/types.zig +1 -2
- package/src/lib/gen1/data.zig +24 -27
- package/src/lib/gen1/helpers.zig +92 -548
- package/src/lib/gen1/mechanics.zig +373 -257
- package/src/lib/gen2/calc.zig +37 -27
- package/src/lib/gen2/chance.zig +84 -496
- 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 +19 -22
- package/src/lib/gen2/generated.zig +19 -26
- package/src/lib/gen2/helpers.zig +46 -25
- package/src/lib/gen2/mechanics.zig +200 -157
- package/src/lib/{binding/node.zig → node.zig} +8 -13
- package/src/lib/pkmn.zig +36 -15
- package/src/lib/{binding/wasm.zig → wasm.zig} +1 -1
- 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/lib/gen1/README.md +0 -894
- package/src/lib/gen2/README.md +0 -329
- 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;
|
|
@@ -71,8 +62,8 @@ pub fn update(battle: anytype, c1: Choice, c2: Choice, options: anytype) !Result
|
|
|
71
62
|
var p1 = battle.side(.P1);
|
|
72
63
|
var p2 = battle.side(.P2);
|
|
73
64
|
|
|
74
|
-
|
|
75
|
-
|
|
65
|
+
const r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
|
|
66
|
+
const r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
|
|
76
67
|
|
|
77
68
|
if (try turnOrder(battle, c1, c2, options) == .P1) {
|
|
78
69
|
if (try doTurn(battle, .P1, c1, r1, s1, .P2, c2, r2, s2, options)) |r| return r;
|
|
@@ -90,11 +81,11 @@ fn start(battle: anytype, options: anytype) !Result {
|
|
|
90
81
|
const p1 = battle.side(.P1);
|
|
91
82
|
const p2 = battle.side(.P2);
|
|
92
83
|
|
|
93
|
-
|
|
84
|
+
const p1_slot = findFirstAlive(p1);
|
|
94
85
|
assert(!showdown or p1_slot == 1);
|
|
95
86
|
if (p1_slot == 0) return if (findFirstAlive(p2) == 0) Result.Tie else Result.Lose;
|
|
96
87
|
|
|
97
|
-
|
|
88
|
+
const p2_slot = findFirstAlive(p2);
|
|
98
89
|
assert(!showdown or p2_slot == 1);
|
|
99
90
|
if (p2_slot == 0) return Result.Win;
|
|
100
91
|
|
|
@@ -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,14 +665,8 @@ fn beforeMove(
|
|
|
635
665
|
volatiles.state +%= battle.last_damage;
|
|
636
666
|
}
|
|
637
667
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
if (options.calc.modify(player, .attacking)) |extend| {
|
|
641
|
-
if (!extend) volatiles.attacks = 0;
|
|
642
|
-
} else {
|
|
643
|
-
volatiles.attacks -= 1;
|
|
644
|
-
}
|
|
645
|
-
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);
|
|
646
670
|
|
|
647
671
|
if (volatiles.attacks != 0) {
|
|
648
672
|
try log.activate(.{ ident, .Bide });
|
|
@@ -676,39 +700,40 @@ fn beforeMove(
|
|
|
676
700
|
}
|
|
677
701
|
|
|
678
702
|
if (volatiles.Thrashing) {
|
|
679
|
-
|
|
703
|
+
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
680
704
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
try options.chance.attacking(player, volatiles.attacks);
|
|
705
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
706
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
707
|
+
|
|
708
|
+
if (volatiles.attacks == 0) {
|
|
709
|
+
const overwritten = volatiles.Confusion;
|
|
687
710
|
|
|
688
|
-
|
|
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)
|
|
689
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);
|
|
690
725
|
}
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
if (handleThrashing(battle, active, player, options)) {
|
|
697
|
-
try log.start(.{ battle.active(player), .ConfusionSilent });
|
|
698
|
-
}
|
|
699
|
-
}
|
|
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
|
+
|
|
700
731
|
return .skip_can;
|
|
701
732
|
}
|
|
702
733
|
|
|
703
734
|
if (volatiles.Binding) {
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
if (options.calc.modify(player, .binding)) |extend| {
|
|
707
|
-
if (!extend) volatiles.attacks = 0;
|
|
708
|
-
} else {
|
|
709
|
-
volatiles.attacks -= 1;
|
|
710
|
-
}
|
|
711
|
-
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);
|
|
712
737
|
|
|
713
738
|
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
714
739
|
if (showdown or battle.last_damage != 0) {
|
|
@@ -738,7 +763,6 @@ fn canMove(
|
|
|
738
763
|
const player_ident = battle.active(player);
|
|
739
764
|
const move = Move.get(side.last_selected_move);
|
|
740
765
|
|
|
741
|
-
var skip = skip_pp;
|
|
742
766
|
if (side.active.volatiles.Charging) {
|
|
743
767
|
side.active.volatiles.Charging = false;
|
|
744
768
|
side.active.volatiles.Invulnerable = false;
|
|
@@ -750,7 +774,7 @@ fn canMove(
|
|
|
750
774
|
}
|
|
751
775
|
|
|
752
776
|
side.last_used_move = side.last_selected_move;
|
|
753
|
-
if (!
|
|
777
|
+
if (!skip_pp) decrementPP(side, mslot, auto);
|
|
754
778
|
|
|
755
779
|
const target = if (move.target == .Self) player else player.foe();
|
|
756
780
|
try options.log.move(.{ player_ident, side.last_selected_move, battle.active(target), from });
|
|
@@ -763,9 +787,14 @@ fn canMove(
|
|
|
763
787
|
|
|
764
788
|
if (move.effect == .Thrashing) {
|
|
765
789
|
Effects.thrashing(battle, player, options);
|
|
766
|
-
} else if (
|
|
767
|
-
// Pokémon Showdown handles this after hit/miss checks and damage calculation
|
|
768
|
-
|
|
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
|
+
}
|
|
769
798
|
}
|
|
770
799
|
|
|
771
800
|
return true;
|
|
@@ -802,7 +831,7 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
|
|
|
802
831
|
assert(active.move(mslot).pp == side.stored().move(mslot).pp);
|
|
803
832
|
}
|
|
804
833
|
|
|
805
|
-
fn incrementPP(side: *Side, mslot: u4) void {
|
|
834
|
+
fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
|
|
806
835
|
var active = &side.active;
|
|
807
836
|
const volatiles = &active.volatiles;
|
|
808
837
|
|
|
@@ -810,8 +839,11 @@ fn incrementPP(side: *Side, mslot: u4) void {
|
|
|
810
839
|
// GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
|
|
811
840
|
if (showdown and volatiles.Transform) return;
|
|
812
841
|
|
|
842
|
+
const n = options.calc.overridden(player, .pp) orelse 1;
|
|
843
|
+
if (volatiles.Transform and !options.chance.pp(player, n)) return;
|
|
844
|
+
|
|
813
845
|
assert(mslot > 0 and mslot <= 4);
|
|
814
|
-
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;
|
|
815
847
|
}
|
|
816
848
|
|
|
817
849
|
// Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
|
|
@@ -859,13 +891,13 @@ fn doMove(
|
|
|
859
891
|
break :miss true;
|
|
860
892
|
}
|
|
861
893
|
break :miss (move.target != .Self and
|
|
862
|
-
!moveHit(battle, player, move, &immune, &mist, options));
|
|
894
|
+
!try moveHit(battle, player, move, &immune, &mist, options));
|
|
863
895
|
};
|
|
864
896
|
assert(!immune or miss or (showdown and move.effect == .Binding));
|
|
865
897
|
|
|
866
898
|
var late = showdown and move.effect != .Explode;
|
|
867
899
|
const skip = status or immune;
|
|
868
|
-
if ((!showdown or (!skip or counter)) and !miss)
|
|
900
|
+
if ((!showdown or (!skip or counter)) and !miss) damage: {
|
|
869
901
|
if (showdown and move.effect.isMulti()) {
|
|
870
902
|
try Effects.multiHit(battle, player, move, options);
|
|
871
903
|
hits = side.active.volatiles.attacks;
|
|
@@ -887,8 +919,9 @@ fn doMove(
|
|
|
887
919
|
ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
|
|
888
920
|
// This can overflow after adjustDamage, but will still be sufficient to OHKO
|
|
889
921
|
battle.last_damage = if (ohko) 65535 else 0;
|
|
890
|
-
if (showdown) break :
|
|
891
|
-
} 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);
|
|
892
925
|
return @as(?Result, Result.Error);
|
|
893
926
|
}
|
|
894
927
|
if (battle.last_damage == 0) {
|
|
@@ -902,11 +935,12 @@ fn doMove(
|
|
|
902
935
|
}
|
|
903
936
|
}
|
|
904
937
|
|
|
905
|
-
var
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
938
|
+
var missed: bool = undefined;
|
|
939
|
+
const zero = battle.last_damage == 0;
|
|
940
|
+
if (!showdown and !skip) {
|
|
941
|
+
missed = !try moveHit(battle, player, move, &immune, &mist, options);
|
|
942
|
+
miss = missed or zero;
|
|
943
|
+
}
|
|
910
944
|
|
|
911
945
|
assert(showdown or miss or battle.last_damage > 0 or skip);
|
|
912
946
|
assert((!showdown and miss) or !(ohko and immune));
|
|
@@ -933,11 +967,32 @@ fn doMove(
|
|
|
933
967
|
try log.immune(.{ foe_ident, .OHKO });
|
|
934
968
|
} else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
|
|
935
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
|
+
}
|
|
936
991
|
} else if (mist) {
|
|
937
992
|
if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
|
|
938
993
|
try log.fail(.{ foe_ident, .None });
|
|
939
994
|
} else {
|
|
940
|
-
if (showdown
|
|
995
|
+
if (!showdown) try options.chance.commit(player, if (zero and !missed) .hit else .miss);
|
|
941
996
|
try log.lastmiss(.{});
|
|
942
997
|
try log.miss(.{battle.active(player)});
|
|
943
998
|
}
|
|
@@ -952,17 +1007,15 @@ fn doMove(
|
|
|
952
1007
|
try buildRage(battle, player.foe(), options);
|
|
953
1008
|
} else if (showdown and move.effect == .Disable) {
|
|
954
1009
|
try buildRage(battle, player.foe(), options);
|
|
955
|
-
} else if (!showdown and immune and move.effect == .Binding) {
|
|
956
|
-
try options.chance.commit(player, .binding);
|
|
957
1010
|
}
|
|
958
1011
|
return null;
|
|
959
|
-
} else {
|
|
1012
|
+
} else if (!showdown) {
|
|
960
1013
|
try options.chance.commit(player, .hit);
|
|
961
1014
|
}
|
|
962
1015
|
|
|
963
1016
|
// On the cartridge MultiHit doesn't get set up until after damage has been applied for the
|
|
964
1017
|
// first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
|
|
965
|
-
// it up above before damage calculation)
|
|
1018
|
+
// it up above before damage calculation)
|
|
966
1019
|
if (!showdown and move.effect.isMulti()) {
|
|
967
1020
|
try Effects.multiHit(battle, player, move, options);
|
|
968
1021
|
hits = side.active.volatiles.attacks;
|
|
@@ -1001,32 +1054,30 @@ fn doMove(
|
|
|
1001
1054
|
try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
1002
1055
|
}
|
|
1003
1056
|
try log.hitcount(.{ battle.active(player.foe()), hit });
|
|
1004
|
-
} else if (showdown) {
|
|
1057
|
+
} else if (showdown and move.effect == .Binding) {
|
|
1005
1058
|
// This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
try buildRage(battle, player.foe(), options);
|
|
1017
|
-
}
|
|
1018
|
-
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);
|
|
1019
1069
|
}
|
|
1070
|
+
return null;
|
|
1020
1071
|
}
|
|
1021
1072
|
}
|
|
1022
1073
|
|
|
1023
1074
|
// Substitute being broken nullifies the move's effect completely so even
|
|
1024
|
-
// 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
|
|
1025
1076
|
if (nullified) return null;
|
|
1026
1077
|
|
|
1027
1078
|
// On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
|
|
1028
1079
|
// but this is only done to setup the MultiHit looping in the first place. Moving the MultiHit
|
|
1029
|
-
// 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
|
|
1030
1081
|
if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
|
|
1031
1082
|
|
|
1032
1083
|
if (foe.stored().hp == 0) return null;
|
|
@@ -1041,7 +1092,7 @@ fn doMove(
|
|
|
1041
1092
|
// below). For Twineedle we change the data to that of one with PoisonChance1 given its
|
|
1042
1093
|
// MultiHit behavior is complete after the loop above, though Pokémon Showdown handles the
|
|
1043
1094
|
// Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
|
|
1044
|
-
// puts the |-status| message before |-hitcount| instead of after
|
|
1095
|
+
// puts the |-status| message before |-hitcount| instead of after
|
|
1045
1096
|
if (move.effect == .Twineedle) {
|
|
1046
1097
|
if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
1047
1098
|
} else if (move.effect == .Disable) {
|
|
@@ -1060,12 +1111,14 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
|
|
|
1060
1111
|
const side = battle.side(player);
|
|
1061
1112
|
|
|
1062
1113
|
// Base speed is used for the critical hit calculation, even when Transform-ed
|
|
1063
|
-
var rate =
|
|
1114
|
+
var rate: u16 = Species.chance(side.stored().species);
|
|
1064
1115
|
// GLITCH: Focus Energy reduces critical hit rate instead of increasing it
|
|
1065
1116
|
rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
|
|
1066
1117
|
rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
|
|
1067
1118
|
|
|
1068
|
-
|
|
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);
|
|
1069
1122
|
}
|
|
1070
1123
|
|
|
1071
1124
|
fn calcDamage(
|
|
@@ -1074,6 +1127,7 @@ fn calcDamage(
|
|
|
1074
1127
|
target_player: Player,
|
|
1075
1128
|
m: ?Move.Data,
|
|
1076
1129
|
crit: bool,
|
|
1130
|
+
options: anytype,
|
|
1077
1131
|
) bool {
|
|
1078
1132
|
// Confusion (indicated when m == null) just needs a 40 BP physical move
|
|
1079
1133
|
const cfz = m == null;
|
|
@@ -1085,28 +1139,8 @@ fn calcDamage(
|
|
|
1085
1139
|
|
|
1086
1140
|
const special = move.type.special();
|
|
1087
1141
|
|
|
1088
|
-
|
|
1089
|
-
var
|
|
1090
|
-
if (crit)
|
|
1091
|
-
if (special) side.stored().stats.spc
|
|
1092
|
-
else side.stored().stats.atk
|
|
1093
|
-
else
|
|
1094
|
-
if (special) side.active.stats.spc
|
|
1095
|
-
else side.active.stats.atk;
|
|
1096
|
-
var def: u32 =
|
|
1097
|
-
if (crit)
|
|
1098
|
-
if (special) target.stored().stats.spc
|
|
1099
|
-
else target.stored().stats.def
|
|
1100
|
-
else
|
|
1101
|
-
// GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
|
|
1102
|
-
if (special)
|
|
1103
|
-
target.active.stats.spc *
|
|
1104
|
-
@as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
|
|
1105
|
-
// Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
|
|
1106
|
-
else
|
|
1107
|
-
target.active.stats.def *
|
|
1108
|
-
@as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
|
|
1109
|
-
// zig fmt: on
|
|
1142
|
+
var atk = attack(side, crit, special);
|
|
1143
|
+
var def = defense(target, crit, special, cfz);
|
|
1110
1144
|
|
|
1111
1145
|
// Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
|
|
1112
1146
|
// will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
|
|
@@ -1114,12 +1148,12 @@ fn calcDamage(
|
|
|
1114
1148
|
if ((!showdown or !cfz) and (atk > 255 or def > 255)) {
|
|
1115
1149
|
atk = @max((atk / 4) & 255, 1);
|
|
1116
1150
|
// GLITCH: not adjusted to be a min of 1 on cartridge (can lead to division-by-zero freeze)
|
|
1117
|
-
def = @max((def / 4) & 255, if (
|
|
1151
|
+
def = @max((def / 4) & 255, if (pkmn.options.mod) 1 else 0);
|
|
1118
1152
|
}
|
|
1119
1153
|
|
|
1120
|
-
const lvl =
|
|
1154
|
+
const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
|
|
1121
1155
|
|
|
1122
|
-
def =
|
|
1156
|
+
def = if (move.effect == .Explode) @max(def / 2, 1) else def;
|
|
1123
1157
|
|
|
1124
1158
|
if (def == 0) return false;
|
|
1125
1159
|
|
|
@@ -1131,11 +1165,41 @@ fn calcDamage(
|
|
|
1131
1165
|
d = @min(997, d);
|
|
1132
1166
|
d += 2;
|
|
1133
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
|
+
|
|
1134
1178
|
battle.last_damage = @intCast(d);
|
|
1135
1179
|
|
|
1136
1180
|
return true;
|
|
1137
1181
|
}
|
|
1138
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
|
+
|
|
1139
1203
|
fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
1140
1204
|
const side = battle.side(player);
|
|
1141
1205
|
const foe = battle.foe(player);
|
|
@@ -1180,7 +1244,6 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1180
1244
|
const foe = battle.foe(player);
|
|
1181
1245
|
|
|
1182
1246
|
if (!try checkHit(battle, player, move, options)) return null;
|
|
1183
|
-
try options.chance.commit(player, .hit);
|
|
1184
1247
|
|
|
1185
1248
|
battle.last_damage = switch (side.last_selected_move) {
|
|
1186
1249
|
.SuperFang => @max(foe.stored().hp / 2, 1),
|
|
@@ -1191,13 +1254,13 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1191
1254
|
.Psywave => power: {
|
|
1192
1255
|
const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
|
|
1193
1256
|
// GLITCH: Psywave infinite glitch loop
|
|
1194
|
-
if (!
|
|
1257
|
+
if (!pkmn.options.mod and max <= 1) return Result.Error;
|
|
1195
1258
|
break :power try Rolls.psywave(battle, player, max, options);
|
|
1196
1259
|
},
|
|
1197
1260
|
else => unreachable,
|
|
1198
1261
|
};
|
|
1199
1262
|
|
|
1200
|
-
if (battle.last_damage == 0) return if (
|
|
1263
|
+
if (battle.last_damage == 0) return if (pkmn.options.mod) null else Result.Error;
|
|
1201
1264
|
|
|
1202
1265
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1203
1266
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
@@ -1234,16 +1297,15 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1234
1297
|
|
|
1235
1298
|
if (!used or !selected) {
|
|
1236
1299
|
// GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
|
|
1237
|
-
if (!
|
|
1300
|
+
if (!pkmn.options.mod) return Result.Error;
|
|
1238
1301
|
try options.log.fail(.{ battle.active(player), .None });
|
|
1239
1302
|
return null;
|
|
1240
1303
|
}
|
|
1241
1304
|
|
|
1242
1305
|
battle.last_damage *|= 2;
|
|
1243
1306
|
|
|
1244
|
-
// Pokémon Showdown calls
|
|
1307
|
+
// Pokémon Showdown calls moveHit before Counter
|
|
1245
1308
|
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
1246
|
-
try options.chance.commit(player, .hit);
|
|
1247
1309
|
|
|
1248
1310
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1249
1311
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
@@ -1272,7 +1334,7 @@ fn applyDamage(
|
|
|
1272
1334
|
subbed.active.volatiles.Substitute = false;
|
|
1273
1335
|
// battle.last_damage is not updated with the amount of HP the Substitute had
|
|
1274
1336
|
try options.log.end(.{ battle.active(sub_player), .Substitute });
|
|
1275
|
-
options.calc.capped(
|
|
1337
|
+
if (reason != .Confusion) options.calc.capped(sub_player);
|
|
1276
1338
|
return true;
|
|
1277
1339
|
} else {
|
|
1278
1340
|
// Safe to truncate since less than subbed.volatiles.substitute which is a u8
|
|
@@ -1306,7 +1368,7 @@ fn mirrorMove(
|
|
|
1306
1368
|
try options.log.fail(.{ battle.active(player), .None });
|
|
1307
1369
|
return null;
|
|
1308
1370
|
} else if (!showdown or foe.last_used_move != .Struggle) {
|
|
1309
|
-
incrementPP(side, mslot);
|
|
1371
|
+
incrementPP(side, player, mslot, options);
|
|
1310
1372
|
}
|
|
1311
1373
|
|
|
1312
1374
|
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
|
|
@@ -1327,7 +1389,7 @@ fn metronome(
|
|
|
1327
1389
|
var side = battle.side(player);
|
|
1328
1390
|
|
|
1329
1391
|
side.last_selected_move = try Rolls.metronome(battle, player, options);
|
|
1330
|
-
incrementPP(side, mslot);
|
|
1392
|
+
incrementPP(side, player, mslot, options);
|
|
1331
1393
|
|
|
1332
1394
|
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
|
|
1333
1395
|
return null;
|
|
@@ -1339,7 +1401,10 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
|
|
|
1339
1401
|
var immune = false;
|
|
1340
1402
|
var mist = false;
|
|
1341
1403
|
|
|
1342
|
-
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
|
+
}
|
|
1343
1408
|
|
|
1344
1409
|
assert(!immune);
|
|
1345
1410
|
if (mist) {
|
|
@@ -1348,11 +1413,11 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
|
|
|
1348
1413
|
try options.log.activate(.{ foe_ident, .Mist });
|
|
1349
1414
|
try options.log.fail(.{ foe_ident, .None });
|
|
1350
1415
|
} else {
|
|
1351
|
-
try options.chance.commit(player, .miss);
|
|
1352
1416
|
try options.log.lastmiss(.{});
|
|
1353
1417
|
try options.log.miss(.{battle.active(player)});
|
|
1354
1418
|
}
|
|
1355
1419
|
|
|
1420
|
+
if (!showdown) try options.chance.commit(player, .miss);
|
|
1356
1421
|
return false;
|
|
1357
1422
|
}
|
|
1358
1423
|
|
|
@@ -1363,7 +1428,7 @@ fn moveHit(
|
|
|
1363
1428
|
immune: *bool,
|
|
1364
1429
|
mist: *bool,
|
|
1365
1430
|
options: anytype,
|
|
1366
|
-
) bool {
|
|
1431
|
+
) !bool {
|
|
1367
1432
|
var side = battle.side(player);
|
|
1368
1433
|
const foe = battle.foe(player);
|
|
1369
1434
|
|
|
@@ -1402,7 +1467,7 @@ fn moveHit(
|
|
|
1402
1467
|
const overwritten = overwrite and state > 0;
|
|
1403
1468
|
assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
|
|
1404
1469
|
|
|
1405
|
-
var accuracy = if (overwritten) state else
|
|
1470
|
+
var accuracy: u16 = if (overwritten) state else move.accuracy;
|
|
1406
1471
|
var boost = BOOSTS[@as(u4, @intCast(@as(i8, side.active.boosts.accuracy) + 6))];
|
|
1407
1472
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1408
1473
|
boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
|
|
@@ -1414,7 +1479,7 @@ fn moveHit(
|
|
|
1414
1479
|
if (overwrite) side.active.volatiles.state = accuracy;
|
|
1415
1480
|
|
|
1416
1481
|
// GLITCH: max accuracy is 255 so 1/256 chance of miss
|
|
1417
|
-
break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
|
|
1482
|
+
break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
|
|
1418
1483
|
};
|
|
1419
1484
|
|
|
1420
1485
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1425,13 +1490,17 @@ fn moveHit(
|
|
|
1425
1490
|
|
|
1426
1491
|
if (!miss) return true;
|
|
1427
1492
|
if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
|
|
1493
|
+
|
|
1428
1494
|
side.active.volatiles.Binding = false;
|
|
1495
|
+
options.chance.observe(.binding, player, .None);
|
|
1496
|
+
|
|
1429
1497
|
return false;
|
|
1430
1498
|
}
|
|
1431
1499
|
|
|
1432
1500
|
fn checkFaint(
|
|
1433
1501
|
battle: anytype,
|
|
1434
1502
|
player: Player,
|
|
1503
|
+
cap: bool,
|
|
1435
1504
|
options: anytype,
|
|
1436
1505
|
) @TypeOf(options.log).Error!?Result {
|
|
1437
1506
|
const side = battle.side(player);
|
|
@@ -1444,8 +1513,8 @@ fn checkFaint(
|
|
|
1444
1513
|
const foe_out = foe_fainted and findFirstAlive(foe) == 0;
|
|
1445
1514
|
const more = player_out or foe_out;
|
|
1446
1515
|
|
|
1447
|
-
if (try faint(battle, player, !(more or foe_fainted), options)) |r| return r;
|
|
1448
|
-
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;
|
|
1449
1518
|
|
|
1450
1519
|
assert(!side.active.volatiles.MultiHit);
|
|
1451
1520
|
assert(!foe.active.volatiles.MultiHit);
|
|
@@ -1466,7 +1535,7 @@ fn checkFaint(
|
|
|
1466
1535
|
return .{ .p1 = foe_choice, .p2 = .Switch };
|
|
1467
1536
|
}
|
|
1468
1537
|
|
|
1469
|
-
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 {
|
|
1470
1539
|
var side = battle.side(player);
|
|
1471
1540
|
var foe = battle.foe(player);
|
|
1472
1541
|
assert(side.stored().hp == 0);
|
|
@@ -1475,7 +1544,7 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
|
|
|
1475
1544
|
assert(!foe_volatiles.MultiHit);
|
|
1476
1545
|
if (foe_volatiles.Bide) {
|
|
1477
1546
|
assert(!foe_volatiles.Thrashing and !foe_volatiles.Rage);
|
|
1478
|
-
foe_volatiles.state = if (
|
|
1547
|
+
foe_volatiles.state = if (pkmn.options.mod) 0 else foe_volatiles.state & 255;
|
|
1479
1548
|
if (foe_volatiles.state != 0) return Result.Error;
|
|
1480
1549
|
}
|
|
1481
1550
|
|
|
@@ -1500,7 +1569,9 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
|
|
|
1500
1569
|
}
|
|
1501
1570
|
|
|
1502
1571
|
try options.log.faint(.{ battle.active(player), done });
|
|
1503
|
-
|
|
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);
|
|
1504
1575
|
return null;
|
|
1505
1576
|
}
|
|
1506
1577
|
|
|
@@ -1564,12 +1635,12 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
|
|
|
1564
1635
|
|
|
1565
1636
|
battle.turn += 1;
|
|
1566
1637
|
|
|
1567
|
-
if (
|
|
1638
|
+
if (pkmn.options.mod and pkmn.options.ebc and checkEBC(battle)) {
|
|
1568
1639
|
try options.log.tie(.{});
|
|
1569
1640
|
return Result.Tie;
|
|
1570
1641
|
}
|
|
1571
1642
|
|
|
1572
|
-
if (
|
|
1643
|
+
if (pkmn.options.mod and battle.turn >= 1000) {
|
|
1573
1644
|
try options.log.tie(.{});
|
|
1574
1645
|
return Result.Tie;
|
|
1575
1646
|
} else if (battle.turn >= 65535) {
|
|
@@ -1660,17 +1731,6 @@ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
|
|
|
1660
1731
|
}
|
|
1661
1732
|
}
|
|
1662
1733
|
|
|
1663
|
-
fn handleThrashing(battle: anytype, active: *ActivePokemon, player: Player, options: anytype) bool {
|
|
1664
|
-
var volatiles = &active.volatiles;
|
|
1665
|
-
assert(volatiles.Thrashing);
|
|
1666
|
-
if (volatiles.attacks > 0) return false;
|
|
1667
|
-
|
|
1668
|
-
volatiles.Thrashing = false;
|
|
1669
|
-
volatiles.Confusion = true;
|
|
1670
|
-
volatiles.confusion = Rolls.confusionDuration(battle, player, true, options);
|
|
1671
|
-
return true;
|
|
1672
|
-
}
|
|
1673
|
-
|
|
1674
1734
|
fn onBegin(
|
|
1675
1735
|
battle: anytype,
|
|
1676
1736
|
player: Player,
|
|
@@ -1679,7 +1739,6 @@ fn onBegin(
|
|
|
1679
1739
|
residual: *bool,
|
|
1680
1740
|
options: anytype,
|
|
1681
1741
|
) !void {
|
|
1682
|
-
assert(move.effect.onBegin());
|
|
1683
1742
|
return switch (move.effect) {
|
|
1684
1743
|
.Confusion => Effects.confusion(battle, player, move, options),
|
|
1685
1744
|
.Conversion => Effects.conversion(battle, player, options),
|
|
@@ -1702,7 +1761,6 @@ fn onBegin(
|
|
|
1702
1761
|
}
|
|
1703
1762
|
|
|
1704
1763
|
fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1705
|
-
assert(move.effect.onEnd());
|
|
1706
1764
|
return switch (move.effect) {
|
|
1707
1765
|
.Bide => Effects.bide(battle, player, options),
|
|
1708
1766
|
// zig fmt: off
|
|
@@ -1724,7 +1782,6 @@ fn alwaysHappens(
|
|
|
1724
1782
|
residual: *bool,
|
|
1725
1783
|
options: anytype,
|
|
1726
1784
|
) !void {
|
|
1727
|
-
assert(move.effect.alwaysHappens());
|
|
1728
1785
|
return switch (move.effect) {
|
|
1729
1786
|
.DrainHP, .DreamEater => Effects.drainHP(battle, player, options),
|
|
1730
1787
|
.Explode => Effects.explode(battle, player),
|
|
@@ -1760,8 +1817,10 @@ pub const Effects = struct {
|
|
|
1760
1817
|
|
|
1761
1818
|
side.active.volatiles.Bide = true;
|
|
1762
1819
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1820
|
+
|
|
1763
1821
|
side.active.volatiles.state = 0;
|
|
1764
1822
|
side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
1823
|
+
options.chance.observe(.attacking, player, .started);
|
|
1765
1824
|
|
|
1766
1825
|
try options.log.start(.{ battle.active(player), .Bide });
|
|
1767
1826
|
}
|
|
@@ -1823,12 +1882,14 @@ pub const Effects = struct {
|
|
|
1823
1882
|
if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
1824
1883
|
if (!try checkHit(battle, player, move, options)) return;
|
|
1825
1884
|
}
|
|
1826
|
-
try options.chance.commit(player, .hit);
|
|
1827
1885
|
}
|
|
1828
1886
|
|
|
1829
1887
|
if (foe.active.volatiles.Confusion) return;
|
|
1830
1888
|
foe.active.volatiles.Confusion = true;
|
|
1831
|
-
|
|
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());
|
|
1832
1893
|
|
|
1833
1894
|
try options.log.start(.{ battle.active(player.foe()), .Confusion });
|
|
1834
1895
|
}
|
|
@@ -1856,10 +1917,7 @@ pub const Effects = struct {
|
|
|
1856
1917
|
const foe_ident = battle.active(player.foe());
|
|
1857
1918
|
|
|
1858
1919
|
// Pokémon Showdown handles hit/miss earlier in doMove
|
|
1859
|
-
if (!showdown)
|
|
1860
|
-
if (!try checkHit(battle, player, move, options)) return null;
|
|
1861
|
-
try options.chance.commit(player, .hit);
|
|
1862
|
-
}
|
|
1920
|
+
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
1863
1921
|
|
|
1864
1922
|
if (volatiles.disable_move != 0) {
|
|
1865
1923
|
try options.log.fail(.{ foe_ident, .None });
|
|
@@ -1877,18 +1935,18 @@ pub const Effects = struct {
|
|
|
1877
1935
|
|
|
1878
1936
|
// Technically this is still considered simply a "miss" on the cartridge,
|
|
1879
1937
|
// but diverging from Pokémon Showdown here would mostly just be pedantic
|
|
1880
|
-
if (n == 0) {
|
|
1938
|
+
if (n == 0 or (pkmn.options.mod and err)) {
|
|
1881
1939
|
try options.log.fail(.{ foe_ident, .None });
|
|
1882
1940
|
return null;
|
|
1883
1941
|
} else if (err) {
|
|
1884
1942
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
1885
|
-
assert(!showdown);
|
|
1886
1943
|
return Result.Error;
|
|
1887
1944
|
}
|
|
1888
1945
|
|
|
1889
1946
|
volatiles.disable_move =
|
|
1890
1947
|
@intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
|
|
1891
1948
|
volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
|
|
1949
|
+
options.chance.observe(.disable, player.foe(), .started);
|
|
1892
1950
|
|
|
1893
1951
|
const id = foe.active.move(volatiles.disable_move).id;
|
|
1894
1952
|
try options.log.start(.{ foe_ident, .Disable, id });
|
|
@@ -1959,7 +2017,7 @@ pub const Effects = struct {
|
|
|
1959
2017
|
if (foe.active.types.includes(move.type)) return;
|
|
1960
2018
|
if (!try Rolls.secondaryChance(battle, player, true, options)) return;
|
|
1961
2019
|
// Freeze Clause Mod
|
|
1962
|
-
if (
|
|
2020
|
+
if (pkmn.options.mod) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
1963
2021
|
|
|
1964
2022
|
foe_stored.status = Status.init(.FRZ);
|
|
1965
2023
|
// GLITCH: Hyper Beam recharging status is not cleared
|
|
@@ -1972,7 +2030,7 @@ pub const Effects = struct {
|
|
|
1972
2030
|
var side = battle.side(player);
|
|
1973
2031
|
var foe = battle.foe(player);
|
|
1974
2032
|
|
|
1975
|
-
|
|
2033
|
+
const side_stored = side.stored();
|
|
1976
2034
|
var foe_stored = foe.stored();
|
|
1977
2035
|
|
|
1978
2036
|
const player_ident = battle.active(player);
|
|
@@ -1995,22 +2053,24 @@ pub const Effects = struct {
|
|
|
1995
2053
|
if (p != player and Status.any(s.stored().status)) {
|
|
1996
2054
|
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
1997
2055
|
s.stored().status = 0;
|
|
2056
|
+
options.chance.observe(.sleep, p, .None);
|
|
1998
2057
|
} else if (showdown and s.stored().status == Status.TOX) {
|
|
1999
2058
|
s.stored().status = Status.init(.PSN);
|
|
2000
2059
|
try log.status(.{ battle.active(p), s.stored().status, .None });
|
|
2001
2060
|
}
|
|
2002
|
-
try clearVolatiles(battle, p,
|
|
2061
|
+
try clearVolatiles(battle, p, options);
|
|
2003
2062
|
}
|
|
2004
2063
|
} else {
|
|
2005
2064
|
if (Status.any(foe_stored.status)) {
|
|
2006
2065
|
if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
|
|
2007
2066
|
foe.last_selected_move = .SKIP_TURN;
|
|
2067
|
+
options.chance.observe(.sleep, player.foe(), .None);
|
|
2008
2068
|
}
|
|
2009
2069
|
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
2010
2070
|
foe_stored.status = 0;
|
|
2011
2071
|
}
|
|
2012
|
-
try clearVolatiles(battle, player,
|
|
2013
|
-
try clearVolatiles(battle, player.foe(),
|
|
2072
|
+
try clearVolatiles(battle, player, options);
|
|
2073
|
+
try clearVolatiles(battle, player.foe(), options);
|
|
2014
2074
|
}
|
|
2015
2075
|
}
|
|
2016
2076
|
|
|
@@ -2061,7 +2121,6 @@ pub const Effects = struct {
|
|
|
2061
2121
|
}
|
|
2062
2122
|
}
|
|
2063
2123
|
|
|
2064
|
-
try options.chance.commit(player, .hit);
|
|
2065
2124
|
foe.active.volatiles.LeechSeed = true;
|
|
2066
2125
|
|
|
2067
2126
|
try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
|
|
@@ -2102,12 +2161,10 @@ pub const Effects = struct {
|
|
|
2102
2161
|
if (!has_mimic) {
|
|
2103
2162
|
// Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
|
|
2104
2163
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2105
|
-
try options.chance.commit(player, .hit);
|
|
2106
2164
|
return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
2107
2165
|
}
|
|
2108
2166
|
}
|
|
2109
2167
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2110
|
-
try options.chance.commit(player, .hit);
|
|
2111
2168
|
|
|
2112
2169
|
const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
|
|
2113
2170
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
@@ -2161,7 +2218,6 @@ pub const Effects = struct {
|
|
|
2161
2218
|
if (immune) return log.immune(.{ foe_ident, .None });
|
|
2162
2219
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2163
2220
|
}
|
|
2164
|
-
try options.chance.commit(player, .hit);
|
|
2165
2221
|
|
|
2166
2222
|
foe_stored.status = Status.init(.PAR);
|
|
2167
2223
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
@@ -2229,7 +2285,6 @@ pub const Effects = struct {
|
|
|
2229
2285
|
|
|
2230
2286
|
if (move.effect == .Poison) {
|
|
2231
2287
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2232
|
-
try options.chance.commit(player, .hit);
|
|
2233
2288
|
} else {
|
|
2234
2289
|
if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
|
|
2235
2290
|
return;
|
|
@@ -2298,13 +2353,12 @@ pub const Effects = struct {
|
|
|
2298
2353
|
if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
|
|
2299
2354
|
});
|
|
2300
2355
|
}
|
|
2301
|
-
// If
|
|
2356
|
+
// If moveHit in doMove didn't return true Pokémon Showdown wouldn't be in here
|
|
2302
2357
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2303
|
-
try options.chance.commit(player, .hit);
|
|
2304
2358
|
}
|
|
2305
2359
|
|
|
2306
2360
|
// Sleep Clause Mod
|
|
2307
|
-
if (
|
|
2361
|
+
if (pkmn.options.mod) {
|
|
2308
2362
|
for (foe.pokemon) |p| {
|
|
2309
2363
|
if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
|
|
2310
2364
|
}
|
|
@@ -2312,6 +2366,8 @@ pub const Effects = struct {
|
|
|
2312
2366
|
foe.active.volatiles.Recharging = false;
|
|
2313
2367
|
|
|
2314
2368
|
foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
|
|
2369
|
+
options.chance.observe(.sleep, player.foe(), .started);
|
|
2370
|
+
|
|
2315
2371
|
const last = battle.side(player).last_selected_move;
|
|
2316
2372
|
try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
|
|
2317
2373
|
}
|
|
@@ -2353,9 +2409,7 @@ pub const Effects = struct {
|
|
|
2353
2409
|
// Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
|
|
2354
2410
|
if (showdown) {
|
|
2355
2411
|
if (battle.side(player).last_selected_move == .Teleport) return;
|
|
2356
|
-
|
|
2357
|
-
try options.chance.commit(player, .hit);
|
|
2358
|
-
}
|
|
2412
|
+
_ = try checkHit(battle, player, move, options);
|
|
2359
2413
|
battle.last_damage = 0;
|
|
2360
2414
|
} else {
|
|
2361
2415
|
try options.log.fail(.{ battle.active(player), .None });
|
|
@@ -2370,6 +2424,8 @@ pub const Effects = struct {
|
|
|
2370
2424
|
|
|
2371
2425
|
volatiles.Thrashing = true;
|
|
2372
2426
|
volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
2427
|
+
|
|
2428
|
+
options.chance.observe(.attacking, player, .started);
|
|
2373
2429
|
}
|
|
2374
2430
|
|
|
2375
2431
|
fn transform(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -2408,13 +2464,14 @@ pub const Effects = struct {
|
|
|
2408
2464
|
if (side.active.volatiles.Binding) return;
|
|
2409
2465
|
side.active.volatiles.Binding = true;
|
|
2410
2466
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2411
|
-
// (Pokémon Showdown
|
|
2467
|
+
// (Pokémon Showdown clears Recharging in onTryMove instead)
|
|
2412
2468
|
if (!showdown) {
|
|
2413
2469
|
foe.active.volatiles.Recharging = false;
|
|
2414
2470
|
} else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
|
|
2415
2471
|
|
|
2416
2472
|
side.active.volatiles.attacks =
|
|
2417
2473
|
try Rolls.distribution(battle, .Binding, player, options) - 1;
|
|
2474
|
+
options.chance.observe(.binding, player, .started);
|
|
2418
2475
|
}
|
|
2419
2476
|
|
|
2420
2477
|
fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2441,7 +2498,7 @@ pub const Effects = struct {
|
|
|
2441
2498
|
}
|
|
2442
2499
|
return log.fail(.{ ident, .None });
|
|
2443
2500
|
}
|
|
2444
|
-
|
|
2501
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
|
|
2445
2502
|
const stat = unmodifiedStats(battle, side).atk;
|
|
2446
2503
|
stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2447
2504
|
try log.boost(.{ ident, reason, n });
|
|
@@ -2461,7 +2518,7 @@ pub const Effects = struct {
|
|
|
2461
2518
|
}
|
|
2462
2519
|
return log.fail(.{ ident, .None });
|
|
2463
2520
|
}
|
|
2464
|
-
|
|
2521
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
|
|
2465
2522
|
const stat = unmodifiedStats(battle, side).def;
|
|
2466
2523
|
stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2467
2524
|
try log.boost(.{ ident, .Defense, n });
|
|
@@ -2478,7 +2535,7 @@ pub const Effects = struct {
|
|
|
2478
2535
|
}
|
|
2479
2536
|
return log.fail(.{ ident, .None });
|
|
2480
2537
|
}
|
|
2481
|
-
|
|
2538
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
|
|
2482
2539
|
const stat = unmodifiedStats(battle, side).spe;
|
|
2483
2540
|
stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2484
2541
|
try log.boost(.{ ident, .Speed, 2 });
|
|
@@ -2498,7 +2555,7 @@ pub const Effects = struct {
|
|
|
2498
2555
|
}
|
|
2499
2556
|
return log.fail(.{ ident, .None });
|
|
2500
2557
|
}
|
|
2501
|
-
|
|
2558
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
|
|
2502
2559
|
const stat = unmodifiedStats(battle, side).spc;
|
|
2503
2560
|
stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2504
2561
|
try log.boost(.{ ident, .SpecialAttack, n });
|
|
@@ -2531,7 +2588,6 @@ pub const Effects = struct {
|
|
|
2531
2588
|
} else {
|
|
2532
2589
|
// checkHit already checks for Invulnerable
|
|
2533
2590
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2534
|
-
try options.chance.commit(player, .hit);
|
|
2535
2591
|
}
|
|
2536
2592
|
|
|
2537
2593
|
var stats = &foe.active.stats;
|
|
@@ -2551,7 +2607,7 @@ pub const Effects = struct {
|
|
|
2551
2607
|
}
|
|
2552
2608
|
return log.fail(.{ foe_ident, .None });
|
|
2553
2609
|
}
|
|
2554
|
-
|
|
2610
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
|
|
2555
2611
|
const stat = unmodifiedStats(battle, foe).atk;
|
|
2556
2612
|
stats.atk = @max(1, stat * mod[0] / mod[1]);
|
|
2557
2613
|
try log.boost(.{ foe_ident, .Attack, -1 });
|
|
@@ -2569,7 +2625,7 @@ pub const Effects = struct {
|
|
|
2569
2625
|
}
|
|
2570
2626
|
return log.fail(.{ foe_ident, .None });
|
|
2571
2627
|
}
|
|
2572
|
-
|
|
2628
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
|
|
2573
2629
|
const stat = unmodifiedStats(battle, foe).def;
|
|
2574
2630
|
stats.def = @max(1, stat * mod[0] / mod[1]);
|
|
2575
2631
|
try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
|
|
@@ -2586,7 +2642,7 @@ pub const Effects = struct {
|
|
|
2586
2642
|
}
|
|
2587
2643
|
return log.fail(.{ foe_ident, .None });
|
|
2588
2644
|
}
|
|
2589
|
-
|
|
2645
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
|
|
2590
2646
|
const stat = unmodifiedStats(battle, foe).spe;
|
|
2591
2647
|
stats.spe = @max(1, stat * mod[0] / mod[1]);
|
|
2592
2648
|
try log.boost(.{ foe_ident, .Speed, -1 });
|
|
@@ -2606,7 +2662,7 @@ pub const Effects = struct {
|
|
|
2606
2662
|
}
|
|
2607
2663
|
return log.fail(.{ foe_ident, .None });
|
|
2608
2664
|
}
|
|
2609
|
-
|
|
2665
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
|
|
2610
2666
|
const stat = unmodifiedStats(battle, foe).spc;
|
|
2611
2667
|
stats.spc = @max(1, stat * mod[0] / mod[1]);
|
|
2612
2668
|
try log.boost(.{ foe_ident, .SpecialAttack, -1 });
|
|
@@ -2645,22 +2701,22 @@ fn isForced(active: anytype) bool {
|
|
|
2645
2701
|
active.volatiles.Thrashing or active.volatiles.Charging;
|
|
2646
2702
|
}
|
|
2647
2703
|
|
|
2648
|
-
fn clearVolatiles(battle: anytype, who: Player,
|
|
2704
|
+
fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
|
|
2649
2705
|
var log = options.log;
|
|
2650
2706
|
var side = battle.side(who);
|
|
2651
2707
|
var volatiles = &side.active.volatiles;
|
|
2652
2708
|
const ident = battle.active(who);
|
|
2653
2709
|
|
|
2654
|
-
options.chance.clearDurations(who, clear);
|
|
2655
|
-
|
|
2656
2710
|
if (volatiles.disable_move != 0) {
|
|
2657
2711
|
volatiles.disable_move = 0;
|
|
2658
2712
|
volatiles.disable_duration = 0;
|
|
2713
|
+
options.chance.observe(.disable, who, .None);
|
|
2659
2714
|
try log.end(.{ ident, .DisableSilent });
|
|
2660
2715
|
}
|
|
2661
2716
|
if (volatiles.Confusion) {
|
|
2662
2717
|
// volatiles.confusion is left unchanged
|
|
2663
2718
|
volatiles.Confusion = false;
|
|
2719
|
+
options.chance.observe(.confusion, who, .None);
|
|
2664
2720
|
try log.end(.{ ident, .ConfusionSilent });
|
|
2665
2721
|
}
|
|
2666
2722
|
if (volatiles.Mist) {
|
|
@@ -2695,10 +2751,25 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
|
|
|
2695
2751
|
}
|
|
2696
2752
|
}
|
|
2697
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
|
+
|
|
2698
2767
|
pub const Rolls = struct {
|
|
2699
2768
|
fn speedTie(battle: anytype, options: anytype) !bool {
|
|
2700
2769
|
const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
|
|
2701
2770
|
player == .P1
|
|
2771
|
+
else if (@hasDecl(@TypeOf(battle.rng), "speedTie"))
|
|
2772
|
+
battle.rng.speedTie()
|
|
2702
2773
|
else if (showdown)
|
|
2703
2774
|
battle.rng.range(u8, 0, 2) == 0
|
|
2704
2775
|
else
|
|
@@ -2708,21 +2779,31 @@ pub const Rolls = struct {
|
|
|
2708
2779
|
return p1;
|
|
2709
2780
|
}
|
|
2710
2781
|
|
|
2711
|
-
fn criticalHit(
|
|
2782
|
+
fn criticalHit(
|
|
2783
|
+
battle: anytype,
|
|
2784
|
+
player: Player,
|
|
2785
|
+
rate: u8,
|
|
2786
|
+
spurious: bool,
|
|
2787
|
+
options: anytype,
|
|
2788
|
+
) !bool {
|
|
2712
2789
|
const crit = if (options.calc.overridden(player, .critical_hit)) |val|
|
|
2713
2790
|
val == .true
|
|
2791
|
+
else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
|
|
2792
|
+
battle.rng.criticalHit(player, rate)
|
|
2714
2793
|
else if (showdown)
|
|
2715
2794
|
battle.rng.chance(u8, rate, 256)
|
|
2716
2795
|
else
|
|
2717
2796
|
std.math.rotl(u8, battle.rng.next(), 3) < rate;
|
|
2718
2797
|
|
|
2719
|
-
try options.chance.criticalHit(player, crit, rate);
|
|
2798
|
+
if (!spurious) try options.chance.criticalHit(player, crit, rate);
|
|
2720
2799
|
return crit;
|
|
2721
2800
|
}
|
|
2722
2801
|
|
|
2723
2802
|
fn damage(battle: anytype, player: Player, options: anytype) !u8 {
|
|
2724
2803
|
const roll = if (options.calc.overridden(player, .damage)) |val|
|
|
2725
2804
|
val
|
|
2805
|
+
else if (@hasDecl(@TypeOf(battle.rng), "damage"))
|
|
2806
|
+
battle.rng.damage(player)
|
|
2726
2807
|
else roll: {
|
|
2727
2808
|
if (showdown) break :roll battle.rng.range(u8, 217, 256);
|
|
2728
2809
|
while (true) {
|
|
@@ -2736,21 +2817,27 @@ pub const Rolls = struct {
|
|
|
2736
2817
|
return roll;
|
|
2737
2818
|
}
|
|
2738
2819
|
|
|
2739
|
-
|
|
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;
|
|
2740
2823
|
const ok = if (options.calc.overridden(player, .hit)) |val|
|
|
2741
2824
|
val == .true
|
|
2825
|
+
else if (@hasDecl(@TypeOf(battle.rng), "hit"))
|
|
2826
|
+
battle.rng.hit(player, acc)
|
|
2742
2827
|
else if (showdown)
|
|
2743
|
-
battle.rng.chance(
|
|
2828
|
+
battle.rng.chance(uN, acc, 256)
|
|
2744
2829
|
else
|
|
2745
|
-
battle.rng.next() <
|
|
2830
|
+
battle.rng.next() < acc;
|
|
2746
2831
|
|
|
2747
|
-
options.chance.hit(ok,
|
|
2832
|
+
try options.chance.hit(player, ok, acc);
|
|
2748
2833
|
return ok;
|
|
2749
2834
|
}
|
|
2750
2835
|
|
|
2751
2836
|
fn confused(battle: anytype, player: Player, options: anytype) !bool {
|
|
2752
2837
|
const cfz = if (options.calc.overridden(player, .confused)) |val|
|
|
2753
2838
|
val == .true
|
|
2839
|
+
else if (@hasDecl(@TypeOf(battle.rng), "confused"))
|
|
2840
|
+
battle.rng.confused(player)
|
|
2754
2841
|
else if (showdown)
|
|
2755
2842
|
!battle.rng.chance(u8, 128, 256)
|
|
2756
2843
|
else
|
|
@@ -2763,6 +2850,8 @@ pub const Rolls = struct {
|
|
|
2763
2850
|
fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
|
|
2764
2851
|
const par = if (options.calc.overridden(player, .paralyzed)) |val|
|
|
2765
2852
|
val == .true
|
|
2853
|
+
else if (@hasDecl(@TypeOf(battle.rng), "paralyzed"))
|
|
2854
|
+
battle.rng.paralyzed(player)
|
|
2766
2855
|
else if (showdown)
|
|
2767
2856
|
battle.rng.chance(u8, 63, 256)
|
|
2768
2857
|
else
|
|
@@ -2775,6 +2864,8 @@ pub const Rolls = struct {
|
|
|
2775
2864
|
fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
|
|
2776
2865
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2777
2866
|
val == .true
|
|
2867
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2868
|
+
battle.rng.secondaryChance(player, 25)
|
|
2778
2869
|
else if (showdown)
|
|
2779
2870
|
battle.rng.chance(u8, 25, 256)
|
|
2780
2871
|
else
|
|
@@ -2789,6 +2880,8 @@ pub const Rolls = struct {
|
|
|
2789
2880
|
|
|
2790
2881
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2791
2882
|
val == .true
|
|
2883
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2884
|
+
battle.rng.secondaryChance(player, rate)
|
|
2792
2885
|
else if (showdown)
|
|
2793
2886
|
battle.rng.chance(u8, rate, 256)
|
|
2794
2887
|
else
|
|
@@ -2803,6 +2896,8 @@ pub const Rolls = struct {
|
|
|
2803
2896
|
|
|
2804
2897
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2805
2898
|
val == .true
|
|
2899
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2900
|
+
battle.rng.secondaryChance(player, rate)
|
|
2806
2901
|
else if (showdown)
|
|
2807
2902
|
battle.rng.chance(u8, rate, 256)
|
|
2808
2903
|
else
|
|
@@ -2815,6 +2910,8 @@ pub const Rolls = struct {
|
|
|
2815
2910
|
fn unboost(battle: anytype, player: Player, options: anytype) !bool {
|
|
2816
2911
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2817
2912
|
val == .true
|
|
2913
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2914
|
+
battle.rng.secondaryChance(player, 85)
|
|
2818
2915
|
else if (showdown)
|
|
2819
2916
|
battle.rng.chance(u8, 85, 256)
|
|
2820
2917
|
else
|
|
@@ -2827,6 +2924,8 @@ pub const Rolls = struct {
|
|
|
2827
2924
|
fn metronome(battle: anytype, player: Player, options: anytype) !Move {
|
|
2828
2925
|
const move: Move = if (options.calc.overridden(player, .metronome)) |val|
|
|
2829
2926
|
val
|
|
2927
|
+
else if (@hasDecl(@TypeOf(battle.rng), "metronome"))
|
|
2928
|
+
battle.rng.metronome(player)
|
|
2830
2929
|
else if (showdown)
|
|
2831
2930
|
Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
|
|
2832
2931
|
else move: {
|
|
@@ -2845,6 +2944,8 @@ pub const Rolls = struct {
|
|
|
2845
2944
|
fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
|
|
2846
2945
|
const power = if (options.calc.overridden(player, .psywave)) |val|
|
|
2847
2946
|
val - 1
|
|
2947
|
+
else if (@hasDecl(@TypeOf(battle.rng), "psywave"))
|
|
2948
|
+
battle.rng.psywave(player)
|
|
2848
2949
|
else if (showdown)
|
|
2849
2950
|
battle.rng.range(u8, 0, max)
|
|
2850
2951
|
else power: {
|
|
@@ -2860,8 +2961,11 @@ pub const Rolls = struct {
|
|
|
2860
2961
|
}
|
|
2861
2962
|
|
|
2862
2963
|
fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2863
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2864
|
-
@intCast(val)
|
|
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"))
|
|
2968
|
+
battle.rng.sleepDuration(player)
|
|
2865
2969
|
else if (showdown)
|
|
2866
2970
|
battle.rng.range(u3, 1, 8)
|
|
2867
2971
|
else duration: {
|
|
@@ -2872,46 +2976,55 @@ pub const Rolls = struct {
|
|
|
2872
2976
|
};
|
|
2873
2977
|
|
|
2874
2978
|
assert(duration >= 1 and duration <= 7);
|
|
2875
|
-
options.chance.duration(
|
|
2979
|
+
options.chance.duration(player, duration);
|
|
2876
2980
|
return duration;
|
|
2877
2981
|
}
|
|
2878
2982
|
|
|
2879
2983
|
fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
|
|
2880
|
-
const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
|
|
2881
|
-
val
|
|
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"))
|
|
2988
|
+
battle.rng.disableDuration(player)
|
|
2882
2989
|
else if (showdown)
|
|
2883
2990
|
battle.rng.range(u4, 1, 9)
|
|
2884
2991
|
else
|
|
2885
2992
|
@intCast((battle.rng.next() & 7) + 1);
|
|
2886
2993
|
|
|
2887
2994
|
assert(duration >= 1 and duration <= 8);
|
|
2888
|
-
options.chance.duration(
|
|
2995
|
+
options.chance.duration(player, duration);
|
|
2889
2996
|
return duration;
|
|
2890
2997
|
}
|
|
2891
2998
|
|
|
2892
|
-
fn confusionDuration(battle: anytype, player: Player,
|
|
2893
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2894
|
-
@intCast(val)
|
|
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)
|
|
2895
3005
|
else if (showdown)
|
|
2896
3006
|
battle.rng.range(u3, 2, 6)
|
|
2897
3007
|
else
|
|
2898
3008
|
@intCast((battle.rng.next() & 3) + 2);
|
|
2899
3009
|
|
|
2900
3010
|
assert(duration >= 2 and duration <= 5);
|
|
2901
|
-
options.chance.duration(
|
|
3011
|
+
options.chance.duration(player, duration);
|
|
2902
3012
|
return duration;
|
|
2903
3013
|
}
|
|
2904
3014
|
|
|
2905
3015
|
fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2906
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2907
|
-
@intCast(val)
|
|
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"))
|
|
3020
|
+
battle.rng.attackingDuration(player)
|
|
2908
3021
|
else if (showdown)
|
|
2909
3022
|
battle.rng.range(u3, 2, 4)
|
|
2910
3023
|
else
|
|
2911
3024
|
@intCast((battle.rng.next() & 1) + 2);
|
|
2912
3025
|
|
|
2913
3026
|
assert(duration >= 2 and duration <= 3);
|
|
2914
|
-
options.chance.duration(
|
|
3027
|
+
options.chance.duration(player, duration);
|
|
2915
3028
|
return duration;
|
|
2916
3029
|
}
|
|
2917
3030
|
|
|
@@ -2924,8 +3037,11 @@ pub const Rolls = struct {
|
|
|
2924
3037
|
options: anytype,
|
|
2925
3038
|
) !u3 {
|
|
2926
3039
|
const roll = if (effect == .Binding) .duration else .multi_hit;
|
|
2927
|
-
const n: u3 = if (options.calc.overridden(player, roll)) |val|
|
|
2928
|
-
@intCast(val)
|
|
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"))
|
|
3044
|
+
battle.rng.distribution(player)
|
|
2929
3045
|
else if (showdown)
|
|
2930
3046
|
DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
|
|
2931
3047
|
else n: {
|
|
@@ -2935,7 +3051,7 @@ pub const Rolls = struct {
|
|
|
2935
3051
|
|
|
2936
3052
|
assert(n >= 2 and n <= 5);
|
|
2937
3053
|
if (effect == .Binding) {
|
|
2938
|
-
options.chance.duration(
|
|
3054
|
+
options.chance.duration(player, n);
|
|
2939
3055
|
} else {
|
|
2940
3056
|
try options.chance.multiHit(player, n);
|
|
2941
3057
|
}
|
|
@@ -2950,15 +3066,15 @@ pub const Rolls = struct {
|
|
|
2950
3066
|
options: anytype,
|
|
2951
3067
|
) !u4 {
|
|
2952
3068
|
// TODO: consider throwing error instead of rerolling?
|
|
2953
|
-
const
|
|
3069
|
+
const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
|
|
2954
3070
|
if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
|
|
2955
3071
|
val
|
|
2956
3072
|
else
|
|
2957
3073
|
null
|
|
2958
3074
|
else
|
|
2959
|
-
null
|
|
2960
|
-
|
|
2961
|
-
|
|
3075
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
|
|
3076
|
+
battle.rng.moveSlot(player, moves, check_pp)
|
|
3077
|
+
else slot: {
|
|
2962
3078
|
if (showdown) {
|
|
2963
3079
|
if (check_pp == 0) {
|
|
2964
3080
|
var i: usize = moves.len;
|
|
@@ -3038,11 +3154,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3038
3154
|
const side = battle.side(player);
|
|
3039
3155
|
const foe = battle.foe(player);
|
|
3040
3156
|
|
|
3041
|
-
|
|
3157
|
+
const active = &side.active;
|
|
3042
3158
|
const stored = side.stored();
|
|
3043
3159
|
|
|
3044
3160
|
// While players are not given any input options on the cartridge in these cases,
|
|
3045
|
-
// 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
|
|
3046
3162
|
//
|
|
3047
3163
|
// Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
|
|
3048
3164
|
// here when not in compatibility mode, but the engine needs to be able to differentiate
|
|
@@ -3050,7 +3166,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3050
3166
|
// passing due to being forced into using a move. Instead of introducing another option
|
|
3051
3167
|
// we simply repurpose Move with no move slot, even though pedantically this is not
|
|
3052
3168
|
// strictly correct as the player would not have been presented the option to move or
|
|
3053
|
-
// switch at all
|
|
3169
|
+
// switch at all
|
|
3054
3170
|
if (isForced(active)) {
|
|
3055
3171
|
out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
|
|
3056
3172
|
n += 1;
|
|
@@ -3069,7 +3185,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3069
3185
|
// On the cartridge, all of these happen after "FIGHT" (indicating you are not
|
|
3070
3186
|
// switching) but before you are allowed to select a move. Pokémon Showdown instead
|
|
3071
3187
|
// either disables all other moves in the case of limited or requires you to select a
|
|
3072
|
-
// move normally if sleeping/frozen/bound
|
|
3188
|
+
// move normally if sleeping/frozen/bound
|
|
3073
3189
|
if (!showdown and (limited or foe.active.volatiles.Binding or
|
|
3074
3190
|
Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
|
|
3075
3191
|
{
|
|
@@ -3082,7 +3198,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3082
3198
|
// Pokémon Showdown handles Bide and Binding moves by checking if the move in question
|
|
3083
3199
|
// is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
|
|
3084
3200
|
// Move will not result in forcing the subsequent use unless the user also had the
|
|
3085
|
-
// proc-ed move in their moveset) and disabling all other moves
|
|
3201
|
+
// proc-ed move in their moveset) and disabling all other moves
|
|
3086
3202
|
if (limited) {
|
|
3087
3203
|
assert(showdown);
|
|
3088
3204
|
assert(side.last_selected_move != .None);
|
|
@@ -3114,7 +3230,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3114
3230
|
// Struggle (Pokémon Showdown would use 'move 1' here)
|
|
3115
3231
|
if (n == before) {
|
|
3116
3232
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
3117
|
-
if (!
|
|
3233
|
+
if (!pkmn.options.mod) {
|
|
3118
3234
|
while (slot <= 4) : (slot += 1) {
|
|
3119
3235
|
if (active.moves[slot - 1].pp != 0) return n;
|
|
3120
3236
|
}
|