@pkmn/engine 0.1.0-dev.d4119c62 → 0.1.0-dev.d4bd80b1
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 +46 -18
- package/build/pkg/addon/browser.d.ts +3 -0
- package/build/pkg/addon/browser.js +47 -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 +110 -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 +53 -0
- package/build/pkg/addon/wasm.js.map +1 -0
- package/build/pkg/addon.d.ts +25 -3
- package/build/pkg/addon.js +39 -71
- 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 +15 -18
- package/build/pkg/gen1.js +28 -19
- package/build/pkg/gen1.js.map +1 -1
- package/build/pkg/index.d.ts +40 -90
- package/build/pkg/index.js +23 -70
- 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 +53 -78
- package/build/tools/debug.js.map +1 -1
- package/build/tools/display/debug.jsx +152 -0
- package/build/tools/display/debug.jsx.map +1 -0
- package/build/tools/display/index.js +152 -0
- package/build/tools/display/index.js.map +1 -0
- package/build/tools/display/ui.jsx +359 -0
- package/build/tools/display/ui.jsx.map +1 -0
- package/build/tools/display/util.js +33 -0
- package/build/tools/display/util.js.map +1 -0
- package/build.zig +330 -104
- package/package.json +60 -134
- package/src/bin/install-pkmn-engine +32 -21
- package/src/bin/pkmn-debug +1 -1
- package/src/lib/bindings/c.zig +255 -0
- package/src/lib/{binding → bindings}/node.zig +8 -21
- package/src/lib/bindings/wasm.zig +59 -0
- 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 +63 -67
- 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 +379 -258
- 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 +217 -163
- package/src/lib/node.zig +18 -0
- package/src/lib/pkmn.zig +37 -15
- package/src/lib/wasm.zig +17 -0
- package/src/tools/display/dom.ts +95 -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/binding/wasm.zig +0 -12
- 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,29 @@
|
|
|
1
|
-
const
|
|
2
|
-
|
|
3
|
-
const pkmn = @import("../pkmn.zig");
|
|
1
|
+
const DEBUG = @import("../common/debug.zig").print;
|
|
4
2
|
|
|
3
|
+
const builtin = @import("builtin");
|
|
4
|
+
const chance = @import("chance.zig");
|
|
5
5
|
const common = @import("../common/data.zig");
|
|
6
|
-
const
|
|
6
|
+
const data = @import("data.zig");
|
|
7
|
+
const pkmn = @import("../pkmn.zig");
|
|
7
8
|
const protocol = @import("../common/protocol.zig");
|
|
8
9
|
const rng = @import("../common/rng.zig");
|
|
9
|
-
|
|
10
|
-
const data = @import("data.zig");
|
|
10
|
+
const std = @import("std");
|
|
11
11
|
|
|
12
12
|
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
13
|
const Boost = protocol.Boost;
|
|
14
|
+
const Choice = common.Choice;
|
|
24
15
|
const Damage = protocol.Damage;
|
|
16
|
+
const Effectiveness = data.Effectiveness;
|
|
17
|
+
const expectEqual = std.testing.expectEqual;
|
|
25
18
|
const Fail = protocol.Fail;
|
|
26
|
-
const Heal = protocol.Heal;
|
|
27
|
-
|
|
28
19
|
const Gen12 = rng.Gen12;
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
const Effectiveness = data.Effectiveness;
|
|
20
|
+
const Heal = protocol.Heal;
|
|
21
|
+
const ID = common.ID;
|
|
32
22
|
const Move = data.Move;
|
|
33
23
|
const MoveSlot = data.MoveSlot;
|
|
34
|
-
const
|
|
24
|
+
const Player = common.Player;
|
|
25
|
+
const Result = common.Result;
|
|
26
|
+
const showdown = pkmn.options.showdown;
|
|
35
27
|
const Side = data.Side;
|
|
36
28
|
const Species = data.Species;
|
|
37
29
|
const Stats = data.Stats;
|
|
@@ -71,8 +63,8 @@ pub fn update(battle: anytype, c1: Choice, c2: Choice, options: anytype) !Result
|
|
|
71
63
|
var p1 = battle.side(.P1);
|
|
72
64
|
var p2 = battle.side(.P2);
|
|
73
65
|
|
|
74
|
-
|
|
75
|
-
|
|
66
|
+
const r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
|
|
67
|
+
const r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
|
|
76
68
|
|
|
77
69
|
if (try turnOrder(battle, c1, c2, options) == .P1) {
|
|
78
70
|
if (try doTurn(battle, .P1, c1, r1, s1, .P2, c2, r2, s2, options)) |r| return r;
|
|
@@ -90,11 +82,11 @@ fn start(battle: anytype, options: anytype) !Result {
|
|
|
90
82
|
const p1 = battle.side(.P1);
|
|
91
83
|
const p2 = battle.side(.P2);
|
|
92
84
|
|
|
93
|
-
|
|
85
|
+
const p1_slot = findFirstAlive(p1);
|
|
94
86
|
assert(!showdown or p1_slot == 1);
|
|
95
87
|
if (p1_slot == 0) return if (findFirstAlive(p2) == 0) Result.Tie else Result.Lose;
|
|
96
88
|
|
|
97
|
-
|
|
89
|
+
const p2_slot = findFirstAlive(p2);
|
|
98
90
|
assert(!showdown or p2_slot == 1);
|
|
99
91
|
if (p2_slot == 0) return Result.Win;
|
|
100
92
|
|
|
@@ -166,7 +158,7 @@ fn selectMove(
|
|
|
166
158
|
// to satisfy the conditions of the Desync Clause Mod. However, because Binding is still
|
|
167
159
|
// set the selected move will not actually be used, it will just be reported as having
|
|
168
160
|
// been used (this differs from how Pokémon Showdown works, but its impossible to
|
|
169
|
-
// replicate the incorrect behavior with the correct mechanisms)
|
|
161
|
+
// replicate the incorrect behavior with the correct mechanisms)
|
|
170
162
|
saveMove(battle, player, choice);
|
|
171
163
|
} else {
|
|
172
164
|
assert(choice.data == 0);
|
|
@@ -174,7 +166,10 @@ fn selectMove(
|
|
|
174
166
|
if (foe_choice.type == .Switch) {
|
|
175
167
|
const last = side.active.move(battle.lastMove(player).index);
|
|
176
168
|
if (last.id == .Metronome) side.last_selected_move = last.id;
|
|
177
|
-
if (last.id == .MirrorMove)
|
|
169
|
+
if (last.id == .MirrorMove) {
|
|
170
|
+
if (!pkmn.options.mod) return Result.Error;
|
|
171
|
+
side.last_selected_move = last.id;
|
|
172
|
+
}
|
|
178
173
|
}
|
|
179
174
|
}
|
|
180
175
|
return null;
|
|
@@ -253,11 +248,11 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: a
|
|
|
253
248
|
active.moves = incoming.moves;
|
|
254
249
|
|
|
255
250
|
statusModify(incoming.status, &active.stats);
|
|
251
|
+
options.chance.switched(player, slot);
|
|
256
252
|
|
|
257
253
|
foe.active.volatiles.Binding = false;
|
|
258
254
|
|
|
259
255
|
try options.log.switched(.{ battle.active(player), incoming });
|
|
260
|
-
options.chance.switched(player, side.order[0], out);
|
|
261
256
|
|
|
262
257
|
if (showdown and incoming.status == Status.TOX) {
|
|
263
258
|
incoming.status = Status.init(.PSN);
|
|
@@ -347,13 +342,13 @@ fn doTurn(
|
|
|
347
342
|
)) |r| return r;
|
|
348
343
|
if (!replace) {
|
|
349
344
|
if (player_choice.type != .Switch) {
|
|
350
|
-
if (try checkFaint(battle, foe_player, options)) |r| return r;
|
|
345
|
+
if (try checkFaint(battle, foe_player, true, options)) |r| return r;
|
|
351
346
|
}
|
|
352
347
|
if (residual) try handleResidual(battle, player, options);
|
|
353
|
-
if (try checkFaint(battle, player, options)) |r| return r;
|
|
348
|
+
if (try checkFaint(battle, player, false, options)) |r| return r;
|
|
354
349
|
} else if (foe_choice.type == .Pass) return null;
|
|
355
350
|
|
|
356
|
-
options.chance.clearPending();
|
|
351
|
+
if (!showdown) options.chance.clearPending();
|
|
357
352
|
|
|
358
353
|
residual = true;
|
|
359
354
|
replace = battle.side(foe_player).stored().hp == 0;
|
|
@@ -369,10 +364,10 @@ fn doTurn(
|
|
|
369
364
|
)) |r| return r;
|
|
370
365
|
if (!replace) {
|
|
371
366
|
if (!calc and foe_choice.type != .Switch) {
|
|
372
|
-
if (try checkFaint(battle, player, options)) |r| return r;
|
|
367
|
+
if (try checkFaint(battle, player, true, options)) |r| return r;
|
|
373
368
|
}
|
|
374
369
|
if (residual) try handleResidual(battle, foe_player, options);
|
|
375
|
-
if (try checkFaint(battle, foe_player, options)) |r| return r;
|
|
370
|
+
if (try checkFaint(battle, foe_player, false, options)) |r| return r;
|
|
376
371
|
}
|
|
377
372
|
|
|
378
373
|
// Flinch is bugged on Pokémon Showdown because it gets implemented with a duration which causes
|
|
@@ -425,13 +420,13 @@ fn executeMove(
|
|
|
425
420
|
mslot = @intCast(battle.lastMove(player).index);
|
|
426
421
|
const stored = side.stored();
|
|
427
422
|
// GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
|
|
428
|
-
auto = isForced(&side.active) or
|
|
423
|
+
auto = pkmn.options.mod or isForced(&side.active) or
|
|
429
424
|
side.active.volatiles.Binding or side.active.volatiles.Bide or
|
|
430
425
|
side.last_selected_move == .HyperBeam or
|
|
431
426
|
Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
|
|
432
427
|
// If it wasn't Hyper Beam or the continuation of a move effect then we must have just
|
|
433
428
|
// 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
|
|
429
|
+
// index 1 and the current Pokémon has the same move in its first slot
|
|
435
430
|
if (!auto) {
|
|
436
431
|
// side.active.moves(slot) is safe to check even though the slot in question might not
|
|
437
432
|
// technically be from this Pokémon because it must be exactly 1 to not desync and
|
|
@@ -458,6 +453,25 @@ fn executeMove(
|
|
|
458
453
|
.err => return @as(?Result, Result.Error),
|
|
459
454
|
}
|
|
460
455
|
|
|
456
|
+
// Pokémon Showdown incorrectly implements PP deduction when handling the Hyper Beam automatic
|
|
457
|
+
// selection glitch - if the move was proced due to Metronome / Mirror Move it either subtracts
|
|
458
|
+
// from the Hyper Beam slot if present or skips PP deduction entirely...
|
|
459
|
+
if (showdown and !skip_pp and side.last_selected_move == .HyperBeam and
|
|
460
|
+
side.last_selected_move != side.active.move(mslot).id)
|
|
461
|
+
{
|
|
462
|
+
assert(mslot == 1);
|
|
463
|
+
const has_beam = has_beam: {
|
|
464
|
+
for (side.active.moves, 0..) |m, i| {
|
|
465
|
+
if (m.id == .HyperBeam) {
|
|
466
|
+
mslot = @intCast(i + 1);
|
|
467
|
+
break :has_beam true;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
break :has_beam false;
|
|
471
|
+
};
|
|
472
|
+
if (!has_beam) skip_pp = true;
|
|
473
|
+
}
|
|
474
|
+
|
|
461
475
|
const can = skip_can or
|
|
462
476
|
try canMove(battle, player, mslot, auto, skip_pp, .None, residual, options);
|
|
463
477
|
if (!can) return null;
|
|
@@ -484,17 +498,22 @@ fn beforeMove(
|
|
|
484
498
|
|
|
485
499
|
if (Status.is(stored.status, .SLP)) {
|
|
486
500
|
const before = stored.status;
|
|
501
|
+
const slf = Status.is(stored.status, .EXT);
|
|
487
502
|
// Even if the EXT bit is set this will still correctly modify the sleep duration
|
|
488
|
-
if (options.calc.
|
|
489
|
-
|
|
503
|
+
if (options.calc.overridden(player, .sleep)) |obs| switch (obs) {
|
|
504
|
+
.started, .ended => stored.status = 0,
|
|
505
|
+
.continuing => if (Status.duration(stored.status) > 1) {
|
|
506
|
+
stored.status -= 1;
|
|
507
|
+
},
|
|
508
|
+
else => unreachable,
|
|
490
509
|
} else {
|
|
491
510
|
stored.status -= 1;
|
|
492
511
|
}
|
|
493
512
|
|
|
494
513
|
const duration = Status.duration(stored.status);
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
514
|
+
try options.chance.sleep(player, if (slf)
|
|
515
|
+
.None
|
|
516
|
+
else if (duration == 0) .ended else .continuing);
|
|
498
517
|
|
|
499
518
|
if (duration == 0) {
|
|
500
519
|
try log.curestatus(.{ ident, before, .Message });
|
|
@@ -532,12 +551,12 @@ fn beforeMove(
|
|
|
532
551
|
}
|
|
533
552
|
|
|
534
553
|
if (volatiles.disable_duration > 0) {
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
554
|
+
volatiles.disable_duration =
|
|
555
|
+
decrement(.disable, player, options, volatiles.disable_duration);
|
|
556
|
+
try options.chance.disable(
|
|
557
|
+
player,
|
|
558
|
+
if (volatiles.disable_duration == 0) .ended else .continuing,
|
|
559
|
+
);
|
|
541
560
|
|
|
542
561
|
if (volatiles.disable_duration == 0) {
|
|
543
562
|
volatiles.disable_move = 0;
|
|
@@ -550,19 +569,24 @@ fn beforeMove(
|
|
|
550
569
|
// This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
|
|
551
570
|
// before the side had a selected a move - we simply need to assume this leads to a desync
|
|
552
571
|
if (side.last_selected_move == .None) {
|
|
553
|
-
assert(!
|
|
572
|
+
assert(!pkmn.options.mod);
|
|
554
573
|
return .err;
|
|
555
574
|
}
|
|
556
575
|
|
|
557
576
|
if (volatiles.Confusion) {
|
|
558
577
|
assert(volatiles.confusion > 0);
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
if (
|
|
578
|
+
if (options.calc.overridden(player, .confusion)) |obs| switch (obs) {
|
|
579
|
+
.started, .ended => volatiles.confusion = 0,
|
|
580
|
+
.continuing => if (volatiles.confusion > 1) {
|
|
581
|
+
volatiles.confusion -= 1;
|
|
582
|
+
},
|
|
583
|
+
.overwritten => {},
|
|
584
|
+
else => unreachable,
|
|
562
585
|
} else {
|
|
563
586
|
volatiles.confusion -= 1;
|
|
564
587
|
}
|
|
565
|
-
|
|
588
|
+
|
|
589
|
+
try options.chance.confusion(player, if (volatiles.confusion == 0) .ended else .continuing);
|
|
566
590
|
|
|
567
591
|
if (volatiles.confusion == 0) {
|
|
568
592
|
volatiles.Confusion = false;
|
|
@@ -580,7 +604,9 @@ fn beforeMove(
|
|
|
580
604
|
volatiles.Charging = false;
|
|
581
605
|
volatiles.Binding = false;
|
|
582
606
|
volatiles.Invulnerable = false;
|
|
583
|
-
|
|
607
|
+
|
|
608
|
+
options.chance.observe(.attacking, player, .None);
|
|
609
|
+
options.chance.observe(.binding, player, .None);
|
|
584
610
|
{
|
|
585
611
|
// This feels (and is) disgusting but the cartridge literally just overwrites
|
|
586
612
|
// the opponent's defense with the user's defense and resets it after. As a
|
|
@@ -588,7 +614,9 @@ fn beforeMove(
|
|
|
588
614
|
const def = foe.active.stats.def;
|
|
589
615
|
foe.active.stats.def = active.stats.def;
|
|
590
616
|
defer foe.active.stats.def = def;
|
|
591
|
-
if (!calcDamage(battle, player, player.foe(), null, false))
|
|
617
|
+
if (!calcDamage(battle, player, player.foe(), null, false, options)) {
|
|
618
|
+
return .err;
|
|
619
|
+
}
|
|
592
620
|
}
|
|
593
621
|
// Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
|
|
594
622
|
// on the targeting behavior of the confused Pokémon's selected move which results
|
|
@@ -617,7 +645,10 @@ fn beforeMove(
|
|
|
617
645
|
volatiles.Thrashing = false;
|
|
618
646
|
volatiles.Charging = false;
|
|
619
647
|
volatiles.Binding = false;
|
|
620
|
-
|
|
648
|
+
|
|
649
|
+
options.chance.observe(.attacking, player, .None);
|
|
650
|
+
options.chance.observe(.binding, player, .None);
|
|
651
|
+
|
|
621
652
|
// GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
|
|
622
653
|
try log.cant(.{ ident, .Paralysis });
|
|
623
654
|
return .done;
|
|
@@ -635,14 +666,8 @@ fn beforeMove(
|
|
|
635
666
|
volatiles.state +%= battle.last_damage;
|
|
636
667
|
}
|
|
637
668
|
|
|
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);
|
|
669
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
670
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
646
671
|
|
|
647
672
|
if (volatiles.attacks != 0) {
|
|
648
673
|
try log.activate(.{ ident, .Bide });
|
|
@@ -676,39 +701,40 @@ fn beforeMove(
|
|
|
676
701
|
}
|
|
677
702
|
|
|
678
703
|
if (volatiles.Thrashing) {
|
|
679
|
-
|
|
704
|
+
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
680
705
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
try options.chance.attacking(player, volatiles.attacks);
|
|
706
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
707
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
708
|
+
|
|
709
|
+
if (volatiles.attacks == 0) {
|
|
710
|
+
const overwritten = volatiles.Confusion;
|
|
687
711
|
|
|
688
|
-
|
|
712
|
+
volatiles.Thrashing = false;
|
|
713
|
+
volatiles.Confusion = true;
|
|
714
|
+
volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
715
|
+
|
|
716
|
+
// On Pokémon Showdown this could leak information to the opponent that thrashing has
|
|
717
|
+
// ended (the engine's protocol is always from the omniscient perspective and clients
|
|
718
|
+
// can be expected to recognize this pattern and pass the information only to the
|
|
719
|
+
// correct player)
|
|
689
720
|
try log.start(.{ battle.active(player), .ConfusionSilent });
|
|
721
|
+
options.chance.observe(.confusion, player, if (overwritten)
|
|
722
|
+
if (pkmn.options.overwrite) .overwritten else .continuing
|
|
723
|
+
else
|
|
724
|
+
.started);
|
|
725
|
+
if (!overwritten) options.calc.confusion(player);
|
|
690
726
|
}
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
if (handleThrashing(battle, active, player, options)) {
|
|
697
|
-
try log.start(.{ battle.active(player), .ConfusionSilent });
|
|
698
|
-
}
|
|
699
|
-
}
|
|
727
|
+
|
|
728
|
+
// This shouldn't actually set last_used_move, but Pokémon Showdown sets last
|
|
729
|
+
// used in useMove and doesn't have the notion of skipping canMove semantics
|
|
730
|
+
if (showdown) side.last_used_move = side.last_selected_move;
|
|
731
|
+
|
|
700
732
|
return .skip_can;
|
|
701
733
|
}
|
|
702
734
|
|
|
703
735
|
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);
|
|
736
|
+
volatiles.attacks = decrement(.binding, player, options, volatiles.attacks);
|
|
737
|
+
try options.chance.binding(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
712
738
|
|
|
713
739
|
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
714
740
|
if (showdown or battle.last_damage != 0) {
|
|
@@ -738,7 +764,6 @@ fn canMove(
|
|
|
738
764
|
const player_ident = battle.active(player);
|
|
739
765
|
const move = Move.get(side.last_selected_move);
|
|
740
766
|
|
|
741
|
-
var skip = skip_pp;
|
|
742
767
|
if (side.active.volatiles.Charging) {
|
|
743
768
|
side.active.volatiles.Charging = false;
|
|
744
769
|
side.active.volatiles.Invulnerable = false;
|
|
@@ -750,7 +775,7 @@ fn canMove(
|
|
|
750
775
|
}
|
|
751
776
|
|
|
752
777
|
side.last_used_move = side.last_selected_move;
|
|
753
|
-
if (!
|
|
778
|
+
if (!skip_pp) decrementPP(side, mslot, auto);
|
|
754
779
|
|
|
755
780
|
const target = if (move.target == .Self) player else player.foe();
|
|
756
781
|
try options.log.move(.{ player_ident, side.last_selected_move, battle.active(target), from });
|
|
@@ -763,9 +788,14 @@ fn canMove(
|
|
|
763
788
|
|
|
764
789
|
if (move.effect == .Thrashing) {
|
|
765
790
|
Effects.thrashing(battle, player, options);
|
|
766
|
-
} else if (
|
|
767
|
-
// Pokémon Showdown handles this after hit/miss checks and damage calculation
|
|
768
|
-
|
|
791
|
+
} else if (move.effect == .Binding) {
|
|
792
|
+
// Pokémon Showdown handles this after hit/miss checks and damage calculation, though clears
|
|
793
|
+
// Recharging in onTryMove (required for the Hyper Beam automatic selection glitch)
|
|
794
|
+
if (showdown) {
|
|
795
|
+
battle.foe(player).active.volatiles.Recharging = false;
|
|
796
|
+
} else {
|
|
797
|
+
try Effects.binding(battle, player, false, options);
|
|
798
|
+
}
|
|
769
799
|
}
|
|
770
800
|
|
|
771
801
|
return true;
|
|
@@ -802,7 +832,7 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
|
|
|
802
832
|
assert(active.move(mslot).pp == side.stored().move(mslot).pp);
|
|
803
833
|
}
|
|
804
834
|
|
|
805
|
-
fn incrementPP(side: *Side, mslot: u4) void {
|
|
835
|
+
fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
|
|
806
836
|
var active = &side.active;
|
|
807
837
|
const volatiles = &active.volatiles;
|
|
808
838
|
|
|
@@ -810,8 +840,11 @@ fn incrementPP(side: *Side, mslot: u4) void {
|
|
|
810
840
|
// GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
|
|
811
841
|
if (showdown and volatiles.Transform) return;
|
|
812
842
|
|
|
843
|
+
const n = options.calc.overridden(player, .pp) orelse 1;
|
|
844
|
+
if (volatiles.Transform and !options.chance.pp(player, n)) return;
|
|
845
|
+
|
|
813
846
|
assert(mslot > 0 and mslot <= 4);
|
|
814
|
-
side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +%
|
|
847
|
+
side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% n;
|
|
815
848
|
}
|
|
816
849
|
|
|
817
850
|
// Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
|
|
@@ -859,13 +892,13 @@ fn doMove(
|
|
|
859
892
|
break :miss true;
|
|
860
893
|
}
|
|
861
894
|
break :miss (move.target != .Self and
|
|
862
|
-
!moveHit(battle, player, move, &immune, &mist, options));
|
|
895
|
+
!try moveHit(battle, player, move, &immune, &mist, options));
|
|
863
896
|
};
|
|
864
897
|
assert(!immune or miss or (showdown and move.effect == .Binding));
|
|
865
898
|
|
|
866
899
|
var late = showdown and move.effect != .Explode;
|
|
867
900
|
const skip = status or immune;
|
|
868
|
-
if ((!showdown or (!skip or counter)) and !miss)
|
|
901
|
+
if ((!showdown or (!skip or counter)) and !miss) damage: {
|
|
869
902
|
if (showdown and move.effect.isMulti()) {
|
|
870
903
|
try Effects.multiHit(battle, player, move, options);
|
|
871
904
|
hits = side.active.volatiles.attacks;
|
|
@@ -887,8 +920,9 @@ fn doMove(
|
|
|
887
920
|
ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
|
|
888
921
|
// This can overflow after adjustDamage, but will still be sufficient to OHKO
|
|
889
922
|
battle.last_damage = if (ohko) 65535 else 0;
|
|
890
|
-
if (showdown) break :
|
|
891
|
-
} else if (!calcDamage(battle, player, player.foe(), move, crit)) {
|
|
923
|
+
if (showdown) break :damage; // skip adjustDamage / randomizeDamage
|
|
924
|
+
} else if (!calcDamage(battle, player, player.foe(), move, crit, options)) {
|
|
925
|
+
if (!showdown) try options.chance.commit(player, .err);
|
|
892
926
|
return @as(?Result, Result.Error);
|
|
893
927
|
}
|
|
894
928
|
if (battle.last_damage == 0) {
|
|
@@ -902,11 +936,12 @@ fn doMove(
|
|
|
902
936
|
}
|
|
903
937
|
}
|
|
904
938
|
|
|
905
|
-
var
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
939
|
+
var missed: bool = undefined;
|
|
940
|
+
const zero = battle.last_damage == 0;
|
|
941
|
+
if (!showdown and !skip) {
|
|
942
|
+
missed = !try moveHit(battle, player, move, &immune, &mist, options);
|
|
943
|
+
miss = missed or zero;
|
|
944
|
+
}
|
|
910
945
|
|
|
911
946
|
assert(showdown or miss or battle.last_damage > 0 or skip);
|
|
912
947
|
assert((!showdown and miss) or !(ohko and immune));
|
|
@@ -933,11 +968,32 @@ fn doMove(
|
|
|
933
968
|
try log.immune(.{ foe_ident, .OHKO });
|
|
934
969
|
} else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
|
|
935
970
|
try log.immune(.{ foe_ident, .None });
|
|
971
|
+
if (!showdown) {
|
|
972
|
+
if (move.effect == .Binding) {
|
|
973
|
+
// Rather obnoxiously, the cartridge sets the Binding volatile in canMove and
|
|
974
|
+
// then simply clears it later on if the move happens to miss in moveHit.
|
|
975
|
+
// However, moveHit can return true and we'll still end up in the outer miss
|
|
976
|
+
// handling block because attacking into an immune Pokemon does zero damage
|
|
977
|
+
// (considered missing)
|
|
978
|
+
assert(zero);
|
|
979
|
+
// Thus we still need to actually save the hit result (but not the crit roll).
|
|
980
|
+
// Using .miss for this commit would accomplish what we want, but is somewhat
|
|
981
|
+
// misleading as its entirely possible the move actually did hit (and the target
|
|
982
|
+
// is now trapped) so we use the .binding value instead which is effectively
|
|
983
|
+
// identical, just doesn't claim to know about the actual results of the hit
|
|
984
|
+
// roll)
|
|
985
|
+
try options.chance.commit(player, .binding);
|
|
986
|
+
} else {
|
|
987
|
+
// The .glitch option allows for conditionally committing if we are in a
|
|
988
|
+
// division-by-zero freeze alternative timeline scenario
|
|
989
|
+
try options.chance.commit(player, .glitch);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
936
992
|
} else if (mist) {
|
|
937
993
|
if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
|
|
938
994
|
try log.fail(.{ foe_ident, .None });
|
|
939
995
|
} else {
|
|
940
|
-
if (showdown
|
|
996
|
+
if (!showdown) try options.chance.commit(player, if (zero and !missed) .hit else .miss);
|
|
941
997
|
try log.lastmiss(.{});
|
|
942
998
|
try log.miss(.{battle.active(player)});
|
|
943
999
|
}
|
|
@@ -952,17 +1008,15 @@ fn doMove(
|
|
|
952
1008
|
try buildRage(battle, player.foe(), options);
|
|
953
1009
|
} else if (showdown and move.effect == .Disable) {
|
|
954
1010
|
try buildRage(battle, player.foe(), options);
|
|
955
|
-
} else if (!showdown and immune and move.effect == .Binding) {
|
|
956
|
-
try options.chance.commit(player, .binding);
|
|
957
1011
|
}
|
|
958
1012
|
return null;
|
|
959
|
-
} else {
|
|
1013
|
+
} else if (!showdown) {
|
|
960
1014
|
try options.chance.commit(player, .hit);
|
|
961
1015
|
}
|
|
962
1016
|
|
|
963
1017
|
// On the cartridge MultiHit doesn't get set up until after damage has been applied for the
|
|
964
1018
|
// first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
|
|
965
|
-
// it up above before damage calculation)
|
|
1019
|
+
// it up above before damage calculation)
|
|
966
1020
|
if (!showdown and move.effect.isMulti()) {
|
|
967
1021
|
try Effects.multiHit(battle, player, move, options);
|
|
968
1022
|
hits = side.active.volatiles.attacks;
|
|
@@ -1001,32 +1055,30 @@ fn doMove(
|
|
|
1001
1055
|
try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
1002
1056
|
}
|
|
1003
1057
|
try log.hitcount(.{ battle.active(player.foe()), hit });
|
|
1004
|
-
} else if (showdown) {
|
|
1058
|
+
} else if (showdown and move.effect == .Binding) {
|
|
1005
1059
|
// 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;
|
|
1060
|
+
try Effects.binding(battle, player, rewrap, options);
|
|
1061
|
+
if (immune) {
|
|
1062
|
+
battle.last_damage = 0;
|
|
1063
|
+
assert(foe.stored().hp > 0);
|
|
1064
|
+
// Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
|
|
1065
|
+
if (sub) {
|
|
1066
|
+
try log.activate(.{ battle.active(player.foe()), .Substitute });
|
|
1067
|
+
} else {
|
|
1068
|
+
try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
|
|
1069
|
+
try buildRage(battle, player.foe(), options);
|
|
1019
1070
|
}
|
|
1071
|
+
return null;
|
|
1020
1072
|
}
|
|
1021
1073
|
}
|
|
1022
1074
|
|
|
1023
1075
|
// 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
|
|
1076
|
+
// if an effect was intended to "always happen" it will still get skipped
|
|
1025
1077
|
if (nullified) return null;
|
|
1026
1078
|
|
|
1027
1079
|
// On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
|
|
1028
1080
|
// 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
|
|
1081
|
+
// setup before the loop means we can avoid having to waste time doing no-op handler searches
|
|
1030
1082
|
if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
|
|
1031
1083
|
|
|
1032
1084
|
if (foe.stored().hp == 0) return null;
|
|
@@ -1041,7 +1093,7 @@ fn doMove(
|
|
|
1041
1093
|
// below). For Twineedle we change the data to that of one with PoisonChance1 given its
|
|
1042
1094
|
// MultiHit behavior is complete after the loop above, though Pokémon Showdown handles the
|
|
1043
1095
|
// Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
|
|
1044
|
-
// puts the |-status| message before |-hitcount| instead of after
|
|
1096
|
+
// puts the |-status| message before |-hitcount| instead of after
|
|
1045
1097
|
if (move.effect == .Twineedle) {
|
|
1046
1098
|
if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
1047
1099
|
} else if (move.effect == .Disable) {
|
|
@@ -1060,12 +1112,14 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
|
|
|
1060
1112
|
const side = battle.side(player);
|
|
1061
1113
|
|
|
1062
1114
|
// Base speed is used for the critical hit calculation, even when Transform-ed
|
|
1063
|
-
var rate =
|
|
1115
|
+
var rate: u16 = Species.chance(side.stored().species);
|
|
1064
1116
|
// GLITCH: Focus Energy reduces critical hit rate instead of increasing it
|
|
1065
1117
|
rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
|
|
1066
1118
|
rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
|
|
1067
1119
|
|
|
1068
|
-
|
|
1120
|
+
const spurious =
|
|
1121
|
+
!showdown and ((move.bp == 0 and move.effect != .OHKO) or move.target == .Depends);
|
|
1122
|
+
return Rolls.criticalHit(battle, player, @intCast(rate), spurious, options);
|
|
1069
1123
|
}
|
|
1070
1124
|
|
|
1071
1125
|
fn calcDamage(
|
|
@@ -1074,6 +1128,7 @@ fn calcDamage(
|
|
|
1074
1128
|
target_player: Player,
|
|
1075
1129
|
m: ?Move.Data,
|
|
1076
1130
|
crit: bool,
|
|
1131
|
+
options: anytype,
|
|
1077
1132
|
) bool {
|
|
1078
1133
|
// Confusion (indicated when m == null) just needs a 40 BP physical move
|
|
1079
1134
|
const cfz = m == null;
|
|
@@ -1085,28 +1140,8 @@ fn calcDamage(
|
|
|
1085
1140
|
|
|
1086
1141
|
const special = move.type.special();
|
|
1087
1142
|
|
|
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
|
|
1143
|
+
var atk = attack(side, crit, special);
|
|
1144
|
+
var def = defense(target, crit, special, cfz);
|
|
1110
1145
|
|
|
1111
1146
|
// Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
|
|
1112
1147
|
// will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
|
|
@@ -1114,12 +1149,12 @@ fn calcDamage(
|
|
|
1114
1149
|
if ((!showdown or !cfz) and (atk > 255 or def > 255)) {
|
|
1115
1150
|
atk = @max((atk / 4) & 255, 1);
|
|
1116
1151
|
// 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 (
|
|
1152
|
+
def = @max((def / 4) & 255, if (pkmn.options.mod) 1 else 0);
|
|
1118
1153
|
}
|
|
1119
1154
|
|
|
1120
|
-
const lvl =
|
|
1155
|
+
const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
|
|
1121
1156
|
|
|
1122
|
-
def =
|
|
1157
|
+
def = if (move.effect == .Explode) @max(def / 2, 1) else def;
|
|
1123
1158
|
|
|
1124
1159
|
if (def == 0) return false;
|
|
1125
1160
|
|
|
@@ -1131,11 +1166,41 @@ fn calcDamage(
|
|
|
1131
1166
|
d = @min(997, d);
|
|
1132
1167
|
d += 2;
|
|
1133
1168
|
|
|
1169
|
+
// Whether or not a critical hit occurred can change whether a division-by-zero freeze is
|
|
1170
|
+
// encountered so we need to make sure that in the event of a miss we commit the status of the
|
|
1171
|
+
// crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
|
|
1172
|
+
// that results in counter intuitive probabilities in the common case
|
|
1173
|
+
if (pkmn.options.chance and !pkmn.options.mod and move.effect != .Explode) {
|
|
1174
|
+
atk = attack(side, !crit, special);
|
|
1175
|
+
def = defense(target, !crit, special, false);
|
|
1176
|
+
if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1134
1179
|
battle.last_damage = @intCast(d);
|
|
1135
1180
|
|
|
1136
1181
|
return true;
|
|
1137
1182
|
}
|
|
1138
1183
|
|
|
1184
|
+
fn attack(side: *Side, crit: bool, special: bool) u32 {
|
|
1185
|
+
return if (crit)
|
|
1186
|
+
if (special) side.stored().stats.spc else side.stored().stats.atk
|
|
1187
|
+
else if (special) side.active.stats.spc else side.active.stats.atk;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
fn defense(target: *Side, crit: bool, special: bool, cfz: bool) u32 {
|
|
1191
|
+
return if (crit)
|
|
1192
|
+
if (special) target.stored().stats.spc else target.stored().stats.def
|
|
1193
|
+
else
|
|
1194
|
+
// GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
|
|
1195
|
+
if (special)
|
|
1196
|
+
target.active.stats.spc *
|
|
1197
|
+
@as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
|
|
1198
|
+
else
|
|
1199
|
+
// Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
|
|
1200
|
+
target.active.stats.def *
|
|
1201
|
+
@as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1139
1204
|
fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
1140
1205
|
const side = battle.side(player);
|
|
1141
1206
|
const foe = battle.foe(player);
|
|
@@ -1180,7 +1245,6 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1180
1245
|
const foe = battle.foe(player);
|
|
1181
1246
|
|
|
1182
1247
|
if (!try checkHit(battle, player, move, options)) return null;
|
|
1183
|
-
try options.chance.commit(player, .hit);
|
|
1184
1248
|
|
|
1185
1249
|
battle.last_damage = switch (side.last_selected_move) {
|
|
1186
1250
|
.SuperFang => @max(foe.stored().hp / 2, 1),
|
|
@@ -1191,13 +1255,13 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1191
1255
|
.Psywave => power: {
|
|
1192
1256
|
const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
|
|
1193
1257
|
// GLITCH: Psywave infinite glitch loop
|
|
1194
|
-
if (!
|
|
1258
|
+
if (!pkmn.options.mod and max <= 1) return Result.Error;
|
|
1195
1259
|
break :power try Rolls.psywave(battle, player, max, options);
|
|
1196
1260
|
},
|
|
1197
1261
|
else => unreachable,
|
|
1198
1262
|
};
|
|
1199
1263
|
|
|
1200
|
-
if (battle.last_damage == 0) return if (
|
|
1264
|
+
if (battle.last_damage == 0) return if (pkmn.options.mod) null else Result.Error;
|
|
1201
1265
|
|
|
1202
1266
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1203
1267
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
@@ -1234,16 +1298,19 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1234
1298
|
|
|
1235
1299
|
if (!used or !selected) {
|
|
1236
1300
|
// GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
|
|
1237
|
-
if (!
|
|
1301
|
+
if (!pkmn.options.mod) return Result.Error;
|
|
1238
1302
|
try options.log.fail(.{ battle.active(player), .None });
|
|
1239
1303
|
return null;
|
|
1240
1304
|
}
|
|
1241
1305
|
|
|
1242
|
-
|
|
1306
|
+
// NOTE: llvm/llvm-project#58557
|
|
1307
|
+
battle.last_damage = if (comptime builtin.target.isWasm())
|
|
1308
|
+
@max(battle.last_damage *% 2, std.math.maxInt(u16))
|
|
1309
|
+
else
|
|
1310
|
+
battle.last_damage *| 2;
|
|
1243
1311
|
|
|
1244
|
-
// Pokémon Showdown calls
|
|
1312
|
+
// Pokémon Showdown calls moveHit before Counter
|
|
1245
1313
|
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
1246
|
-
try options.chance.commit(player, .hit);
|
|
1247
1314
|
|
|
1248
1315
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1249
1316
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
@@ -1272,7 +1339,7 @@ fn applyDamage(
|
|
|
1272
1339
|
subbed.active.volatiles.Substitute = false;
|
|
1273
1340
|
// battle.last_damage is not updated with the amount of HP the Substitute had
|
|
1274
1341
|
try options.log.end(.{ battle.active(sub_player), .Substitute });
|
|
1275
|
-
options.calc.capped(
|
|
1342
|
+
if (reason != .Confusion) options.calc.capped(sub_player);
|
|
1276
1343
|
return true;
|
|
1277
1344
|
} else {
|
|
1278
1345
|
// Safe to truncate since less than subbed.volatiles.substitute which is a u8
|
|
@@ -1306,7 +1373,7 @@ fn mirrorMove(
|
|
|
1306
1373
|
try options.log.fail(.{ battle.active(player), .None });
|
|
1307
1374
|
return null;
|
|
1308
1375
|
} else if (!showdown or foe.last_used_move != .Struggle) {
|
|
1309
|
-
incrementPP(side, mslot);
|
|
1376
|
+
incrementPP(side, player, mslot, options);
|
|
1310
1377
|
}
|
|
1311
1378
|
|
|
1312
1379
|
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
|
|
@@ -1327,7 +1394,7 @@ fn metronome(
|
|
|
1327
1394
|
var side = battle.side(player);
|
|
1328
1395
|
|
|
1329
1396
|
side.last_selected_move = try Rolls.metronome(battle, player, options);
|
|
1330
|
-
incrementPP(side, mslot);
|
|
1397
|
+
incrementPP(side, player, mslot, options);
|
|
1331
1398
|
|
|
1332
1399
|
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
|
|
1333
1400
|
return null;
|
|
@@ -1339,7 +1406,10 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
|
|
|
1339
1406
|
var immune = false;
|
|
1340
1407
|
var mist = false;
|
|
1341
1408
|
|
|
1342
|
-
if (moveHit(battle, player, move, &immune, &mist, options))
|
|
1409
|
+
if (try moveHit(battle, player, move, &immune, &mist, options)) {
|
|
1410
|
+
if (!showdown) try options.chance.commit(player, .hit);
|
|
1411
|
+
return true;
|
|
1412
|
+
}
|
|
1343
1413
|
|
|
1344
1414
|
assert(!immune);
|
|
1345
1415
|
if (mist) {
|
|
@@ -1348,11 +1418,11 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
|
|
|
1348
1418
|
try options.log.activate(.{ foe_ident, .Mist });
|
|
1349
1419
|
try options.log.fail(.{ foe_ident, .None });
|
|
1350
1420
|
} else {
|
|
1351
|
-
try options.chance.commit(player, .miss);
|
|
1352
1421
|
try options.log.lastmiss(.{});
|
|
1353
1422
|
try options.log.miss(.{battle.active(player)});
|
|
1354
1423
|
}
|
|
1355
1424
|
|
|
1425
|
+
if (!showdown) try options.chance.commit(player, .miss);
|
|
1356
1426
|
return false;
|
|
1357
1427
|
}
|
|
1358
1428
|
|
|
@@ -1363,7 +1433,7 @@ fn moveHit(
|
|
|
1363
1433
|
immune: *bool,
|
|
1364
1434
|
mist: *bool,
|
|
1365
1435
|
options: anytype,
|
|
1366
|
-
) bool {
|
|
1436
|
+
) !bool {
|
|
1367
1437
|
var side = battle.side(player);
|
|
1368
1438
|
const foe = battle.foe(player);
|
|
1369
1439
|
|
|
@@ -1402,7 +1472,7 @@ fn moveHit(
|
|
|
1402
1472
|
const overwritten = overwrite and state > 0;
|
|
1403
1473
|
assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
|
|
1404
1474
|
|
|
1405
|
-
var accuracy = if (overwritten) state else
|
|
1475
|
+
var accuracy: u16 = if (overwritten) state else move.accuracy;
|
|
1406
1476
|
var boost = BOOSTS[@as(u4, @intCast(@as(i8, side.active.boosts.accuracy) + 6))];
|
|
1407
1477
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1408
1478
|
boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
|
|
@@ -1414,7 +1484,7 @@ fn moveHit(
|
|
|
1414
1484
|
if (overwrite) side.active.volatiles.state = accuracy;
|
|
1415
1485
|
|
|
1416
1486
|
// GLITCH: max accuracy is 255 so 1/256 chance of miss
|
|
1417
|
-
break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
|
|
1487
|
+
break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
|
|
1418
1488
|
};
|
|
1419
1489
|
|
|
1420
1490
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1425,13 +1495,17 @@ fn moveHit(
|
|
|
1425
1495
|
|
|
1426
1496
|
if (!miss) return true;
|
|
1427
1497
|
if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
|
|
1498
|
+
|
|
1428
1499
|
side.active.volatiles.Binding = false;
|
|
1500
|
+
options.chance.observe(.binding, player, .None);
|
|
1501
|
+
|
|
1429
1502
|
return false;
|
|
1430
1503
|
}
|
|
1431
1504
|
|
|
1432
1505
|
fn checkFaint(
|
|
1433
1506
|
battle: anytype,
|
|
1434
1507
|
player: Player,
|
|
1508
|
+
cap: bool,
|
|
1435
1509
|
options: anytype,
|
|
1436
1510
|
) @TypeOf(options.log).Error!?Result {
|
|
1437
1511
|
const side = battle.side(player);
|
|
@@ -1444,8 +1518,8 @@ fn checkFaint(
|
|
|
1444
1518
|
const foe_out = foe_fainted and findFirstAlive(foe) == 0;
|
|
1445
1519
|
const more = player_out or foe_out;
|
|
1446
1520
|
|
|
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;
|
|
1521
|
+
if (try faint(battle, player, !(more or foe_fainted), cap, options)) |r| return r;
|
|
1522
|
+
if (foe_fainted) if (try faint(battle, player.foe(), !more, false, options)) |r| return r;
|
|
1449
1523
|
|
|
1450
1524
|
assert(!side.active.volatiles.MultiHit);
|
|
1451
1525
|
assert(!foe.active.volatiles.MultiHit);
|
|
@@ -1466,7 +1540,7 @@ fn checkFaint(
|
|
|
1466
1540
|
return .{ .p1 = foe_choice, .p2 = .Switch };
|
|
1467
1541
|
}
|
|
1468
1542
|
|
|
1469
|
-
fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
|
|
1543
|
+
fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytype) !?Result {
|
|
1470
1544
|
var side = battle.side(player);
|
|
1471
1545
|
var foe = battle.foe(player);
|
|
1472
1546
|
assert(side.stored().hp == 0);
|
|
@@ -1475,7 +1549,7 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
|
|
|
1475
1549
|
assert(!foe_volatiles.MultiHit);
|
|
1476
1550
|
if (foe_volatiles.Bide) {
|
|
1477
1551
|
assert(!foe_volatiles.Thrashing and !foe_volatiles.Rage);
|
|
1478
|
-
foe_volatiles.state = if (
|
|
1552
|
+
foe_volatiles.state = if (pkmn.options.mod) 0 else foe_volatiles.state & 255;
|
|
1479
1553
|
if (foe_volatiles.state != 0) return Result.Error;
|
|
1480
1554
|
}
|
|
1481
1555
|
|
|
@@ -1500,7 +1574,9 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
|
|
|
1500
1574
|
}
|
|
1501
1575
|
|
|
1502
1576
|
try options.log.faint(.{ battle.active(player), done });
|
|
1503
|
-
|
|
1577
|
+
// We can only safely set capped if the faint occured as the direct result of an opponent's
|
|
1578
|
+
// move (as opposed to due to residual damage, recoil, Explosion, etc)
|
|
1579
|
+
if (cap) options.calc.capped(player);
|
|
1504
1580
|
return null;
|
|
1505
1581
|
}
|
|
1506
1582
|
|
|
@@ -1564,12 +1640,12 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
|
|
|
1564
1640
|
|
|
1565
1641
|
battle.turn += 1;
|
|
1566
1642
|
|
|
1567
|
-
if (
|
|
1643
|
+
if (pkmn.options.mod and pkmn.options.ebc and checkEBC(battle)) {
|
|
1568
1644
|
try options.log.tie(.{});
|
|
1569
1645
|
return Result.Tie;
|
|
1570
1646
|
}
|
|
1571
1647
|
|
|
1572
|
-
if (
|
|
1648
|
+
if (pkmn.options.mod and battle.turn >= 1000) {
|
|
1573
1649
|
try options.log.tie(.{});
|
|
1574
1650
|
return Result.Tie;
|
|
1575
1651
|
} else if (battle.turn >= 65535) {
|
|
@@ -1660,17 +1736,6 @@ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
|
|
|
1660
1736
|
}
|
|
1661
1737
|
}
|
|
1662
1738
|
|
|
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
1739
|
fn onBegin(
|
|
1675
1740
|
battle: anytype,
|
|
1676
1741
|
player: Player,
|
|
@@ -1679,7 +1744,6 @@ fn onBegin(
|
|
|
1679
1744
|
residual: *bool,
|
|
1680
1745
|
options: anytype,
|
|
1681
1746
|
) !void {
|
|
1682
|
-
assert(move.effect.onBegin());
|
|
1683
1747
|
return switch (move.effect) {
|
|
1684
1748
|
.Confusion => Effects.confusion(battle, player, move, options),
|
|
1685
1749
|
.Conversion => Effects.conversion(battle, player, options),
|
|
@@ -1702,7 +1766,6 @@ fn onBegin(
|
|
|
1702
1766
|
}
|
|
1703
1767
|
|
|
1704
1768
|
fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1705
|
-
assert(move.effect.onEnd());
|
|
1706
1769
|
return switch (move.effect) {
|
|
1707
1770
|
.Bide => Effects.bide(battle, player, options),
|
|
1708
1771
|
// zig fmt: off
|
|
@@ -1724,7 +1787,6 @@ fn alwaysHappens(
|
|
|
1724
1787
|
residual: *bool,
|
|
1725
1788
|
options: anytype,
|
|
1726
1789
|
) !void {
|
|
1727
|
-
assert(move.effect.alwaysHappens());
|
|
1728
1790
|
return switch (move.effect) {
|
|
1729
1791
|
.DrainHP, .DreamEater => Effects.drainHP(battle, player, options),
|
|
1730
1792
|
.Explode => Effects.explode(battle, player),
|
|
@@ -1760,8 +1822,10 @@ pub const Effects = struct {
|
|
|
1760
1822
|
|
|
1761
1823
|
side.active.volatiles.Bide = true;
|
|
1762
1824
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1825
|
+
|
|
1763
1826
|
side.active.volatiles.state = 0;
|
|
1764
1827
|
side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
1828
|
+
options.chance.observe(.attacking, player, .started);
|
|
1765
1829
|
|
|
1766
1830
|
try options.log.start(.{ battle.active(player), .Bide });
|
|
1767
1831
|
}
|
|
@@ -1823,12 +1887,14 @@ pub const Effects = struct {
|
|
|
1823
1887
|
if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
1824
1888
|
if (!try checkHit(battle, player, move, options)) return;
|
|
1825
1889
|
}
|
|
1826
|
-
try options.chance.commit(player, .hit);
|
|
1827
1890
|
}
|
|
1828
1891
|
|
|
1829
1892
|
if (foe.active.volatiles.Confusion) return;
|
|
1830
1893
|
foe.active.volatiles.Confusion = true;
|
|
1831
|
-
|
|
1894
|
+
|
|
1895
|
+
foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
1896
|
+
options.chance.observe(.confusion, player.foe(), .started);
|
|
1897
|
+
options.calc.confusion(player.foe());
|
|
1832
1898
|
|
|
1833
1899
|
try options.log.start(.{ battle.active(player.foe()), .Confusion });
|
|
1834
1900
|
}
|
|
@@ -1856,10 +1922,7 @@ pub const Effects = struct {
|
|
|
1856
1922
|
const foe_ident = battle.active(player.foe());
|
|
1857
1923
|
|
|
1858
1924
|
// 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
|
-
}
|
|
1925
|
+
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
1863
1926
|
|
|
1864
1927
|
if (volatiles.disable_move != 0) {
|
|
1865
1928
|
try options.log.fail(.{ foe_ident, .None });
|
|
@@ -1877,18 +1940,18 @@ pub const Effects = struct {
|
|
|
1877
1940
|
|
|
1878
1941
|
// Technically this is still considered simply a "miss" on the cartridge,
|
|
1879
1942
|
// but diverging from Pokémon Showdown here would mostly just be pedantic
|
|
1880
|
-
if (n == 0) {
|
|
1943
|
+
if (n == 0 or (pkmn.options.mod and err)) {
|
|
1881
1944
|
try options.log.fail(.{ foe_ident, .None });
|
|
1882
1945
|
return null;
|
|
1883
1946
|
} else if (err) {
|
|
1884
1947
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
1885
|
-
assert(!showdown);
|
|
1886
1948
|
return Result.Error;
|
|
1887
1949
|
}
|
|
1888
1950
|
|
|
1889
1951
|
volatiles.disable_move =
|
|
1890
1952
|
@intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
|
|
1891
1953
|
volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
|
|
1954
|
+
options.chance.observe(.disable, player.foe(), .started);
|
|
1892
1955
|
|
|
1893
1956
|
const id = foe.active.move(volatiles.disable_move).id;
|
|
1894
1957
|
try options.log.start(.{ foe_ident, .Disable, id });
|
|
@@ -1959,7 +2022,7 @@ pub const Effects = struct {
|
|
|
1959
2022
|
if (foe.active.types.includes(move.type)) return;
|
|
1960
2023
|
if (!try Rolls.secondaryChance(battle, player, true, options)) return;
|
|
1961
2024
|
// Freeze Clause Mod
|
|
1962
|
-
if (
|
|
2025
|
+
if (pkmn.options.mod) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
1963
2026
|
|
|
1964
2027
|
foe_stored.status = Status.init(.FRZ);
|
|
1965
2028
|
// GLITCH: Hyper Beam recharging status is not cleared
|
|
@@ -1972,7 +2035,7 @@ pub const Effects = struct {
|
|
|
1972
2035
|
var side = battle.side(player);
|
|
1973
2036
|
var foe = battle.foe(player);
|
|
1974
2037
|
|
|
1975
|
-
|
|
2038
|
+
const side_stored = side.stored();
|
|
1976
2039
|
var foe_stored = foe.stored();
|
|
1977
2040
|
|
|
1978
2041
|
const player_ident = battle.active(player);
|
|
@@ -1995,22 +2058,24 @@ pub const Effects = struct {
|
|
|
1995
2058
|
if (p != player and Status.any(s.stored().status)) {
|
|
1996
2059
|
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
1997
2060
|
s.stored().status = 0;
|
|
2061
|
+
options.chance.observe(.sleep, p, .None);
|
|
1998
2062
|
} else if (showdown and s.stored().status == Status.TOX) {
|
|
1999
2063
|
s.stored().status = Status.init(.PSN);
|
|
2000
2064
|
try log.status(.{ battle.active(p), s.stored().status, .None });
|
|
2001
2065
|
}
|
|
2002
|
-
try clearVolatiles(battle, p,
|
|
2066
|
+
try clearVolatiles(battle, p, options);
|
|
2003
2067
|
}
|
|
2004
2068
|
} else {
|
|
2005
2069
|
if (Status.any(foe_stored.status)) {
|
|
2006
2070
|
if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
|
|
2007
2071
|
foe.last_selected_move = .SKIP_TURN;
|
|
2072
|
+
options.chance.observe(.sleep, player.foe(), .None);
|
|
2008
2073
|
}
|
|
2009
2074
|
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
2010
2075
|
foe_stored.status = 0;
|
|
2011
2076
|
}
|
|
2012
|
-
try clearVolatiles(battle, player,
|
|
2013
|
-
try clearVolatiles(battle, player.foe(),
|
|
2077
|
+
try clearVolatiles(battle, player, options);
|
|
2078
|
+
try clearVolatiles(battle, player.foe(), options);
|
|
2014
2079
|
}
|
|
2015
2080
|
}
|
|
2016
2081
|
|
|
@@ -2061,7 +2126,6 @@ pub const Effects = struct {
|
|
|
2061
2126
|
}
|
|
2062
2127
|
}
|
|
2063
2128
|
|
|
2064
|
-
try options.chance.commit(player, .hit);
|
|
2065
2129
|
foe.active.volatiles.LeechSeed = true;
|
|
2066
2130
|
|
|
2067
2131
|
try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
|
|
@@ -2102,12 +2166,10 @@ pub const Effects = struct {
|
|
|
2102
2166
|
if (!has_mimic) {
|
|
2103
2167
|
// Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
|
|
2104
2168
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2105
|
-
try options.chance.commit(player, .hit);
|
|
2106
2169
|
return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
2107
2170
|
}
|
|
2108
2171
|
}
|
|
2109
2172
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2110
|
-
try options.chance.commit(player, .hit);
|
|
2111
2173
|
|
|
2112
2174
|
const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
|
|
2113
2175
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
@@ -2161,7 +2223,6 @@ pub const Effects = struct {
|
|
|
2161
2223
|
if (immune) return log.immune(.{ foe_ident, .None });
|
|
2162
2224
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2163
2225
|
}
|
|
2164
|
-
try options.chance.commit(player, .hit);
|
|
2165
2226
|
|
|
2166
2227
|
foe_stored.status = Status.init(.PAR);
|
|
2167
2228
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
@@ -2229,7 +2290,6 @@ pub const Effects = struct {
|
|
|
2229
2290
|
|
|
2230
2291
|
if (move.effect == .Poison) {
|
|
2231
2292
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2232
|
-
try options.chance.commit(player, .hit);
|
|
2233
2293
|
} else {
|
|
2234
2294
|
if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
|
|
2235
2295
|
return;
|
|
@@ -2298,13 +2358,12 @@ pub const Effects = struct {
|
|
|
2298
2358
|
if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
|
|
2299
2359
|
});
|
|
2300
2360
|
}
|
|
2301
|
-
// If
|
|
2361
|
+
// If moveHit in doMove didn't return true Pokémon Showdown wouldn't be in here
|
|
2302
2362
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2303
|
-
try options.chance.commit(player, .hit);
|
|
2304
2363
|
}
|
|
2305
2364
|
|
|
2306
2365
|
// Sleep Clause Mod
|
|
2307
|
-
if (
|
|
2366
|
+
if (pkmn.options.mod) {
|
|
2308
2367
|
for (foe.pokemon) |p| {
|
|
2309
2368
|
if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
|
|
2310
2369
|
}
|
|
@@ -2312,6 +2371,8 @@ pub const Effects = struct {
|
|
|
2312
2371
|
foe.active.volatiles.Recharging = false;
|
|
2313
2372
|
|
|
2314
2373
|
foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
|
|
2374
|
+
options.chance.observe(.sleep, player.foe(), .started);
|
|
2375
|
+
|
|
2315
2376
|
const last = battle.side(player).last_selected_move;
|
|
2316
2377
|
try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
|
|
2317
2378
|
}
|
|
@@ -2353,9 +2414,7 @@ pub const Effects = struct {
|
|
|
2353
2414
|
// Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
|
|
2354
2415
|
if (showdown) {
|
|
2355
2416
|
if (battle.side(player).last_selected_move == .Teleport) return;
|
|
2356
|
-
|
|
2357
|
-
try options.chance.commit(player, .hit);
|
|
2358
|
-
}
|
|
2417
|
+
_ = try checkHit(battle, player, move, options);
|
|
2359
2418
|
battle.last_damage = 0;
|
|
2360
2419
|
} else {
|
|
2361
2420
|
try options.log.fail(.{ battle.active(player), .None });
|
|
@@ -2370,6 +2429,8 @@ pub const Effects = struct {
|
|
|
2370
2429
|
|
|
2371
2430
|
volatiles.Thrashing = true;
|
|
2372
2431
|
volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
2432
|
+
|
|
2433
|
+
options.chance.observe(.attacking, player, .started);
|
|
2373
2434
|
}
|
|
2374
2435
|
|
|
2375
2436
|
fn transform(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -2408,13 +2469,14 @@ pub const Effects = struct {
|
|
|
2408
2469
|
if (side.active.volatiles.Binding) return;
|
|
2409
2470
|
side.active.volatiles.Binding = true;
|
|
2410
2471
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2411
|
-
// (Pokémon Showdown
|
|
2472
|
+
// (Pokémon Showdown clears Recharging in onTryMove instead)
|
|
2412
2473
|
if (!showdown) {
|
|
2413
2474
|
foe.active.volatiles.Recharging = false;
|
|
2414
2475
|
} else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
|
|
2415
2476
|
|
|
2416
2477
|
side.active.volatiles.attacks =
|
|
2417
2478
|
try Rolls.distribution(battle, .Binding, player, options) - 1;
|
|
2479
|
+
options.chance.observe(.binding, player, .started);
|
|
2418
2480
|
}
|
|
2419
2481
|
|
|
2420
2482
|
fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2441,7 +2503,7 @@ pub const Effects = struct {
|
|
|
2441
2503
|
}
|
|
2442
2504
|
return log.fail(.{ ident, .None });
|
|
2443
2505
|
}
|
|
2444
|
-
|
|
2506
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
|
|
2445
2507
|
const stat = unmodifiedStats(battle, side).atk;
|
|
2446
2508
|
stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2447
2509
|
try log.boost(.{ ident, reason, n });
|
|
@@ -2461,7 +2523,7 @@ pub const Effects = struct {
|
|
|
2461
2523
|
}
|
|
2462
2524
|
return log.fail(.{ ident, .None });
|
|
2463
2525
|
}
|
|
2464
|
-
|
|
2526
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
|
|
2465
2527
|
const stat = unmodifiedStats(battle, side).def;
|
|
2466
2528
|
stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2467
2529
|
try log.boost(.{ ident, .Defense, n });
|
|
@@ -2478,7 +2540,7 @@ pub const Effects = struct {
|
|
|
2478
2540
|
}
|
|
2479
2541
|
return log.fail(.{ ident, .None });
|
|
2480
2542
|
}
|
|
2481
|
-
|
|
2543
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
|
|
2482
2544
|
const stat = unmodifiedStats(battle, side).spe;
|
|
2483
2545
|
stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2484
2546
|
try log.boost(.{ ident, .Speed, 2 });
|
|
@@ -2498,7 +2560,7 @@ pub const Effects = struct {
|
|
|
2498
2560
|
}
|
|
2499
2561
|
return log.fail(.{ ident, .None });
|
|
2500
2562
|
}
|
|
2501
|
-
|
|
2563
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
|
|
2502
2564
|
const stat = unmodifiedStats(battle, side).spc;
|
|
2503
2565
|
stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2504
2566
|
try log.boost(.{ ident, .SpecialAttack, n });
|
|
@@ -2531,7 +2593,6 @@ pub const Effects = struct {
|
|
|
2531
2593
|
} else {
|
|
2532
2594
|
// checkHit already checks for Invulnerable
|
|
2533
2595
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2534
|
-
try options.chance.commit(player, .hit);
|
|
2535
2596
|
}
|
|
2536
2597
|
|
|
2537
2598
|
var stats = &foe.active.stats;
|
|
@@ -2551,7 +2612,7 @@ pub const Effects = struct {
|
|
|
2551
2612
|
}
|
|
2552
2613
|
return log.fail(.{ foe_ident, .None });
|
|
2553
2614
|
}
|
|
2554
|
-
|
|
2615
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
|
|
2555
2616
|
const stat = unmodifiedStats(battle, foe).atk;
|
|
2556
2617
|
stats.atk = @max(1, stat * mod[0] / mod[1]);
|
|
2557
2618
|
try log.boost(.{ foe_ident, .Attack, -1 });
|
|
@@ -2569,7 +2630,7 @@ pub const Effects = struct {
|
|
|
2569
2630
|
}
|
|
2570
2631
|
return log.fail(.{ foe_ident, .None });
|
|
2571
2632
|
}
|
|
2572
|
-
|
|
2633
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
|
|
2573
2634
|
const stat = unmodifiedStats(battle, foe).def;
|
|
2574
2635
|
stats.def = @max(1, stat * mod[0] / mod[1]);
|
|
2575
2636
|
try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
|
|
@@ -2586,7 +2647,7 @@ pub const Effects = struct {
|
|
|
2586
2647
|
}
|
|
2587
2648
|
return log.fail(.{ foe_ident, .None });
|
|
2588
2649
|
}
|
|
2589
|
-
|
|
2650
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
|
|
2590
2651
|
const stat = unmodifiedStats(battle, foe).spe;
|
|
2591
2652
|
stats.spe = @max(1, stat * mod[0] / mod[1]);
|
|
2592
2653
|
try log.boost(.{ foe_ident, .Speed, -1 });
|
|
@@ -2606,7 +2667,7 @@ pub const Effects = struct {
|
|
|
2606
2667
|
}
|
|
2607
2668
|
return log.fail(.{ foe_ident, .None });
|
|
2608
2669
|
}
|
|
2609
|
-
|
|
2670
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
|
|
2610
2671
|
const stat = unmodifiedStats(battle, foe).spc;
|
|
2611
2672
|
stats.spc = @max(1, stat * mod[0] / mod[1]);
|
|
2612
2673
|
try log.boost(.{ foe_ident, .SpecialAttack, -1 });
|
|
@@ -2645,22 +2706,22 @@ fn isForced(active: anytype) bool {
|
|
|
2645
2706
|
active.volatiles.Thrashing or active.volatiles.Charging;
|
|
2646
2707
|
}
|
|
2647
2708
|
|
|
2648
|
-
fn clearVolatiles(battle: anytype, who: Player,
|
|
2709
|
+
fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
|
|
2649
2710
|
var log = options.log;
|
|
2650
2711
|
var side = battle.side(who);
|
|
2651
2712
|
var volatiles = &side.active.volatiles;
|
|
2652
2713
|
const ident = battle.active(who);
|
|
2653
2714
|
|
|
2654
|
-
options.chance.clearDurations(who, clear);
|
|
2655
|
-
|
|
2656
2715
|
if (volatiles.disable_move != 0) {
|
|
2657
2716
|
volatiles.disable_move = 0;
|
|
2658
2717
|
volatiles.disable_duration = 0;
|
|
2718
|
+
options.chance.observe(.disable, who, .None);
|
|
2659
2719
|
try log.end(.{ ident, .DisableSilent });
|
|
2660
2720
|
}
|
|
2661
2721
|
if (volatiles.Confusion) {
|
|
2662
2722
|
// volatiles.confusion is left unchanged
|
|
2663
2723
|
volatiles.Confusion = false;
|
|
2724
|
+
options.chance.observe(.confusion, who, .None);
|
|
2664
2725
|
try log.end(.{ ident, .ConfusionSilent });
|
|
2665
2726
|
}
|
|
2666
2727
|
if (volatiles.Mist) {
|
|
@@ -2695,10 +2756,25 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
|
|
|
2695
2756
|
}
|
|
2696
2757
|
}
|
|
2697
2758
|
|
|
2759
|
+
fn decrement(
|
|
2760
|
+
comptime field: chance.Action.Field,
|
|
2761
|
+
player: Player,
|
|
2762
|
+
options: anytype,
|
|
2763
|
+
n: anytype,
|
|
2764
|
+
) @TypeOf(n) {
|
|
2765
|
+
return if (options.calc.overridden(player, field)) |obs| switch (obs) {
|
|
2766
|
+
.started, .ended => 0,
|
|
2767
|
+
.continuing => if (n > 1) n - 1 else n,
|
|
2768
|
+
else => unreachable,
|
|
2769
|
+
} else n - 1;
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2698
2772
|
pub const Rolls = struct {
|
|
2699
2773
|
fn speedTie(battle: anytype, options: anytype) !bool {
|
|
2700
2774
|
const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
|
|
2701
2775
|
player == .P1
|
|
2776
|
+
else if (@hasDecl(@TypeOf(battle.rng), "speedTie"))
|
|
2777
|
+
battle.rng.speedTie()
|
|
2702
2778
|
else if (showdown)
|
|
2703
2779
|
battle.rng.range(u8, 0, 2) == 0
|
|
2704
2780
|
else
|
|
@@ -2708,21 +2784,31 @@ pub const Rolls = struct {
|
|
|
2708
2784
|
return p1;
|
|
2709
2785
|
}
|
|
2710
2786
|
|
|
2711
|
-
fn criticalHit(
|
|
2787
|
+
fn criticalHit(
|
|
2788
|
+
battle: anytype,
|
|
2789
|
+
player: Player,
|
|
2790
|
+
rate: u8,
|
|
2791
|
+
spurious: bool,
|
|
2792
|
+
options: anytype,
|
|
2793
|
+
) !bool {
|
|
2712
2794
|
const crit = if (options.calc.overridden(player, .critical_hit)) |val|
|
|
2713
2795
|
val == .true
|
|
2796
|
+
else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
|
|
2797
|
+
battle.rng.criticalHit(player, rate)
|
|
2714
2798
|
else if (showdown)
|
|
2715
2799
|
battle.rng.chance(u8, rate, 256)
|
|
2716
2800
|
else
|
|
2717
2801
|
std.math.rotl(u8, battle.rng.next(), 3) < rate;
|
|
2718
2802
|
|
|
2719
|
-
try options.chance.criticalHit(player, crit, rate);
|
|
2803
|
+
if (!spurious) try options.chance.criticalHit(player, crit, rate);
|
|
2720
2804
|
return crit;
|
|
2721
2805
|
}
|
|
2722
2806
|
|
|
2723
2807
|
fn damage(battle: anytype, player: Player, options: anytype) !u8 {
|
|
2724
2808
|
const roll = if (options.calc.overridden(player, .damage)) |val|
|
|
2725
2809
|
val
|
|
2810
|
+
else if (@hasDecl(@TypeOf(battle.rng), "damage"))
|
|
2811
|
+
battle.rng.damage(player)
|
|
2726
2812
|
else roll: {
|
|
2727
2813
|
if (showdown) break :roll battle.rng.range(u8, 217, 256);
|
|
2728
2814
|
while (true) {
|
|
@@ -2736,21 +2822,27 @@ pub const Rolls = struct {
|
|
|
2736
2822
|
return roll;
|
|
2737
2823
|
}
|
|
2738
2824
|
|
|
2739
|
-
|
|
2825
|
+
const uN = if (pkmn.options.miss) u8 else u9;
|
|
2826
|
+
fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) !bool {
|
|
2827
|
+
const acc: uN = if (pkmn.options.miss) accuracy else @as(uN, accuracy) + 1;
|
|
2740
2828
|
const ok = if (options.calc.overridden(player, .hit)) |val|
|
|
2741
2829
|
val == .true
|
|
2830
|
+
else if (@hasDecl(@TypeOf(battle.rng), "hit"))
|
|
2831
|
+
battle.rng.hit(player, acc)
|
|
2742
2832
|
else if (showdown)
|
|
2743
|
-
battle.rng.chance(
|
|
2833
|
+
battle.rng.chance(uN, acc, 256)
|
|
2744
2834
|
else
|
|
2745
|
-
battle.rng.next() <
|
|
2835
|
+
battle.rng.next() < acc;
|
|
2746
2836
|
|
|
2747
|
-
options.chance.hit(ok,
|
|
2837
|
+
try options.chance.hit(player, ok, acc);
|
|
2748
2838
|
return ok;
|
|
2749
2839
|
}
|
|
2750
2840
|
|
|
2751
2841
|
fn confused(battle: anytype, player: Player, options: anytype) !bool {
|
|
2752
2842
|
const cfz = if (options.calc.overridden(player, .confused)) |val|
|
|
2753
2843
|
val == .true
|
|
2844
|
+
else if (@hasDecl(@TypeOf(battle.rng), "confused"))
|
|
2845
|
+
battle.rng.confused(player)
|
|
2754
2846
|
else if (showdown)
|
|
2755
2847
|
!battle.rng.chance(u8, 128, 256)
|
|
2756
2848
|
else
|
|
@@ -2763,6 +2855,8 @@ pub const Rolls = struct {
|
|
|
2763
2855
|
fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
|
|
2764
2856
|
const par = if (options.calc.overridden(player, .paralyzed)) |val|
|
|
2765
2857
|
val == .true
|
|
2858
|
+
else if (@hasDecl(@TypeOf(battle.rng), "paralyzed"))
|
|
2859
|
+
battle.rng.paralyzed(player)
|
|
2766
2860
|
else if (showdown)
|
|
2767
2861
|
battle.rng.chance(u8, 63, 256)
|
|
2768
2862
|
else
|
|
@@ -2775,6 +2869,8 @@ pub const Rolls = struct {
|
|
|
2775
2869
|
fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
|
|
2776
2870
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2777
2871
|
val == .true
|
|
2872
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2873
|
+
battle.rng.secondaryChance(player, 25)
|
|
2778
2874
|
else if (showdown)
|
|
2779
2875
|
battle.rng.chance(u8, 25, 256)
|
|
2780
2876
|
else
|
|
@@ -2789,6 +2885,8 @@ pub const Rolls = struct {
|
|
|
2789
2885
|
|
|
2790
2886
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2791
2887
|
val == .true
|
|
2888
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2889
|
+
battle.rng.secondaryChance(player, rate)
|
|
2792
2890
|
else if (showdown)
|
|
2793
2891
|
battle.rng.chance(u8, rate, 256)
|
|
2794
2892
|
else
|
|
@@ -2803,6 +2901,8 @@ pub const Rolls = struct {
|
|
|
2803
2901
|
|
|
2804
2902
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2805
2903
|
val == .true
|
|
2904
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2905
|
+
battle.rng.secondaryChance(player, rate)
|
|
2806
2906
|
else if (showdown)
|
|
2807
2907
|
battle.rng.chance(u8, rate, 256)
|
|
2808
2908
|
else
|
|
@@ -2815,6 +2915,8 @@ pub const Rolls = struct {
|
|
|
2815
2915
|
fn unboost(battle: anytype, player: Player, options: anytype) !bool {
|
|
2816
2916
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2817
2917
|
val == .true
|
|
2918
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2919
|
+
battle.rng.secondaryChance(player, 85)
|
|
2818
2920
|
else if (showdown)
|
|
2819
2921
|
battle.rng.chance(u8, 85, 256)
|
|
2820
2922
|
else
|
|
@@ -2827,6 +2929,8 @@ pub const Rolls = struct {
|
|
|
2827
2929
|
fn metronome(battle: anytype, player: Player, options: anytype) !Move {
|
|
2828
2930
|
const move: Move = if (options.calc.overridden(player, .metronome)) |val|
|
|
2829
2931
|
val
|
|
2932
|
+
else if (@hasDecl(@TypeOf(battle.rng), "metronome"))
|
|
2933
|
+
battle.rng.metronome(player)
|
|
2830
2934
|
else if (showdown)
|
|
2831
2935
|
Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
|
|
2832
2936
|
else move: {
|
|
@@ -2845,6 +2949,8 @@ pub const Rolls = struct {
|
|
|
2845
2949
|
fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
|
|
2846
2950
|
const power = if (options.calc.overridden(player, .psywave)) |val|
|
|
2847
2951
|
val - 1
|
|
2952
|
+
else if (@hasDecl(@TypeOf(battle.rng), "psywave"))
|
|
2953
|
+
battle.rng.psywave(player)
|
|
2848
2954
|
else if (showdown)
|
|
2849
2955
|
battle.rng.range(u8, 0, max)
|
|
2850
2956
|
else power: {
|
|
@@ -2860,8 +2966,11 @@ pub const Rolls = struct {
|
|
|
2860
2966
|
}
|
|
2861
2967
|
|
|
2862
2968
|
fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2863
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2864
|
-
@intCast(val)
|
|
2969
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2970
|
+
if (val >= 1 and val <= 7) @as(u3, @intCast(val)) else null
|
|
2971
|
+
else
|
|
2972
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
|
|
2973
|
+
battle.rng.sleepDuration(player)
|
|
2865
2974
|
else if (showdown)
|
|
2866
2975
|
battle.rng.range(u3, 1, 8)
|
|
2867
2976
|
else duration: {
|
|
@@ -2872,46 +2981,55 @@ pub const Rolls = struct {
|
|
|
2872
2981
|
};
|
|
2873
2982
|
|
|
2874
2983
|
assert(duration >= 1 and duration <= 7);
|
|
2875
|
-
options.chance.duration(
|
|
2984
|
+
options.chance.duration(player, duration);
|
|
2876
2985
|
return duration;
|
|
2877
2986
|
}
|
|
2878
2987
|
|
|
2879
2988
|
fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
|
|
2880
|
-
const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
|
|
2881
|
-
val
|
|
2989
|
+
const duration: u4 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2990
|
+
if (val >= 1 and val <= 8) val else null
|
|
2991
|
+
else
|
|
2992
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
|
|
2993
|
+
battle.rng.disableDuration(player)
|
|
2882
2994
|
else if (showdown)
|
|
2883
2995
|
battle.rng.range(u4, 1, 9)
|
|
2884
2996
|
else
|
|
2885
2997
|
@intCast((battle.rng.next() & 7) + 1);
|
|
2886
2998
|
|
|
2887
2999
|
assert(duration >= 1 and duration <= 8);
|
|
2888
|
-
options.chance.duration(
|
|
3000
|
+
options.chance.duration(player, duration);
|
|
2889
3001
|
return duration;
|
|
2890
3002
|
}
|
|
2891
3003
|
|
|
2892
|
-
fn confusionDuration(battle: anytype, player: Player,
|
|
2893
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2894
|
-
@intCast(val)
|
|
3004
|
+
fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
3005
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
3006
|
+
if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
|
|
3007
|
+
else
|
|
3008
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
|
|
3009
|
+
battle.rng.confusionDuration(player)
|
|
2895
3010
|
else if (showdown)
|
|
2896
3011
|
battle.rng.range(u3, 2, 6)
|
|
2897
3012
|
else
|
|
2898
3013
|
@intCast((battle.rng.next() & 3) + 2);
|
|
2899
3014
|
|
|
2900
3015
|
assert(duration >= 2 and duration <= 5);
|
|
2901
|
-
options.chance.duration(
|
|
3016
|
+
options.chance.duration(player, duration);
|
|
2902
3017
|
return duration;
|
|
2903
3018
|
}
|
|
2904
3019
|
|
|
2905
3020
|
fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2906
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2907
|
-
@intCast(val)
|
|
3021
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
3022
|
+
if (val >= 2 and val <= 3) @as(u3, @intCast(val)) else null
|
|
3023
|
+
else
|
|
3024
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
|
|
3025
|
+
battle.rng.attackingDuration(player)
|
|
2908
3026
|
else if (showdown)
|
|
2909
3027
|
battle.rng.range(u3, 2, 4)
|
|
2910
3028
|
else
|
|
2911
3029
|
@intCast((battle.rng.next() & 1) + 2);
|
|
2912
3030
|
|
|
2913
3031
|
assert(duration >= 2 and duration <= 3);
|
|
2914
|
-
options.chance.duration(
|
|
3032
|
+
options.chance.duration(player, duration);
|
|
2915
3033
|
return duration;
|
|
2916
3034
|
}
|
|
2917
3035
|
|
|
@@ -2924,8 +3042,11 @@ pub const Rolls = struct {
|
|
|
2924
3042
|
options: anytype,
|
|
2925
3043
|
) !u3 {
|
|
2926
3044
|
const roll = if (effect == .Binding) .duration else .multi_hit;
|
|
2927
|
-
const n: u3 = if (options.calc.overridden(player, roll)) |val|
|
|
2928
|
-
@intCast(val)
|
|
3045
|
+
const n: u3 = (if (options.calc.overridden(player, roll)) |val|
|
|
3046
|
+
if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
|
|
3047
|
+
else
|
|
3048
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "distribution"))
|
|
3049
|
+
battle.rng.distribution(player)
|
|
2929
3050
|
else if (showdown)
|
|
2930
3051
|
DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
|
|
2931
3052
|
else n: {
|
|
@@ -2935,7 +3056,7 @@ pub const Rolls = struct {
|
|
|
2935
3056
|
|
|
2936
3057
|
assert(n >= 2 and n <= 5);
|
|
2937
3058
|
if (effect == .Binding) {
|
|
2938
|
-
options.chance.duration(
|
|
3059
|
+
options.chance.duration(player, n);
|
|
2939
3060
|
} else {
|
|
2940
3061
|
try options.chance.multiHit(player, n);
|
|
2941
3062
|
}
|
|
@@ -2950,15 +3071,15 @@ pub const Rolls = struct {
|
|
|
2950
3071
|
options: anytype,
|
|
2951
3072
|
) !u4 {
|
|
2952
3073
|
// TODO: consider throwing error instead of rerolling?
|
|
2953
|
-
const
|
|
3074
|
+
const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
|
|
2954
3075
|
if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
|
|
2955
3076
|
val
|
|
2956
3077
|
else
|
|
2957
3078
|
null
|
|
2958
3079
|
else
|
|
2959
|
-
null
|
|
2960
|
-
|
|
2961
|
-
|
|
3080
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
|
|
3081
|
+
battle.rng.moveSlot(player, moves, check_pp)
|
|
3082
|
+
else slot: {
|
|
2962
3083
|
if (showdown) {
|
|
2963
3084
|
if (check_pp == 0) {
|
|
2964
3085
|
var i: usize = moves.len;
|
|
@@ -3038,11 +3159,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3038
3159
|
const side = battle.side(player);
|
|
3039
3160
|
const foe = battle.foe(player);
|
|
3040
3161
|
|
|
3041
|
-
|
|
3162
|
+
const active = &side.active;
|
|
3042
3163
|
const stored = side.stored();
|
|
3043
3164
|
|
|
3044
3165
|
// 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
|
|
3166
|
+
// Pokémon Showdown instead produces a list with a single move that must be chosen
|
|
3046
3167
|
//
|
|
3047
3168
|
// Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
|
|
3048
3169
|
// here when not in compatibility mode, but the engine needs to be able to differentiate
|
|
@@ -3050,7 +3171,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3050
3171
|
// passing due to being forced into using a move. Instead of introducing another option
|
|
3051
3172
|
// we simply repurpose Move with no move slot, even though pedantically this is not
|
|
3052
3173
|
// strictly correct as the player would not have been presented the option to move or
|
|
3053
|
-
// switch at all
|
|
3174
|
+
// switch at all
|
|
3054
3175
|
if (isForced(active)) {
|
|
3055
3176
|
out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
|
|
3056
3177
|
n += 1;
|
|
@@ -3069,7 +3190,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3069
3190
|
// On the cartridge, all of these happen after "FIGHT" (indicating you are not
|
|
3070
3191
|
// switching) but before you are allowed to select a move. Pokémon Showdown instead
|
|
3071
3192
|
// either disables all other moves in the case of limited or requires you to select a
|
|
3072
|
-
// move normally if sleeping/frozen/bound
|
|
3193
|
+
// move normally if sleeping/frozen/bound
|
|
3073
3194
|
if (!showdown and (limited or foe.active.volatiles.Binding or
|
|
3074
3195
|
Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
|
|
3075
3196
|
{
|
|
@@ -3082,7 +3203,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3082
3203
|
// Pokémon Showdown handles Bide and Binding moves by checking if the move in question
|
|
3083
3204
|
// is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
|
|
3084
3205
|
// 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
|
|
3206
|
+
// proc-ed move in their moveset) and disabling all other moves
|
|
3086
3207
|
if (limited) {
|
|
3087
3208
|
assert(showdown);
|
|
3088
3209
|
assert(side.last_selected_move != .None);
|
|
@@ -3114,7 +3235,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3114
3235
|
// Struggle (Pokémon Showdown would use 'move 1' here)
|
|
3115
3236
|
if (n == before) {
|
|
3116
3237
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
3117
|
-
if (!
|
|
3238
|
+
if (!pkmn.options.mod) {
|
|
3118
3239
|
while (slot <= 4) : (slot += 1) {
|
|
3119
3240
|
if (active.moves[slot - 1].pp != 0) return n;
|
|
3120
3241
|
}
|