@pkmn/engine 0.1.0-dev.15f64810 → 0.1.0-dev.1c183b1a
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 -1
- package/README.md +28 -11
- 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 +26 -4
- package/build/pkg/addon.js +34 -65
- package/build/pkg/addon.js.map +1 -1
- package/build/pkg/data/layout.json +15 -2
- package/build/pkg/data/protocol.json +23 -9
- package/build/pkg/data.d.ts +23 -1
- package/build/pkg/data.js.map +1 -1
- package/build/pkg/gen1.d.ts +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 +29 -1
- package/build/pkg/protocol.js +105 -103
- package/build/pkg/protocol.js.map +1 -1
- package/build/tools/debug.js +83 -73
- package/build/tools/debug.js.map +1 -1
- package/build/tools/display/debug.jsx +66 -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 +360 -0
- package/build/tools/display/ui.jsx.map +1 -0
- package/build/tools/display/util.js +157 -0
- package/build/tools/display/util.js.map +1 -0
- package/build.zig +255 -102
- package/package.json +59 -138
- package/src/bin/install-pkmn-engine +25 -7
- package/src/bin/pkmn-debug +1 -1
- package/src/lib/bindings/c.zig +200 -0
- package/src/lib/bindings/node.zig +117 -0
- package/src/lib/bindings/wasm.zig +50 -0
- package/src/lib/common/array.zig +70 -0
- package/src/lib/common/data.zig +8 -8
- package/src/lib/common/debug.zig +33 -28
- package/src/lib/common/js.zig +3 -7
- package/src/lib/common/optional.zig +11 -17
- package/src/lib/common/options.zig +4 -0
- package/src/lib/common/protocol.zig +196 -160
- package/src/lib/common/rational.zig +338 -254
- package/src/lib/common/rng.zig +5 -7
- package/src/lib/common/util.zig +33 -1
- package/src/lib/gen1/calc.zig +651 -205
- package/src/lib/gen1/chance.zig +412 -249
- package/src/lib/gen1/data/moves.zig +2 -4
- package/src/lib/gen1/data/species.zig +1 -3
- package/src/lib/gen1/data/types.zig +3 -4
- package/src/lib/gen1/data.zig +40 -43
- package/src/lib/gen1/helpers.zig +17 -562
- package/src/lib/gen1/mechanics.zig +328 -229
- package/src/lib/gen2/calc.zig +35 -28
- package/src/lib/gen2/chance.zig +91 -510
- 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 +7 -5
- package/src/lib/gen2/data.zig +33 -35
- package/src/lib/gen2/generated.zig +18 -23
- package/src/lib/gen2/helpers.zig +9 -20
- package/src/lib/gen2/mechanics.zig +105 -132
- package/src/lib/napi.h +2 -0
- package/src/lib/node.zig +4 -121
- package/src/lib/pkmn.zig +103 -22
- package/src/lib/wasm.zig +9 -9
- package/src/tools/display/dom.ts +108 -0
- package/src/tools/display/ui.css +354 -0
- package/build/test/display.js +0 -400
- package/build/test/display.js.map +0 -1
- package/src/test/display/pkmn.html.tmpl +0 -368
|
@@ -1,37 +1,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;
|
|
@@ -92,11 +84,11 @@ fn start(battle: anytype, options: anytype) !Result {
|
|
|
92
84
|
|
|
93
85
|
const p1_slot = findFirstAlive(p1);
|
|
94
86
|
assert(!showdown or p1_slot == 1);
|
|
95
|
-
if (p1_slot == 0) return if (findFirstAlive(p2) == 0)
|
|
87
|
+
if (p1_slot == 0) return if (findFirstAlive(p2) == 0) .Tie else .Lose;
|
|
96
88
|
|
|
97
89
|
const p2_slot = findFirstAlive(p2);
|
|
98
90
|
assert(!showdown or p2_slot == 1);
|
|
99
|
-
if (p2_slot == 0) return
|
|
91
|
+
if (p2_slot == 0) return .Win;
|
|
100
92
|
|
|
101
93
|
try switchIn(battle, .P1, p1_slot, true, options);
|
|
102
94
|
try switchIn(battle, .P2, p2_slot, true, options);
|
|
@@ -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 .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,7 +420,7 @@ 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);
|
|
@@ -437,7 +432,7 @@ fn executeMove(
|
|
|
437
432
|
// technically be from this Pokémon because it must be exactly 1 to not desync and
|
|
438
433
|
// every Pokémon must have at least one move
|
|
439
434
|
if (mslot != 1 or side.active.move(mslot).id != side.last_selected_move) {
|
|
440
|
-
return
|
|
435
|
+
return .Error;
|
|
441
436
|
} else {
|
|
442
437
|
auto = true;
|
|
443
438
|
}
|
|
@@ -455,7 +450,26 @@ fn executeMove(
|
|
|
455
450
|
.skip_can => skip_can = true,
|
|
456
451
|
.skip_pp => skip_pp = true,
|
|
457
452
|
.ok => {},
|
|
458
|
-
.err => return
|
|
453
|
+
.err => return .Error,
|
|
454
|
+
}
|
|
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;
|
|
459
473
|
}
|
|
460
474
|
|
|
461
475
|
const can = skip_can or
|
|
@@ -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,19 +614,14 @@ 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
|
-
// Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
|
|
594
|
-
// on the targeting behavior of the confused Pokémon's selected move which results
|
|
595
|
-
// in the wrong behavior with respect to the Substitute + Confusion glitch
|
|
596
|
-
const target = if (showdown and Move.get(side.last_selected_move).target == .Self)
|
|
597
|
-
player
|
|
598
|
-
else
|
|
599
|
-
player.foe();
|
|
600
621
|
|
|
601
622
|
const uncapped = battle.last_damage;
|
|
602
623
|
// Skipping adjustDamage / randomizeDamage / checkHit
|
|
603
|
-
_ = try applyDamage(battle, player,
|
|
624
|
+
_ = try applyDamage(battle, player, player.foe(), .Confusion, options);
|
|
604
625
|
// Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
|
|
605
626
|
if (showdown) battle.last_damage = uncapped;
|
|
606
627
|
|
|
@@ -617,7 +638,10 @@ fn beforeMove(
|
|
|
617
638
|
volatiles.Thrashing = false;
|
|
618
639
|
volatiles.Charging = false;
|
|
619
640
|
volatiles.Binding = false;
|
|
620
|
-
|
|
641
|
+
|
|
642
|
+
options.chance.observe(.attacking, player, .None);
|
|
643
|
+
options.chance.observe(.binding, player, .None);
|
|
644
|
+
|
|
621
645
|
// GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
|
|
622
646
|
try log.cant(.{ ident, .Paralysis });
|
|
623
647
|
return .done;
|
|
@@ -635,12 +659,8 @@ fn beforeMove(
|
|
|
635
659
|
volatiles.state +%= battle.last_damage;
|
|
636
660
|
}
|
|
637
661
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
} else {
|
|
641
|
-
volatiles.attacks -= 1;
|
|
642
|
-
}
|
|
643
|
-
try options.chance.attacking(player, volatiles.attacks);
|
|
662
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
663
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
644
664
|
|
|
645
665
|
if (volatiles.attacks != 0) {
|
|
646
666
|
try log.activate(.{ ident, .Bide });
|
|
@@ -674,35 +694,40 @@ fn beforeMove(
|
|
|
674
694
|
}
|
|
675
695
|
|
|
676
696
|
if (volatiles.Thrashing) {
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
}
|
|
682
|
-
try options.chance.attacking(player, volatiles.attacks);
|
|
697
|
+
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
698
|
+
|
|
699
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
700
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
683
701
|
|
|
684
|
-
if (
|
|
702
|
+
if (volatiles.attacks == 0) {
|
|
703
|
+
const overwritten = volatiles.Confusion;
|
|
704
|
+
|
|
705
|
+
volatiles.Thrashing = false;
|
|
706
|
+
volatiles.Confusion = true;
|
|
707
|
+
volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
708
|
+
|
|
709
|
+
// On Pokémon Showdown this could leak information to the opponent that thrashing has
|
|
710
|
+
// ended (the engine's protocol is always from the omniscient perspective and clients
|
|
711
|
+
// can be expected to recognize this pattern and pass the information only to the
|
|
712
|
+
// correct player)
|
|
685
713
|
try log.start(.{ battle.active(player), .ConfusionSilent });
|
|
714
|
+
options.chance.observe(.confusion, player, if (overwritten)
|
|
715
|
+
if (pkmn.options.overwrite) .overwritten else .continuing
|
|
716
|
+
else
|
|
717
|
+
.started);
|
|
718
|
+
if (!overwritten) options.calc.confusion(player);
|
|
686
719
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
if (handleThrashing(battle, active, player, options)) {
|
|
693
|
-
try log.start(.{ battle.active(player), .ConfusionSilent });
|
|
694
|
-
}
|
|
695
|
-
}
|
|
720
|
+
|
|
721
|
+
// This shouldn't actually set last_used_move, but Pokémon Showdown sets last
|
|
722
|
+
// used in useMove and doesn't have the notion of skipping canMove semantics
|
|
723
|
+
if (showdown) side.last_used_move = side.last_selected_move;
|
|
724
|
+
|
|
696
725
|
return .skip_can;
|
|
697
726
|
}
|
|
698
727
|
|
|
699
728
|
if (volatiles.Binding) {
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
} else {
|
|
703
|
-
volatiles.attacks -= 1;
|
|
704
|
-
}
|
|
705
|
-
try options.chance.binding(player, volatiles.attacks);
|
|
729
|
+
volatiles.attacks = decrement(.binding, player, options, volatiles.attacks);
|
|
730
|
+
try options.chance.binding(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
706
731
|
|
|
707
732
|
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
708
733
|
if (showdown or battle.last_damage != 0) {
|
|
@@ -756,9 +781,14 @@ fn canMove(
|
|
|
756
781
|
|
|
757
782
|
if (move.effect == .Thrashing) {
|
|
758
783
|
Effects.thrashing(battle, player, options);
|
|
759
|
-
} else if (
|
|
760
|
-
// Pokémon Showdown handles this after hit/miss checks and damage calculation
|
|
761
|
-
|
|
784
|
+
} else if (move.effect == .Binding) {
|
|
785
|
+
// Pokémon Showdown handles this after hit/miss checks and damage calculation, though clears
|
|
786
|
+
// Recharging in onTryMove (required for the Hyper Beam automatic selection glitch)
|
|
787
|
+
if (showdown) {
|
|
788
|
+
battle.foe(player).active.volatiles.Recharging = false;
|
|
789
|
+
} else {
|
|
790
|
+
try Effects.binding(battle, player, false, options);
|
|
791
|
+
}
|
|
762
792
|
}
|
|
763
793
|
|
|
764
794
|
return true;
|
|
@@ -795,7 +825,7 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
|
|
|
795
825
|
assert(active.move(mslot).pp == side.stored().move(mslot).pp);
|
|
796
826
|
}
|
|
797
827
|
|
|
798
|
-
fn incrementPP(side: *Side, mslot: u4) void {
|
|
828
|
+
fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
|
|
799
829
|
var active = &side.active;
|
|
800
830
|
const volatiles = &active.volatiles;
|
|
801
831
|
|
|
@@ -803,8 +833,11 @@ fn incrementPP(side: *Side, mslot: u4) void {
|
|
|
803
833
|
// GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
|
|
804
834
|
if (showdown and volatiles.Transform) return;
|
|
805
835
|
|
|
836
|
+
const n = options.calc.overridden(player, .pp) orelse 1;
|
|
837
|
+
if (volatiles.Transform and !options.chance.pp(player, n)) return;
|
|
838
|
+
|
|
806
839
|
assert(mslot > 0 and mslot <= 4);
|
|
807
|
-
side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +%
|
|
840
|
+
side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% n;
|
|
808
841
|
}
|
|
809
842
|
|
|
810
843
|
// Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
|
|
@@ -845,7 +878,7 @@ fn doMove(
|
|
|
845
878
|
var miss = showdown and miss: {
|
|
846
879
|
immune = move.target != .Self and !status and !counter and
|
|
847
880
|
(@intFromEnum(move.type.effectiveness(foe.active.types.type1)) == 0 or
|
|
848
|
-
|
|
881
|
+
@intFromEnum(move.type.effectiveness(foe.active.types.type2)) == 0);
|
|
849
882
|
if (immune and move.effect != .Binding) break :miss true;
|
|
850
883
|
if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) {
|
|
851
884
|
battle.last_damage = 0;
|
|
@@ -858,7 +891,7 @@ fn doMove(
|
|
|
858
891
|
|
|
859
892
|
var late = showdown and move.effect != .Explode;
|
|
860
893
|
const skip = status or immune;
|
|
861
|
-
if ((!showdown or (!skip or counter)) and !miss)
|
|
894
|
+
if ((!showdown or (!skip or counter)) and !miss) damage: {
|
|
862
895
|
if (showdown and move.effect.isMulti()) {
|
|
863
896
|
try Effects.multiHit(battle, player, move, options);
|
|
864
897
|
hits = side.active.volatiles.attacks;
|
|
@@ -880,9 +913,10 @@ fn doMove(
|
|
|
880
913
|
ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
|
|
881
914
|
// This can overflow after adjustDamage, but will still be sufficient to OHKO
|
|
882
915
|
battle.last_damage = if (ohko) 65535 else 0;
|
|
883
|
-
if (showdown) break :
|
|
884
|
-
} else if (!calcDamage(battle, player, player.foe(), move, crit)) {
|
|
885
|
-
|
|
916
|
+
if (showdown) break :damage; // skip adjustDamage / randomizeDamage
|
|
917
|
+
} else if (!calcDamage(battle, player, player.foe(), move, crit, options)) {
|
|
918
|
+
if (!showdown) try options.chance.commit(player, .err);
|
|
919
|
+
return .Error;
|
|
886
920
|
}
|
|
887
921
|
if (battle.last_damage == 0) {
|
|
888
922
|
immune = true;
|
|
@@ -895,11 +929,12 @@ fn doMove(
|
|
|
895
929
|
}
|
|
896
930
|
}
|
|
897
931
|
|
|
932
|
+
var missed: bool = undefined;
|
|
898
933
|
const zero = battle.last_damage == 0;
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
934
|
+
if (!showdown and !skip) {
|
|
935
|
+
missed = !try moveHit(battle, player, move, &immune, &mist, options);
|
|
936
|
+
miss = missed or zero;
|
|
937
|
+
}
|
|
903
938
|
|
|
904
939
|
assert(showdown or miss or battle.last_damage > 0 or skip);
|
|
905
940
|
assert((!showdown and miss) or !(ohko and immune));
|
|
@@ -926,11 +961,32 @@ fn doMove(
|
|
|
926
961
|
try log.immune(.{ foe_ident, .OHKO });
|
|
927
962
|
} else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
|
|
928
963
|
try log.immune(.{ foe_ident, .None });
|
|
964
|
+
if (!showdown) {
|
|
965
|
+
if (move.effect == .Binding) {
|
|
966
|
+
// Rather obnoxiously, the cartridge sets the Binding volatile in canMove and
|
|
967
|
+
// then simply clears it later on if the move happens to miss in moveHit.
|
|
968
|
+
// However, moveHit can return true and we'll still end up in the outer miss
|
|
969
|
+
// handling block because attacking into an immune Pokemon does zero damage
|
|
970
|
+
// (considered missing)
|
|
971
|
+
assert(zero);
|
|
972
|
+
// Thus we still need to actually save the hit result (but not the crit roll).
|
|
973
|
+
// Using .miss for this commit would accomplish what we want, but is somewhat
|
|
974
|
+
// misleading as its entirely possible the move actually did hit (and the target
|
|
975
|
+
// is now trapped) so we use the .binding value instead which is effectively
|
|
976
|
+
// identical, just doesn't claim to know about the actual results of the hit
|
|
977
|
+
// roll)
|
|
978
|
+
try options.chance.commit(player, .binding);
|
|
979
|
+
} else {
|
|
980
|
+
// The .glitch option allows for conditionally committing if we are in a
|
|
981
|
+
// division-by-zero freeze alternative timeline scenario
|
|
982
|
+
try options.chance.commit(player, .glitch);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
929
985
|
} else if (mist) {
|
|
930
986
|
if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
|
|
931
987
|
try log.fail(.{ foe_ident, .None });
|
|
932
988
|
} else {
|
|
933
|
-
if (!showdown
|
|
989
|
+
if (!showdown) try options.chance.commit(player, if (zero and !missed) .hit else .miss);
|
|
934
990
|
try log.lastmiss(.{});
|
|
935
991
|
try log.miss(.{battle.active(player)});
|
|
936
992
|
}
|
|
@@ -945,8 +1001,6 @@ fn doMove(
|
|
|
945
1001
|
try buildRage(battle, player.foe(), options);
|
|
946
1002
|
} else if (showdown and move.effect == .Disable) {
|
|
947
1003
|
try buildRage(battle, player.foe(), options);
|
|
948
|
-
} else if (!showdown and immune and move.effect == .Binding) {
|
|
949
|
-
try options.chance.commit(player, .binding);
|
|
950
1004
|
}
|
|
951
1005
|
return null;
|
|
952
1006
|
} else if (!showdown) {
|
|
@@ -965,7 +1019,7 @@ fn doMove(
|
|
|
965
1019
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
966
1020
|
|
|
967
1021
|
var nullified = false;
|
|
968
|
-
var hit:
|
|
1022
|
+
var hit: usize = 0;
|
|
969
1023
|
while (hit < hits) {
|
|
970
1024
|
if (hit == 0) {
|
|
971
1025
|
if (crit) try log.crit(.{battle.active(player.foe())});
|
|
@@ -993,23 +1047,21 @@ fn doMove(
|
|
|
993
1047
|
if (showdown and move.effect == .Twineedle and !nullified and foe.stored().hp > 0) {
|
|
994
1048
|
try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
995
1049
|
}
|
|
996
|
-
try log.hitcount(.{ battle.active(player.foe()), hit });
|
|
997
|
-
} else if (showdown) {
|
|
1050
|
+
try log.hitcount(.{ battle.active(player.foe()), @as(u4, @intCast(hit)) });
|
|
1051
|
+
} else if (showdown and move.effect == .Binding) {
|
|
998
1052
|
// This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
try buildRage(battle, player.foe(), options);
|
|
1010
|
-
}
|
|
1011
|
-
return null;
|
|
1053
|
+
try Effects.binding(battle, player, rewrap, options);
|
|
1054
|
+
if (immune) {
|
|
1055
|
+
battle.last_damage = 0;
|
|
1056
|
+
assert(foe.stored().hp > 0);
|
|
1057
|
+
// Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
|
|
1058
|
+
if (sub) {
|
|
1059
|
+
try log.activate(.{ battle.active(player.foe()), .Substitute });
|
|
1060
|
+
} else {
|
|
1061
|
+
try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
|
|
1062
|
+
try buildRage(battle, player.foe(), options);
|
|
1012
1063
|
}
|
|
1064
|
+
return null;
|
|
1013
1065
|
}
|
|
1014
1066
|
}
|
|
1015
1067
|
|
|
@@ -1069,6 +1121,7 @@ fn calcDamage(
|
|
|
1069
1121
|
target_player: Player,
|
|
1070
1122
|
m: ?Move.Data,
|
|
1071
1123
|
crit: bool,
|
|
1124
|
+
options: anytype,
|
|
1072
1125
|
) bool {
|
|
1073
1126
|
// Confusion (indicated when m == null) just needs a 40 BP physical move
|
|
1074
1127
|
const cfz = m == null;
|
|
@@ -1080,28 +1133,8 @@ fn calcDamage(
|
|
|
1080
1133
|
|
|
1081
1134
|
const special = move.type.special();
|
|
1082
1135
|
|
|
1083
|
-
|
|
1084
|
-
var
|
|
1085
|
-
if (crit)
|
|
1086
|
-
if (special) side.stored().stats.spc
|
|
1087
|
-
else side.stored().stats.atk
|
|
1088
|
-
else
|
|
1089
|
-
if (special) side.active.stats.spc
|
|
1090
|
-
else side.active.stats.atk;
|
|
1091
|
-
var def: u32 =
|
|
1092
|
-
if (crit)
|
|
1093
|
-
if (special) target.stored().stats.spc
|
|
1094
|
-
else target.stored().stats.def
|
|
1095
|
-
else
|
|
1096
|
-
// GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
|
|
1097
|
-
if (special)
|
|
1098
|
-
target.active.stats.spc *
|
|
1099
|
-
@as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
|
|
1100
|
-
// Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
|
|
1101
|
-
else
|
|
1102
|
-
target.active.stats.def *
|
|
1103
|
-
@as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
|
|
1104
|
-
// zig fmt: on
|
|
1136
|
+
var atk = attack(side, crit, special);
|
|
1137
|
+
var def = defense(target, crit, special, cfz);
|
|
1105
1138
|
|
|
1106
1139
|
// Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
|
|
1107
1140
|
// will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
|
|
@@ -1109,7 +1142,7 @@ fn calcDamage(
|
|
|
1109
1142
|
if ((!showdown or !cfz) and (atk > 255 or def > 255)) {
|
|
1110
1143
|
atk = @max((atk / 4) & 255, 1);
|
|
1111
1144
|
// GLITCH: not adjusted to be a min of 1 on cartridge (can lead to division-by-zero freeze)
|
|
1112
|
-
def = @max((def / 4) & 255, if (
|
|
1145
|
+
def = @max((def / 4) & 255, if (pkmn.options.mod) 1 else 0);
|
|
1113
1146
|
}
|
|
1114
1147
|
|
|
1115
1148
|
const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
|
|
@@ -1126,11 +1159,41 @@ fn calcDamage(
|
|
|
1126
1159
|
d = @min(997, d);
|
|
1127
1160
|
d += 2;
|
|
1128
1161
|
|
|
1162
|
+
// Whether or not a critical hit occurred can change whether a division-by-zero freeze is
|
|
1163
|
+
// encountered so we need to make sure that in the event of a miss we commit the status of the
|
|
1164
|
+
// crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
|
|
1165
|
+
// that results in counter intuitive probabilities in the common case
|
|
1166
|
+
if (pkmn.options.chance and !pkmn.options.mod and move.effect != .Explode) {
|
|
1167
|
+
atk = attack(side, !crit, special);
|
|
1168
|
+
def = defense(target, !crit, special, false);
|
|
1169
|
+
if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1129
1172
|
battle.last_damage = @intCast(d);
|
|
1130
1173
|
|
|
1131
1174
|
return true;
|
|
1132
1175
|
}
|
|
1133
1176
|
|
|
1177
|
+
fn attack(side: *Side, crit: bool, special: bool) u32 {
|
|
1178
|
+
return if (crit)
|
|
1179
|
+
if (special) side.stored().stats.spc else side.stored().stats.atk
|
|
1180
|
+
else if (special) side.active.stats.spc else side.active.stats.atk;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
fn defense(target: *Side, crit: bool, special: bool, cfz: bool) u32 {
|
|
1184
|
+
return if (crit)
|
|
1185
|
+
if (special) target.stored().stats.spc else target.stored().stats.def
|
|
1186
|
+
else
|
|
1187
|
+
// GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
|
|
1188
|
+
if (special)
|
|
1189
|
+
target.active.stats.spc *
|
|
1190
|
+
@as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
|
|
1191
|
+
else
|
|
1192
|
+
// Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
|
|
1193
|
+
target.active.stats.def *
|
|
1194
|
+
@as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1134
1197
|
fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
1135
1198
|
const side = battle.side(player);
|
|
1136
1199
|
const foe = battle.foe(player);
|
|
@@ -1185,13 +1248,13 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1185
1248
|
.Psywave => power: {
|
|
1186
1249
|
const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
|
|
1187
1250
|
// GLITCH: Psywave infinite glitch loop
|
|
1188
|
-
if (!
|
|
1251
|
+
if (!pkmn.options.mod and max <= 1) return .Error;
|
|
1189
1252
|
break :power try Rolls.psywave(battle, player, max, options);
|
|
1190
1253
|
},
|
|
1191
1254
|
else => unreachable,
|
|
1192
1255
|
};
|
|
1193
1256
|
|
|
1194
|
-
if (battle.last_damage == 0) return if (
|
|
1257
|
+
if (battle.last_damage == 0) return if (pkmn.options.mod) null else .Error;
|
|
1195
1258
|
|
|
1196
1259
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1197
1260
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
@@ -1211,15 +1274,15 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1211
1274
|
// Pretend Splash was used as a stand-in when no move has been used to fail below with 0 BP
|
|
1212
1275
|
const foe_last_selected_move =
|
|
1213
1276
|
Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1277
|
+
.Splash
|
|
1278
|
+
else
|
|
1279
|
+
foe.last_selected_move);
|
|
1217
1280
|
|
|
1218
1281
|
const used = battle.lastMove(player.foe()).counterable != 0;
|
|
1219
1282
|
const selected = foe_last_selected_move.bp > 0 and
|
|
1220
1283
|
foe.last_selected_move != .Counter and
|
|
1221
1284
|
(foe_last_selected_move.type == .Normal or
|
|
1222
|
-
|
|
1285
|
+
foe_last_selected_move.type == .Fighting);
|
|
1223
1286
|
|
|
1224
1287
|
if (!used and !selected) {
|
|
1225
1288
|
try options.log.fail(.{ battle.active(player), .None });
|
|
@@ -1228,12 +1291,16 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1228
1291
|
|
|
1229
1292
|
if (!used or !selected) {
|
|
1230
1293
|
// GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
|
|
1231
|
-
if (!
|
|
1294
|
+
if (!pkmn.options.mod) return .Error;
|
|
1232
1295
|
try options.log.fail(.{ battle.active(player), .None });
|
|
1233
1296
|
return null;
|
|
1234
1297
|
}
|
|
1235
1298
|
|
|
1236
|
-
|
|
1299
|
+
// NOTE: llvm/llvm-project#58557
|
|
1300
|
+
battle.last_damage = if (comptime builtin.target.cpu.arch.isWasm())
|
|
1301
|
+
@max(battle.last_damage *% 2, std.math.maxInt(u16))
|
|
1302
|
+
else
|
|
1303
|
+
battle.last_damage *| 2;
|
|
1237
1304
|
|
|
1238
1305
|
// Pokémon Showdown calls moveHit before Counter
|
|
1239
1306
|
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
@@ -1265,7 +1332,7 @@ fn applyDamage(
|
|
|
1265
1332
|
subbed.active.volatiles.Substitute = false;
|
|
1266
1333
|
// battle.last_damage is not updated with the amount of HP the Substitute had
|
|
1267
1334
|
try options.log.end(.{ battle.active(sub_player), .Substitute });
|
|
1268
|
-
options.calc.capped(
|
|
1335
|
+
if (reason != .Confusion) options.calc.capped(sub_player);
|
|
1269
1336
|
return true;
|
|
1270
1337
|
} else {
|
|
1271
1338
|
// Safe to truncate since less than subbed.volatiles.substitute which is a u8
|
|
@@ -1299,7 +1366,7 @@ fn mirrorMove(
|
|
|
1299
1366
|
try options.log.fail(.{ battle.active(player), .None });
|
|
1300
1367
|
return null;
|
|
1301
1368
|
} else if (!showdown or foe.last_used_move != .Struggle) {
|
|
1302
|
-
incrementPP(side, mslot);
|
|
1369
|
+
incrementPP(side, player, mslot, options);
|
|
1303
1370
|
}
|
|
1304
1371
|
|
|
1305
1372
|
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
|
|
@@ -1320,7 +1387,7 @@ fn metronome(
|
|
|
1320
1387
|
var side = battle.side(player);
|
|
1321
1388
|
|
|
1322
1389
|
side.last_selected_move = try Rolls.metronome(battle, player, options);
|
|
1323
|
-
incrementPP(side, mslot);
|
|
1390
|
+
incrementPP(side, player, mslot, options);
|
|
1324
1391
|
|
|
1325
1392
|
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
|
|
1326
1393
|
return null;
|
|
@@ -1379,7 +1446,16 @@ fn moveHit(
|
|
|
1379
1446
|
break :miss true;
|
|
1380
1447
|
}
|
|
1381
1448
|
if (!showdown) {
|
|
1382
|
-
if (move.effect == .Swift)
|
|
1449
|
+
if (move.effect == .Swift) {
|
|
1450
|
+
// If Swift could cause a division-by-zero freeze (which doesn't exist on Pokémon
|
|
1451
|
+
// Showdown, hence why the block above doesn't include this code), we need the
|
|
1452
|
+
// damage roll to be committed. However, the commit code will not save this roll if
|
|
1453
|
+
// the glitch occurs and the move was not marked as having hit. To work around this
|
|
1454
|
+
// we flag that the move hit (as Swift trivially does), but pass the probability of
|
|
1455
|
+
// the hit as zero to ensure it doesn't actually get saved.
|
|
1456
|
+
try options.chance.hit(player, true, 0);
|
|
1457
|
+
return true;
|
|
1458
|
+
}
|
|
1383
1459
|
if (foe.active.volatiles.Invulnerable) break :miss true;
|
|
1384
1460
|
}
|
|
1385
1461
|
|
|
@@ -1421,13 +1497,17 @@ fn moveHit(
|
|
|
1421
1497
|
|
|
1422
1498
|
if (!miss) return true;
|
|
1423
1499
|
if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
|
|
1500
|
+
|
|
1424
1501
|
side.active.volatiles.Binding = false;
|
|
1502
|
+
options.chance.observe(.binding, player, .None);
|
|
1503
|
+
|
|
1425
1504
|
return false;
|
|
1426
1505
|
}
|
|
1427
1506
|
|
|
1428
1507
|
fn checkFaint(
|
|
1429
1508
|
battle: anytype,
|
|
1430
1509
|
player: Player,
|
|
1510
|
+
cap: bool,
|
|
1431
1511
|
options: anytype,
|
|
1432
1512
|
) @TypeOf(options.log).Error!?Result {
|
|
1433
1513
|
const side = battle.side(player);
|
|
@@ -1440,21 +1520,21 @@ fn checkFaint(
|
|
|
1440
1520
|
const foe_out = foe_fainted and findFirstAlive(foe) == 0;
|
|
1441
1521
|
const more = player_out or foe_out;
|
|
1442
1522
|
|
|
1443
|
-
if (try faint(battle, player, !(more or foe_fainted), options)) |r| return r;
|
|
1444
|
-
if (foe_fainted) if (try faint(battle, player.foe(), !more, options)) |r| return r;
|
|
1523
|
+
if (try faint(battle, player, !(more or foe_fainted), cap, options)) |r| return r;
|
|
1524
|
+
if (foe_fainted) if (try faint(battle, player.foe(), !more, false, options)) |r| return r;
|
|
1445
1525
|
|
|
1446
1526
|
assert(!side.active.volatiles.MultiHit);
|
|
1447
1527
|
assert(!foe.active.volatiles.MultiHit);
|
|
1448
1528
|
|
|
1449
1529
|
if (player_out and foe_out) {
|
|
1450
1530
|
try options.log.tie(.{});
|
|
1451
|
-
return
|
|
1531
|
+
return .Tie;
|
|
1452
1532
|
} else if (player_out) {
|
|
1453
1533
|
try options.log.win(.{player.foe()});
|
|
1454
|
-
return if (player == .P1)
|
|
1534
|
+
return if (player == .P1) .Lose else .Win;
|
|
1455
1535
|
} else if (foe_out) {
|
|
1456
1536
|
try options.log.win(.{player});
|
|
1457
|
-
return if (player == .P1)
|
|
1537
|
+
return if (player == .P1) .Win else .Lose;
|
|
1458
1538
|
}
|
|
1459
1539
|
|
|
1460
1540
|
const foe_choice: Choice.Type = if (foe_fainted) .Switch else .Pass;
|
|
@@ -1462,7 +1542,7 @@ fn checkFaint(
|
|
|
1462
1542
|
return .{ .p1 = foe_choice, .p2 = .Switch };
|
|
1463
1543
|
}
|
|
1464
1544
|
|
|
1465
|
-
fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
|
|
1545
|
+
fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytype) !?Result {
|
|
1466
1546
|
var side = battle.side(player);
|
|
1467
1547
|
var foe = battle.foe(player);
|
|
1468
1548
|
assert(side.stored().hp == 0);
|
|
@@ -1471,8 +1551,8 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
|
|
|
1471
1551
|
assert(!foe_volatiles.MultiHit);
|
|
1472
1552
|
if (foe_volatiles.Bide) {
|
|
1473
1553
|
assert(!foe_volatiles.Thrashing and !foe_volatiles.Rage);
|
|
1474
|
-
foe_volatiles.state = if (
|
|
1475
|
-
if (foe_volatiles.state != 0) return
|
|
1554
|
+
foe_volatiles.state = if (pkmn.options.mod) 0 else foe_volatiles.state & 255;
|
|
1555
|
+
if (foe_volatiles.state != 0) return .Error;
|
|
1476
1556
|
}
|
|
1477
1557
|
|
|
1478
1558
|
// Clearing these is not strictly necessary as provided the battle hasn't ended the side that
|
|
@@ -1496,7 +1576,9 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
|
|
|
1496
1576
|
}
|
|
1497
1577
|
|
|
1498
1578
|
try options.log.faint(.{ battle.active(player), done });
|
|
1499
|
-
|
|
1579
|
+
// We can only safely set capped if the faint occured as the direct result of an opponent's
|
|
1580
|
+
// move (as opposed to due to residual damage, recoil, Explosion, etc)
|
|
1581
|
+
if (cap) options.calc.capped(player);
|
|
1500
1582
|
return null;
|
|
1501
1583
|
}
|
|
1502
1584
|
|
|
@@ -1560,21 +1642,21 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
|
|
|
1560
1642
|
|
|
1561
1643
|
battle.turn += 1;
|
|
1562
1644
|
|
|
1563
|
-
if (
|
|
1645
|
+
if (pkmn.options.mod and pkmn.options.ebc and checkEBC(battle)) {
|
|
1564
1646
|
try options.log.tie(.{});
|
|
1565
|
-
return
|
|
1647
|
+
return .Tie;
|
|
1566
1648
|
}
|
|
1567
1649
|
|
|
1568
|
-
if (
|
|
1650
|
+
if (pkmn.options.mod and battle.turn >= 1000) {
|
|
1569
1651
|
try options.log.tie(.{});
|
|
1570
|
-
return
|
|
1652
|
+
return .Tie;
|
|
1571
1653
|
} else if (battle.turn >= 65535) {
|
|
1572
|
-
return
|
|
1654
|
+
return .Error;
|
|
1573
1655
|
}
|
|
1574
1656
|
|
|
1575
1657
|
try options.log.turn(.{battle.turn});
|
|
1576
1658
|
|
|
1577
|
-
return
|
|
1659
|
+
return .Default;
|
|
1578
1660
|
}
|
|
1579
1661
|
|
|
1580
1662
|
fn checkEBC(battle: anytype) bool {
|
|
@@ -1656,17 +1738,6 @@ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
|
|
|
1656
1738
|
}
|
|
1657
1739
|
}
|
|
1658
1740
|
|
|
1659
|
-
fn handleThrashing(battle: anytype, active: *ActivePokemon, player: Player, options: anytype) bool {
|
|
1660
|
-
var volatiles = &active.volatiles;
|
|
1661
|
-
assert(volatiles.Thrashing);
|
|
1662
|
-
if (volatiles.attacks > 0) return false;
|
|
1663
|
-
|
|
1664
|
-
volatiles.Thrashing = false;
|
|
1665
|
-
volatiles.Confusion = true;
|
|
1666
|
-
volatiles.confusion = Rolls.confusionDuration(battle, player, true, options);
|
|
1667
|
-
return true;
|
|
1668
|
-
}
|
|
1669
|
-
|
|
1670
1741
|
fn onBegin(
|
|
1671
1742
|
battle: anytype,
|
|
1672
1743
|
player: Player,
|
|
@@ -1753,8 +1824,10 @@ pub const Effects = struct {
|
|
|
1753
1824
|
|
|
1754
1825
|
side.active.volatiles.Bide = true;
|
|
1755
1826
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1827
|
+
|
|
1756
1828
|
side.active.volatiles.state = 0;
|
|
1757
1829
|
side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
1830
|
+
options.chance.observe(.attacking, player, .started);
|
|
1758
1831
|
|
|
1759
1832
|
try options.log.start(.{ battle.active(player), .Bide });
|
|
1760
1833
|
}
|
|
@@ -1820,7 +1893,10 @@ pub const Effects = struct {
|
|
|
1820
1893
|
|
|
1821
1894
|
if (foe.active.volatiles.Confusion) return;
|
|
1822
1895
|
foe.active.volatiles.Confusion = true;
|
|
1823
|
-
|
|
1896
|
+
|
|
1897
|
+
foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
1898
|
+
options.chance.observe(.confusion, player.foe(), .started);
|
|
1899
|
+
options.calc.confusion(player.foe());
|
|
1824
1900
|
|
|
1825
1901
|
try options.log.start(.{ battle.active(player.foe()), .Confusion });
|
|
1826
1902
|
}
|
|
@@ -1866,18 +1942,18 @@ pub const Effects = struct {
|
|
|
1866
1942
|
|
|
1867
1943
|
// Technically this is still considered simply a "miss" on the cartridge,
|
|
1868
1944
|
// but diverging from Pokémon Showdown here would mostly just be pedantic
|
|
1869
|
-
if (n == 0) {
|
|
1945
|
+
if (n == 0 or (pkmn.options.mod and err)) {
|
|
1870
1946
|
try options.log.fail(.{ foe_ident, .None });
|
|
1871
1947
|
return null;
|
|
1872
1948
|
} else if (err) {
|
|
1873
1949
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
1874
|
-
|
|
1875
|
-
return Result.Error;
|
|
1950
|
+
return .Error;
|
|
1876
1951
|
}
|
|
1877
1952
|
|
|
1878
1953
|
volatiles.disable_move =
|
|
1879
1954
|
@intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
|
|
1880
1955
|
volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
|
|
1956
|
+
options.chance.observe(.disable, player.foe(), .started);
|
|
1881
1957
|
|
|
1882
1958
|
const id = foe.active.move(volatiles.disable_move).id;
|
|
1883
1959
|
try options.log.start(.{ foe_ident, .Disable, id });
|
|
@@ -1948,7 +2024,7 @@ pub const Effects = struct {
|
|
|
1948
2024
|
if (foe.active.types.includes(move.type)) return;
|
|
1949
2025
|
if (!try Rolls.secondaryChance(battle, player, true, options)) return;
|
|
1950
2026
|
// Freeze Clause Mod
|
|
1951
|
-
if (
|
|
2027
|
+
if (pkmn.options.mod) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
1952
2028
|
|
|
1953
2029
|
foe_stored.status = Status.init(.FRZ);
|
|
1954
2030
|
// GLITCH: Hyper Beam recharging status is not cleared
|
|
@@ -1984,22 +2060,24 @@ pub const Effects = struct {
|
|
|
1984
2060
|
if (p != player and Status.any(s.stored().status)) {
|
|
1985
2061
|
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
1986
2062
|
s.stored().status = 0;
|
|
2063
|
+
options.chance.observe(.sleep, p, .None);
|
|
1987
2064
|
} else if (showdown and s.stored().status == Status.TOX) {
|
|
1988
2065
|
s.stored().status = Status.init(.PSN);
|
|
1989
2066
|
try log.status(.{ battle.active(p), s.stored().status, .None });
|
|
1990
2067
|
}
|
|
1991
|
-
try clearVolatiles(battle, p,
|
|
2068
|
+
try clearVolatiles(battle, p, options);
|
|
1992
2069
|
}
|
|
1993
2070
|
} else {
|
|
1994
2071
|
if (Status.any(foe_stored.status)) {
|
|
1995
2072
|
if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
|
|
1996
2073
|
foe.last_selected_move = .SKIP_TURN;
|
|
2074
|
+
options.chance.observe(.sleep, player.foe(), .None);
|
|
1997
2075
|
}
|
|
1998
2076
|
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
1999
2077
|
foe_stored.status = 0;
|
|
2000
2078
|
}
|
|
2001
|
-
try clearVolatiles(battle, player,
|
|
2002
|
-
try clearVolatiles(battle, player.foe(),
|
|
2079
|
+
try clearVolatiles(battle, player, options);
|
|
2080
|
+
try clearVolatiles(battle, player.foe(), options);
|
|
2003
2081
|
}
|
|
2004
2082
|
}
|
|
2005
2083
|
|
|
@@ -2287,7 +2365,7 @@ pub const Effects = struct {
|
|
|
2287
2365
|
}
|
|
2288
2366
|
|
|
2289
2367
|
// Sleep Clause Mod
|
|
2290
|
-
if (
|
|
2368
|
+
if (pkmn.options.mod) {
|
|
2291
2369
|
for (foe.pokemon) |p| {
|
|
2292
2370
|
if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
|
|
2293
2371
|
}
|
|
@@ -2295,6 +2373,8 @@ pub const Effects = struct {
|
|
|
2295
2373
|
foe.active.volatiles.Recharging = false;
|
|
2296
2374
|
|
|
2297
2375
|
foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
|
|
2376
|
+
options.chance.observe(.sleep, player.foe(), .started);
|
|
2377
|
+
|
|
2298
2378
|
const last = battle.side(player).last_selected_move;
|
|
2299
2379
|
try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
|
|
2300
2380
|
}
|
|
@@ -2351,6 +2431,8 @@ pub const Effects = struct {
|
|
|
2351
2431
|
|
|
2352
2432
|
volatiles.Thrashing = true;
|
|
2353
2433
|
volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
2434
|
+
|
|
2435
|
+
options.chance.observe(.attacking, player, .started);
|
|
2354
2436
|
}
|
|
2355
2437
|
|
|
2356
2438
|
fn transform(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -2389,13 +2471,14 @@ pub const Effects = struct {
|
|
|
2389
2471
|
if (side.active.volatiles.Binding) return;
|
|
2390
2472
|
side.active.volatiles.Binding = true;
|
|
2391
2473
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2392
|
-
// (Pokémon Showdown
|
|
2474
|
+
// (Pokémon Showdown clears Recharging in onTryMove instead)
|
|
2393
2475
|
if (!showdown) {
|
|
2394
2476
|
foe.active.volatiles.Recharging = false;
|
|
2395
2477
|
} else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
|
|
2396
2478
|
|
|
2397
2479
|
side.active.volatiles.attacks =
|
|
2398
2480
|
try Rolls.distribution(battle, .Binding, player, options) - 1;
|
|
2481
|
+
options.chance.observe(.binding, player, .started);
|
|
2399
2482
|
}
|
|
2400
2483
|
|
|
2401
2484
|
fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2625,22 +2708,22 @@ fn isForced(active: anytype) bool {
|
|
|
2625
2708
|
active.volatiles.Thrashing or active.volatiles.Charging;
|
|
2626
2709
|
}
|
|
2627
2710
|
|
|
2628
|
-
fn clearVolatiles(battle: anytype, who: Player,
|
|
2711
|
+
fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
|
|
2629
2712
|
var log = options.log;
|
|
2630
2713
|
var side = battle.side(who);
|
|
2631
2714
|
var volatiles = &side.active.volatiles;
|
|
2632
2715
|
const ident = battle.active(who);
|
|
2633
2716
|
|
|
2634
|
-
options.chance.clearDurations(who, clear);
|
|
2635
|
-
|
|
2636
2717
|
if (volatiles.disable_move != 0) {
|
|
2637
2718
|
volatiles.disable_move = 0;
|
|
2638
2719
|
volatiles.disable_duration = 0;
|
|
2720
|
+
options.chance.observe(.disable, who, .None);
|
|
2639
2721
|
try log.end(.{ ident, .DisableSilent });
|
|
2640
2722
|
}
|
|
2641
2723
|
if (volatiles.Confusion) {
|
|
2642
2724
|
// volatiles.confusion is left unchanged
|
|
2643
2725
|
volatiles.Confusion = false;
|
|
2726
|
+
options.chance.observe(.confusion, who, .None);
|
|
2644
2727
|
try log.end(.{ ident, .ConfusionSilent });
|
|
2645
2728
|
}
|
|
2646
2729
|
if (volatiles.Mist) {
|
|
@@ -2675,6 +2758,19 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
|
|
|
2675
2758
|
}
|
|
2676
2759
|
}
|
|
2677
2760
|
|
|
2761
|
+
fn decrement(
|
|
2762
|
+
comptime field: chance.Action.Field,
|
|
2763
|
+
player: Player,
|
|
2764
|
+
options: anytype,
|
|
2765
|
+
n: anytype,
|
|
2766
|
+
) @TypeOf(n) {
|
|
2767
|
+
return if (options.calc.overridden(player, field)) |obs| switch (obs) {
|
|
2768
|
+
.started, .ended => 0,
|
|
2769
|
+
.continuing => if (n > 1) n - 1 else n,
|
|
2770
|
+
else => unreachable,
|
|
2771
|
+
} else n - 1;
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2678
2774
|
pub const Rolls = struct {
|
|
2679
2775
|
fn speedTie(battle: anytype, options: anytype) !bool {
|
|
2680
2776
|
const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
|
|
@@ -2872,9 +2968,10 @@ pub const Rolls = struct {
|
|
|
2872
2968
|
}
|
|
2873
2969
|
|
|
2874
2970
|
fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2875
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2876
|
-
@intCast(val)
|
|
2877
|
-
else
|
|
2971
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2972
|
+
if (val >= 1 and val <= 7) @as(u3, @intCast(val)) else null
|
|
2973
|
+
else
|
|
2974
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
|
|
2878
2975
|
battle.rng.sleepDuration(player)
|
|
2879
2976
|
else if (showdown)
|
|
2880
2977
|
battle.rng.range(u3, 1, 8)
|
|
@@ -2886,14 +2983,15 @@ pub const Rolls = struct {
|
|
|
2886
2983
|
};
|
|
2887
2984
|
|
|
2888
2985
|
assert(duration >= 1 and duration <= 7);
|
|
2889
|
-
options.chance.duration(
|
|
2986
|
+
options.chance.duration(player, duration);
|
|
2890
2987
|
return duration;
|
|
2891
2988
|
}
|
|
2892
2989
|
|
|
2893
2990
|
fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
|
|
2894
|
-
const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
|
|
2895
|
-
val
|
|
2896
|
-
else
|
|
2991
|
+
const duration: u4 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2992
|
+
if (val >= 1 and val <= 8) val else null
|
|
2993
|
+
else
|
|
2994
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
|
|
2897
2995
|
battle.rng.disableDuration(player)
|
|
2898
2996
|
else if (showdown)
|
|
2899
2997
|
battle.rng.range(u4, 1, 9)
|
|
@@ -2901,29 +2999,31 @@ pub const Rolls = struct {
|
|
|
2901
2999
|
@intCast((battle.rng.next() & 7) + 1);
|
|
2902
3000
|
|
|
2903
3001
|
assert(duration >= 1 and duration <= 8);
|
|
2904
|
-
options.chance.duration(
|
|
3002
|
+
options.chance.duration(player, duration);
|
|
2905
3003
|
return duration;
|
|
2906
3004
|
}
|
|
2907
3005
|
|
|
2908
|
-
fn confusionDuration(battle: anytype, player: Player,
|
|
2909
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2910
|
-
@intCast(val)
|
|
2911
|
-
else
|
|
2912
|
-
battle.rng
|
|
3006
|
+
fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
3007
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
3008
|
+
if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
|
|
3009
|
+
else
|
|
3010
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
|
|
3011
|
+
battle.rng.confusionDuration(player)
|
|
2913
3012
|
else if (showdown)
|
|
2914
3013
|
battle.rng.range(u3, 2, 6)
|
|
2915
3014
|
else
|
|
2916
3015
|
@intCast((battle.rng.next() & 3) + 2);
|
|
2917
3016
|
|
|
2918
3017
|
assert(duration >= 2 and duration <= 5);
|
|
2919
|
-
options.chance.duration(
|
|
3018
|
+
options.chance.duration(player, duration);
|
|
2920
3019
|
return duration;
|
|
2921
3020
|
}
|
|
2922
3021
|
|
|
2923
3022
|
fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2924
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2925
|
-
@intCast(val)
|
|
2926
|
-
else
|
|
3023
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
3024
|
+
if (val >= 2 and val <= 3) @as(u3, @intCast(val)) else null
|
|
3025
|
+
else
|
|
3026
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
|
|
2927
3027
|
battle.rng.attackingDuration(player)
|
|
2928
3028
|
else if (showdown)
|
|
2929
3029
|
battle.rng.range(u3, 2, 4)
|
|
@@ -2931,7 +3031,7 @@ pub const Rolls = struct {
|
|
|
2931
3031
|
@intCast((battle.rng.next() & 1) + 2);
|
|
2932
3032
|
|
|
2933
3033
|
assert(duration >= 2 and duration <= 3);
|
|
2934
|
-
options.chance.duration(
|
|
3034
|
+
options.chance.duration(player, duration);
|
|
2935
3035
|
return duration;
|
|
2936
3036
|
}
|
|
2937
3037
|
|
|
@@ -2944,9 +3044,10 @@ pub const Rolls = struct {
|
|
|
2944
3044
|
options: anytype,
|
|
2945
3045
|
) !u3 {
|
|
2946
3046
|
const roll = if (effect == .Binding) .duration else .multi_hit;
|
|
2947
|
-
const n: u3 = if (options.calc.overridden(player, roll)) |val|
|
|
2948
|
-
@intCast(val)
|
|
2949
|
-
else
|
|
3047
|
+
const n: u3 = (if (options.calc.overridden(player, roll)) |val|
|
|
3048
|
+
if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
|
|
3049
|
+
else
|
|
3050
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "distribution"))
|
|
2950
3051
|
battle.rng.distribution(player)
|
|
2951
3052
|
else if (showdown)
|
|
2952
3053
|
DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
|
|
@@ -2957,7 +3058,7 @@ pub const Rolls = struct {
|
|
|
2957
3058
|
|
|
2958
3059
|
assert(n >= 2 and n <= 5);
|
|
2959
3060
|
if (effect == .Binding) {
|
|
2960
|
-
options.chance.duration(
|
|
3061
|
+
options.chance.duration(player, n);
|
|
2961
3062
|
} else {
|
|
2962
3063
|
try options.chance.multiHit(player, n);
|
|
2963
3064
|
}
|
|
@@ -2972,15 +3073,13 @@ pub const Rolls = struct {
|
|
|
2972
3073
|
options: anytype,
|
|
2973
3074
|
) !u4 {
|
|
2974
3075
|
// TODO: consider throwing error instead of rerolling?
|
|
2975
|
-
const
|
|
3076
|
+
const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
|
|
2976
3077
|
if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
|
|
2977
3078
|
val
|
|
2978
3079
|
else
|
|
2979
3080
|
null
|
|
2980
3081
|
else
|
|
2981
|
-
null
|
|
2982
|
-
|
|
2983
|
-
const slot: u4 = overridden orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
|
|
3082
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
|
|
2984
3083
|
battle.rng.moveSlot(player, moves, check_pp)
|
|
2985
3084
|
else slot: {
|
|
2986
3085
|
if (showdown) {
|
|
@@ -3046,11 +3145,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3046
3145
|
},
|
|
3047
3146
|
.Switch => {
|
|
3048
3147
|
const side = battle.side(player);
|
|
3049
|
-
var slot:
|
|
3148
|
+
var slot: usize = 2;
|
|
3050
3149
|
while (slot <= 6) : (slot += 1) {
|
|
3051
3150
|
const id = side.order[slot - 1];
|
|
3052
3151
|
if (id == 0 or side.pokemon[id - 1].hp == 0) continue;
|
|
3053
|
-
out[n] = .{ .type = .Switch, .data = slot };
|
|
3152
|
+
out[n] = .{ .type = .Switch, .data = @intCast(slot) };
|
|
3054
3153
|
n += 1;
|
|
3055
3154
|
}
|
|
3056
3155
|
if (n == 0) {
|
|
@@ -3081,11 +3180,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3081
3180
|
return n;
|
|
3082
3181
|
}
|
|
3083
3182
|
|
|
3084
|
-
var slot:
|
|
3183
|
+
var slot: usize = 2;
|
|
3085
3184
|
while (slot <= 6) : (slot += 1) {
|
|
3086
3185
|
const id = side.order[slot - 1];
|
|
3087
3186
|
if (id == 0 or side.pokemon[id - 1].hp == 0) continue;
|
|
3088
|
-
out[n] = .{ .type = .Switch, .data = slot };
|
|
3187
|
+
out[n] = .{ .type = .Switch, .data = @intCast(slot) };
|
|
3089
3188
|
n += 1;
|
|
3090
3189
|
}
|
|
3091
3190
|
|
|
@@ -3118,7 +3217,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3118
3217
|
const struggle =
|
|
3119
3218
|
m.id == .Bide and (m.pp == 0 or active.volatiles.disable_move == slot);
|
|
3120
3219
|
const s = if (struggle) 0 else slot;
|
|
3121
|
-
out[n] = .{ .type = .Move, .data = s };
|
|
3220
|
+
out[n] = .{ .type = .Move, .data = @intCast(s) };
|
|
3122
3221
|
n += 1;
|
|
3123
3222
|
return n;
|
|
3124
3223
|
}
|
|
@@ -3132,13 +3231,13 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3132
3231
|
if (m.id == .None) break;
|
|
3133
3232
|
if (m.pp == 0) continue;
|
|
3134
3233
|
if (active.volatiles.disable_move == slot) continue;
|
|
3135
|
-
out[n] = .{ .type = .Move, .data = slot };
|
|
3234
|
+
out[n] = .{ .type = .Move, .data = @intCast(slot) };
|
|
3136
3235
|
n += 1;
|
|
3137
3236
|
}
|
|
3138
3237
|
// Struggle (Pokémon Showdown would use 'move 1' here)
|
|
3139
3238
|
if (n == before) {
|
|
3140
3239
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
3141
|
-
if (!
|
|
3240
|
+
if (!pkmn.options.mod) {
|
|
3142
3241
|
while (slot <= 4) : (slot += 1) {
|
|
3143
3242
|
if (active.moves[slot - 1].pp != 0) return n;
|
|
3144
3243
|
}
|