@pkmn/engine 0.1.0-dev.77410aff → 0.1.0-dev.78dc891c
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 +40 -28
- 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 +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 -6
- 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 +109 -107
- package/build/pkg/protocol.js.map +1 -1
- package/build/tools/debug.js +84 -75
- 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 +366 -148
- package/package.json +61 -133
- package/src/bin/install-pkmn-engine +47 -21
- package/src/bin/pkmn-debug +1 -1
- package/src/lib/bindings/c.zig +201 -0
- package/src/lib/{binding → bindings}/node.zig +9 -22
- package/src/lib/bindings/wasm.zig +51 -0
- package/src/lib/common/array.zig +70 -0
- package/src/lib/common/data.zig +4 -4
- package/src/lib/common/debug.zig +45 -29
- package/src/lib/common/js.zig +3 -7
- package/src/lib/common/optional.zig +27 -23
- package/src/lib/common/options.zig +6 -0
- package/src/lib/common/protocol.zig +205 -174
- package/src/lib/common/rational.zig +338 -255
- package/src/lib/common/rng.zig +7 -9
- package/src/lib/common/util.zig +53 -1
- package/src/lib/gen1/calc.zig +721 -172
- package/src/lib/gen1/chance.zig +486 -300
- 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 +3 -4
- package/src/lib/gen1/data.zig +34 -34
- package/src/lib/gen1/helpers.zig +116 -561
- package/src/lib/gen1/mechanics.zig +402 -269
- package/src/lib/gen2/calc.zig +35 -28
- package/src/lib/gen2/chance.zig +111 -511
- 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 +31 -30
- package/src/lib/gen2/generated.zig +18 -23
- package/src/lib/gen2/helpers.zig +68 -39
- package/src/lib/gen2/mechanics.zig +215 -157
- package/src/lib/napi.h +2 -0
- package/src/lib/node.zig +14 -0
- package/src/lib/pkmn.zig +125 -29
- package/src/lib/wasm.zig +13 -0
- 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/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;
|
|
@@ -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);
|
|
@@ -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 .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,19 +420,19 @@ 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
|
|
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
|
-
|
|
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);
|
|
701
|
+
|
|
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);
|
|
683
708
|
|
|
684
|
-
|
|
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,20 +878,20 @@ 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;
|
|
852
885
|
break :miss true;
|
|
853
886
|
}
|
|
854
887
|
break :miss (move.target != .Self and
|
|
855
|
-
!moveHit(battle, player, move, &immune, &mist, options));
|
|
888
|
+
!try moveHit(battle, player, move, &immune, &mist, options));
|
|
856
889
|
};
|
|
857
890
|
assert(!immune or miss or (showdown and move.effect == .Binding));
|
|
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,17 +1001,15 @@ 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
|
-
} else {
|
|
1006
|
+
} else if (!showdown) {
|
|
953
1007
|
try options.chance.commit(player, .hit);
|
|
954
1008
|
}
|
|
955
1009
|
|
|
956
1010
|
// On the cartridge MultiHit doesn't get set up until after damage has been applied for the
|
|
957
1011
|
// first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
|
|
958
|
-
// it up above before damage calculation)
|
|
1012
|
+
// it up above before damage calculation)
|
|
959
1013
|
if (!showdown and move.effect.isMulti()) {
|
|
960
1014
|
try Effects.multiHit(battle, player, move, options);
|
|
961
1015
|
hits = side.active.volatiles.attacks;
|
|
@@ -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,33 +1047,31 @@ 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
|
|
|
1016
1068
|
// Substitute being broken nullifies the move's effect completely so even
|
|
1017
|
-
// if an effect was intended to "always happen" it will still get skipped
|
|
1069
|
+
// if an effect was intended to "always happen" it will still get skipped
|
|
1018
1070
|
if (nullified) return null;
|
|
1019
1071
|
|
|
1020
1072
|
// On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
|
|
1021
1073
|
// but this is only done to setup the MultiHit looping in the first place. Moving the MultiHit
|
|
1022
|
-
// setup before the loop means we can avoid having to waste time doing no-op handler searches
|
|
1074
|
+
// setup before the loop means we can avoid having to waste time doing no-op handler searches
|
|
1023
1075
|
if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
|
|
1024
1076
|
|
|
1025
1077
|
if (foe.stored().hp == 0) return null;
|
|
@@ -1034,7 +1086,7 @@ fn doMove(
|
|
|
1034
1086
|
// below). For Twineedle we change the data to that of one with PoisonChance1 given its
|
|
1035
1087
|
// MultiHit behavior is complete after the loop above, though Pokémon Showdown handles the
|
|
1036
1088
|
// Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
|
|
1037
|
-
// puts the |-status| message before |-hitcount| instead of after
|
|
1089
|
+
// puts the |-status| message before |-hitcount| instead of after
|
|
1038
1090
|
if (move.effect == .Twineedle) {
|
|
1039
1091
|
if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
1040
1092
|
} else if (move.effect == .Disable) {
|
|
@@ -1053,12 +1105,14 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
|
|
|
1053
1105
|
const side = battle.side(player);
|
|
1054
1106
|
|
|
1055
1107
|
// Base speed is used for the critical hit calculation, even when Transform-ed
|
|
1056
|
-
var rate =
|
|
1108
|
+
var rate: u16 = Species.chance(side.stored().species);
|
|
1057
1109
|
// GLITCH: Focus Energy reduces critical hit rate instead of increasing it
|
|
1058
1110
|
rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
|
|
1059
1111
|
rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
|
|
1060
1112
|
|
|
1061
|
-
|
|
1113
|
+
const spurious =
|
|
1114
|
+
!showdown and ((move.bp == 0 and move.effect != .OHKO) or move.target == .Depends);
|
|
1115
|
+
return Rolls.criticalHit(battle, player, @intCast(rate), spurious, options);
|
|
1062
1116
|
}
|
|
1063
1117
|
|
|
1064
1118
|
fn calcDamage(
|
|
@@ -1067,6 +1121,7 @@ fn calcDamage(
|
|
|
1067
1121
|
target_player: Player,
|
|
1068
1122
|
m: ?Move.Data,
|
|
1069
1123
|
crit: bool,
|
|
1124
|
+
options: anytype,
|
|
1070
1125
|
) bool {
|
|
1071
1126
|
// Confusion (indicated when m == null) just needs a 40 BP physical move
|
|
1072
1127
|
const cfz = m == null;
|
|
@@ -1078,28 +1133,8 @@ fn calcDamage(
|
|
|
1078
1133
|
|
|
1079
1134
|
const special = move.type.special();
|
|
1080
1135
|
|
|
1081
|
-
|
|
1082
|
-
var
|
|
1083
|
-
if (crit)
|
|
1084
|
-
if (special) side.stored().stats.spc
|
|
1085
|
-
else side.stored().stats.atk
|
|
1086
|
-
else
|
|
1087
|
-
if (special) side.active.stats.spc
|
|
1088
|
-
else side.active.stats.atk;
|
|
1089
|
-
var def: u32 =
|
|
1090
|
-
if (crit)
|
|
1091
|
-
if (special) target.stored().stats.spc
|
|
1092
|
-
else target.stored().stats.def
|
|
1093
|
-
else
|
|
1094
|
-
// GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
|
|
1095
|
-
if (special)
|
|
1096
|
-
target.active.stats.spc *
|
|
1097
|
-
@as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
|
|
1098
|
-
// Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
|
|
1099
|
-
else
|
|
1100
|
-
target.active.stats.def *
|
|
1101
|
-
@as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
|
|
1102
|
-
// zig fmt: on
|
|
1136
|
+
var atk = attack(side, crit, special);
|
|
1137
|
+
var def = defense(target, crit, special, cfz);
|
|
1103
1138
|
|
|
1104
1139
|
// Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
|
|
1105
1140
|
// will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
|
|
@@ -1107,12 +1142,12 @@ fn calcDamage(
|
|
|
1107
1142
|
if ((!showdown or !cfz) and (atk > 255 or def > 255)) {
|
|
1108
1143
|
atk = @max((atk / 4) & 255, 1);
|
|
1109
1144
|
// GLITCH: not adjusted to be a min of 1 on cartridge (can lead to division-by-zero freeze)
|
|
1110
|
-
def = @max((def / 4) & 255, if (
|
|
1145
|
+
def = @max((def / 4) & 255, if (pkmn.options.mod) 1 else 0);
|
|
1111
1146
|
}
|
|
1112
1147
|
|
|
1113
|
-
const lvl =
|
|
1148
|
+
const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
|
|
1114
1149
|
|
|
1115
|
-
def =
|
|
1150
|
+
def = if (move.effect == .Explode) @max(def / 2, 1) else def;
|
|
1116
1151
|
|
|
1117
1152
|
if (def == 0) return false;
|
|
1118
1153
|
|
|
@@ -1124,11 +1159,41 @@ fn calcDamage(
|
|
|
1124
1159
|
d = @min(997, d);
|
|
1125
1160
|
d += 2;
|
|
1126
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
|
+
|
|
1127
1172
|
battle.last_damage = @intCast(d);
|
|
1128
1173
|
|
|
1129
1174
|
return true;
|
|
1130
1175
|
}
|
|
1131
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
|
+
|
|
1132
1197
|
fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
1133
1198
|
const side = battle.side(player);
|
|
1134
1199
|
const foe = battle.foe(player);
|
|
@@ -1173,7 +1238,6 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1173
1238
|
const foe = battle.foe(player);
|
|
1174
1239
|
|
|
1175
1240
|
if (!try checkHit(battle, player, move, options)) return null;
|
|
1176
|
-
try options.chance.commit(player, .hit);
|
|
1177
1241
|
|
|
1178
1242
|
battle.last_damage = switch (side.last_selected_move) {
|
|
1179
1243
|
.SuperFang => @max(foe.stored().hp / 2, 1),
|
|
@@ -1184,13 +1248,13 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1184
1248
|
.Psywave => power: {
|
|
1185
1249
|
const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
|
|
1186
1250
|
// GLITCH: Psywave infinite glitch loop
|
|
1187
|
-
if (!
|
|
1251
|
+
if (!pkmn.options.mod and max <= 1) return .Error;
|
|
1188
1252
|
break :power try Rolls.psywave(battle, player, max, options);
|
|
1189
1253
|
},
|
|
1190
1254
|
else => unreachable,
|
|
1191
1255
|
};
|
|
1192
1256
|
|
|
1193
|
-
if (battle.last_damage == 0) return if (
|
|
1257
|
+
if (battle.last_damage == 0) return if (pkmn.options.mod) null else .Error;
|
|
1194
1258
|
|
|
1195
1259
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1196
1260
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
@@ -1210,15 +1274,15 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1210
1274
|
// Pretend Splash was used as a stand-in when no move has been used to fail below with 0 BP
|
|
1211
1275
|
const foe_last_selected_move =
|
|
1212
1276
|
Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1277
|
+
.Splash
|
|
1278
|
+
else
|
|
1279
|
+
foe.last_selected_move);
|
|
1216
1280
|
|
|
1217
1281
|
const used = battle.lastMove(player.foe()).counterable != 0;
|
|
1218
1282
|
const selected = foe_last_selected_move.bp > 0 and
|
|
1219
1283
|
foe.last_selected_move != .Counter and
|
|
1220
1284
|
(foe_last_selected_move.type == .Normal or
|
|
1221
|
-
|
|
1285
|
+
foe_last_selected_move.type == .Fighting);
|
|
1222
1286
|
|
|
1223
1287
|
if (!used and !selected) {
|
|
1224
1288
|
try options.log.fail(.{ battle.active(player), .None });
|
|
@@ -1227,16 +1291,19 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1227
1291
|
|
|
1228
1292
|
if (!used or !selected) {
|
|
1229
1293
|
// GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
|
|
1230
|
-
if (!
|
|
1294
|
+
if (!pkmn.options.mod) return .Error;
|
|
1231
1295
|
try options.log.fail(.{ battle.active(player), .None });
|
|
1232
1296
|
return null;
|
|
1233
1297
|
}
|
|
1234
1298
|
|
|
1235
|
-
|
|
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;
|
|
1236
1304
|
|
|
1237
|
-
// Pokémon Showdown calls
|
|
1305
|
+
// Pokémon Showdown calls moveHit before Counter
|
|
1238
1306
|
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
1239
|
-
try options.chance.commit(player, .hit);
|
|
1240
1307
|
|
|
1241
1308
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1242
1309
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
@@ -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;
|
|
@@ -1332,7 +1399,10 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
|
|
|
1332
1399
|
var immune = false;
|
|
1333
1400
|
var mist = false;
|
|
1334
1401
|
|
|
1335
|
-
if (moveHit(battle, player, move, &immune, &mist, options))
|
|
1402
|
+
if (try moveHit(battle, player, move, &immune, &mist, options)) {
|
|
1403
|
+
if (!showdown) try options.chance.commit(player, .hit);
|
|
1404
|
+
return true;
|
|
1405
|
+
}
|
|
1336
1406
|
|
|
1337
1407
|
assert(!immune);
|
|
1338
1408
|
if (mist) {
|
|
@@ -1341,11 +1411,11 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
|
|
|
1341
1411
|
try options.log.activate(.{ foe_ident, .Mist });
|
|
1342
1412
|
try options.log.fail(.{ foe_ident, .None });
|
|
1343
1413
|
} else {
|
|
1344
|
-
try options.chance.commit(player, .miss);
|
|
1345
1414
|
try options.log.lastmiss(.{});
|
|
1346
1415
|
try options.log.miss(.{battle.active(player)});
|
|
1347
1416
|
}
|
|
1348
1417
|
|
|
1418
|
+
if (!showdown) try options.chance.commit(player, .miss);
|
|
1349
1419
|
return false;
|
|
1350
1420
|
}
|
|
1351
1421
|
|
|
@@ -1356,7 +1426,7 @@ fn moveHit(
|
|
|
1356
1426
|
immune: *bool,
|
|
1357
1427
|
mist: *bool,
|
|
1358
1428
|
options: anytype,
|
|
1359
|
-
) bool {
|
|
1429
|
+
) !bool {
|
|
1360
1430
|
var side = battle.side(player);
|
|
1361
1431
|
const foe = battle.foe(player);
|
|
1362
1432
|
|
|
@@ -1376,7 +1446,16 @@ fn moveHit(
|
|
|
1376
1446
|
break :miss true;
|
|
1377
1447
|
}
|
|
1378
1448
|
if (!showdown) {
|
|
1379
|
-
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
|
+
}
|
|
1380
1459
|
if (foe.active.volatiles.Invulnerable) break :miss true;
|
|
1381
1460
|
}
|
|
1382
1461
|
|
|
@@ -1395,7 +1474,7 @@ fn moveHit(
|
|
|
1395
1474
|
const overwritten = overwrite and state > 0;
|
|
1396
1475
|
assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
|
|
1397
1476
|
|
|
1398
|
-
var accuracy = if (overwritten) state else
|
|
1477
|
+
var accuracy: u16 = if (overwritten) state else move.accuracy;
|
|
1399
1478
|
var boost = BOOSTS[@as(u4, @intCast(@as(i8, side.active.boosts.accuracy) + 6))];
|
|
1400
1479
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1401
1480
|
boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
|
|
@@ -1407,7 +1486,7 @@ fn moveHit(
|
|
|
1407
1486
|
if (overwrite) side.active.volatiles.state = accuracy;
|
|
1408
1487
|
|
|
1409
1488
|
// GLITCH: max accuracy is 255 so 1/256 chance of miss
|
|
1410
|
-
break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
|
|
1489
|
+
break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
|
|
1411
1490
|
};
|
|
1412
1491
|
|
|
1413
1492
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1418,13 +1497,17 @@ fn moveHit(
|
|
|
1418
1497
|
|
|
1419
1498
|
if (!miss) return true;
|
|
1420
1499
|
if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
|
|
1500
|
+
|
|
1421
1501
|
side.active.volatiles.Binding = false;
|
|
1502
|
+
options.chance.observe(.binding, player, .None);
|
|
1503
|
+
|
|
1422
1504
|
return false;
|
|
1423
1505
|
}
|
|
1424
1506
|
|
|
1425
1507
|
fn checkFaint(
|
|
1426
1508
|
battle: anytype,
|
|
1427
1509
|
player: Player,
|
|
1510
|
+
cap: bool,
|
|
1428
1511
|
options: anytype,
|
|
1429
1512
|
) @TypeOf(options.log).Error!?Result {
|
|
1430
1513
|
const side = battle.side(player);
|
|
@@ -1437,21 +1520,21 @@ fn checkFaint(
|
|
|
1437
1520
|
const foe_out = foe_fainted and findFirstAlive(foe) == 0;
|
|
1438
1521
|
const more = player_out or foe_out;
|
|
1439
1522
|
|
|
1440
|
-
if (try faint(battle, player, !(more or foe_fainted), options)) |r| return r;
|
|
1441
|
-
if (foe_fainted) if (try faint(battle, player.foe(), !more, options)) |r| return r;
|
|
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;
|
|
1442
1525
|
|
|
1443
1526
|
assert(!side.active.volatiles.MultiHit);
|
|
1444
1527
|
assert(!foe.active.volatiles.MultiHit);
|
|
1445
1528
|
|
|
1446
1529
|
if (player_out and foe_out) {
|
|
1447
1530
|
try options.log.tie(.{});
|
|
1448
|
-
return
|
|
1531
|
+
return .Tie;
|
|
1449
1532
|
} else if (player_out) {
|
|
1450
1533
|
try options.log.win(.{player.foe()});
|
|
1451
|
-
return if (player == .P1)
|
|
1534
|
+
return if (player == .P1) .Lose else .Win;
|
|
1452
1535
|
} else if (foe_out) {
|
|
1453
1536
|
try options.log.win(.{player});
|
|
1454
|
-
return if (player == .P1)
|
|
1537
|
+
return if (player == .P1) .Win else .Lose;
|
|
1455
1538
|
}
|
|
1456
1539
|
|
|
1457
1540
|
const foe_choice: Choice.Type = if (foe_fainted) .Switch else .Pass;
|
|
@@ -1459,7 +1542,7 @@ fn checkFaint(
|
|
|
1459
1542
|
return .{ .p1 = foe_choice, .p2 = .Switch };
|
|
1460
1543
|
}
|
|
1461
1544
|
|
|
1462
|
-
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 {
|
|
1463
1546
|
var side = battle.side(player);
|
|
1464
1547
|
var foe = battle.foe(player);
|
|
1465
1548
|
assert(side.stored().hp == 0);
|
|
@@ -1468,8 +1551,8 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
|
|
|
1468
1551
|
assert(!foe_volatiles.MultiHit);
|
|
1469
1552
|
if (foe_volatiles.Bide) {
|
|
1470
1553
|
assert(!foe_volatiles.Thrashing and !foe_volatiles.Rage);
|
|
1471
|
-
foe_volatiles.state = if (
|
|
1472
|
-
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;
|
|
1473
1556
|
}
|
|
1474
1557
|
|
|
1475
1558
|
// Clearing these is not strictly necessary as provided the battle hasn't ended the side that
|
|
@@ -1493,7 +1576,9 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
|
|
|
1493
1576
|
}
|
|
1494
1577
|
|
|
1495
1578
|
try options.log.faint(.{ battle.active(player), done });
|
|
1496
|
-
|
|
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);
|
|
1497
1582
|
return null;
|
|
1498
1583
|
}
|
|
1499
1584
|
|
|
@@ -1557,21 +1642,21 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
|
|
|
1557
1642
|
|
|
1558
1643
|
battle.turn += 1;
|
|
1559
1644
|
|
|
1560
|
-
if (
|
|
1645
|
+
if (pkmn.options.mod and pkmn.options.ebc and checkEBC(battle)) {
|
|
1561
1646
|
try options.log.tie(.{});
|
|
1562
|
-
return
|
|
1647
|
+
return .Tie;
|
|
1563
1648
|
}
|
|
1564
1649
|
|
|
1565
|
-
if (
|
|
1650
|
+
if (pkmn.options.mod and battle.turn >= 1000) {
|
|
1566
1651
|
try options.log.tie(.{});
|
|
1567
|
-
return
|
|
1652
|
+
return .Tie;
|
|
1568
1653
|
} else if (battle.turn >= 65535) {
|
|
1569
|
-
return
|
|
1654
|
+
return .Error;
|
|
1570
1655
|
}
|
|
1571
1656
|
|
|
1572
1657
|
try options.log.turn(.{battle.turn});
|
|
1573
1658
|
|
|
1574
|
-
return
|
|
1659
|
+
return .Default;
|
|
1575
1660
|
}
|
|
1576
1661
|
|
|
1577
1662
|
fn checkEBC(battle: anytype) bool {
|
|
@@ -1653,17 +1738,6 @@ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
|
|
|
1653
1738
|
}
|
|
1654
1739
|
}
|
|
1655
1740
|
|
|
1656
|
-
fn handleThrashing(battle: anytype, active: *ActivePokemon, player: Player, options: anytype) bool {
|
|
1657
|
-
var volatiles = &active.volatiles;
|
|
1658
|
-
assert(volatiles.Thrashing);
|
|
1659
|
-
if (volatiles.attacks > 0) return false;
|
|
1660
|
-
|
|
1661
|
-
volatiles.Thrashing = false;
|
|
1662
|
-
volatiles.Confusion = true;
|
|
1663
|
-
volatiles.confusion = Rolls.confusionDuration(battle, player, true, options);
|
|
1664
|
-
return true;
|
|
1665
|
-
}
|
|
1666
|
-
|
|
1667
1741
|
fn onBegin(
|
|
1668
1742
|
battle: anytype,
|
|
1669
1743
|
player: Player,
|
|
@@ -1750,8 +1824,10 @@ pub const Effects = struct {
|
|
|
1750
1824
|
|
|
1751
1825
|
side.active.volatiles.Bide = true;
|
|
1752
1826
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1827
|
+
|
|
1753
1828
|
side.active.volatiles.state = 0;
|
|
1754
1829
|
side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
1830
|
+
options.chance.observe(.attacking, player, .started);
|
|
1755
1831
|
|
|
1756
1832
|
try options.log.start(.{ battle.active(player), .Bide });
|
|
1757
1833
|
}
|
|
@@ -1813,12 +1889,14 @@ pub const Effects = struct {
|
|
|
1813
1889
|
if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
1814
1890
|
if (!try checkHit(battle, player, move, options)) return;
|
|
1815
1891
|
}
|
|
1816
|
-
try options.chance.commit(player, .hit);
|
|
1817
1892
|
}
|
|
1818
1893
|
|
|
1819
1894
|
if (foe.active.volatiles.Confusion) return;
|
|
1820
1895
|
foe.active.volatiles.Confusion = true;
|
|
1821
|
-
|
|
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());
|
|
1822
1900
|
|
|
1823
1901
|
try options.log.start(.{ battle.active(player.foe()), .Confusion });
|
|
1824
1902
|
}
|
|
@@ -1846,10 +1924,7 @@ pub const Effects = struct {
|
|
|
1846
1924
|
const foe_ident = battle.active(player.foe());
|
|
1847
1925
|
|
|
1848
1926
|
// Pokémon Showdown handles hit/miss earlier in doMove
|
|
1849
|
-
if (!showdown)
|
|
1850
|
-
if (!try checkHit(battle, player, move, options)) return null;
|
|
1851
|
-
try options.chance.commit(player, .hit);
|
|
1852
|
-
}
|
|
1927
|
+
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
1853
1928
|
|
|
1854
1929
|
if (volatiles.disable_move != 0) {
|
|
1855
1930
|
try options.log.fail(.{ foe_ident, .None });
|
|
@@ -1867,18 +1942,18 @@ pub const Effects = struct {
|
|
|
1867
1942
|
|
|
1868
1943
|
// Technically this is still considered simply a "miss" on the cartridge,
|
|
1869
1944
|
// but diverging from Pokémon Showdown here would mostly just be pedantic
|
|
1870
|
-
if (n == 0) {
|
|
1945
|
+
if (n == 0 or (pkmn.options.mod and err)) {
|
|
1871
1946
|
try options.log.fail(.{ foe_ident, .None });
|
|
1872
1947
|
return null;
|
|
1873
1948
|
} else if (err) {
|
|
1874
1949
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
1875
|
-
|
|
1876
|
-
return Result.Error;
|
|
1950
|
+
return .Error;
|
|
1877
1951
|
}
|
|
1878
1952
|
|
|
1879
1953
|
volatiles.disable_move =
|
|
1880
1954
|
@intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
|
|
1881
1955
|
volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
|
|
1956
|
+
options.chance.observe(.disable, player.foe(), .started);
|
|
1882
1957
|
|
|
1883
1958
|
const id = foe.active.move(volatiles.disable_move).id;
|
|
1884
1959
|
try options.log.start(.{ foe_ident, .Disable, id });
|
|
@@ -1949,7 +2024,7 @@ pub const Effects = struct {
|
|
|
1949
2024
|
if (foe.active.types.includes(move.type)) return;
|
|
1950
2025
|
if (!try Rolls.secondaryChance(battle, player, true, options)) return;
|
|
1951
2026
|
// Freeze Clause Mod
|
|
1952
|
-
if (
|
|
2027
|
+
if (pkmn.options.mod) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
1953
2028
|
|
|
1954
2029
|
foe_stored.status = Status.init(.FRZ);
|
|
1955
2030
|
// GLITCH: Hyper Beam recharging status is not cleared
|
|
@@ -1985,22 +2060,24 @@ pub const Effects = struct {
|
|
|
1985
2060
|
if (p != player and Status.any(s.stored().status)) {
|
|
1986
2061
|
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
1987
2062
|
s.stored().status = 0;
|
|
2063
|
+
options.chance.observe(.sleep, p, .None);
|
|
1988
2064
|
} else if (showdown and s.stored().status == Status.TOX) {
|
|
1989
2065
|
s.stored().status = Status.init(.PSN);
|
|
1990
2066
|
try log.status(.{ battle.active(p), s.stored().status, .None });
|
|
1991
2067
|
}
|
|
1992
|
-
try clearVolatiles(battle, p,
|
|
2068
|
+
try clearVolatiles(battle, p, options);
|
|
1993
2069
|
}
|
|
1994
2070
|
} else {
|
|
1995
2071
|
if (Status.any(foe_stored.status)) {
|
|
1996
2072
|
if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
|
|
1997
2073
|
foe.last_selected_move = .SKIP_TURN;
|
|
2074
|
+
options.chance.observe(.sleep, player.foe(), .None);
|
|
1998
2075
|
}
|
|
1999
2076
|
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
2000
2077
|
foe_stored.status = 0;
|
|
2001
2078
|
}
|
|
2002
|
-
try clearVolatiles(battle, player,
|
|
2003
|
-
try clearVolatiles(battle, player.foe(),
|
|
2079
|
+
try clearVolatiles(battle, player, options);
|
|
2080
|
+
try clearVolatiles(battle, player.foe(), options);
|
|
2004
2081
|
}
|
|
2005
2082
|
}
|
|
2006
2083
|
|
|
@@ -2051,7 +2128,6 @@ pub const Effects = struct {
|
|
|
2051
2128
|
}
|
|
2052
2129
|
}
|
|
2053
2130
|
|
|
2054
|
-
try options.chance.commit(player, .hit);
|
|
2055
2131
|
foe.active.volatiles.LeechSeed = true;
|
|
2056
2132
|
|
|
2057
2133
|
try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
|
|
@@ -2092,12 +2168,10 @@ pub const Effects = struct {
|
|
|
2092
2168
|
if (!has_mimic) {
|
|
2093
2169
|
// Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
|
|
2094
2170
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2095
|
-
try options.chance.commit(player, .hit);
|
|
2096
2171
|
return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
2097
2172
|
}
|
|
2098
2173
|
}
|
|
2099
2174
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2100
|
-
try options.chance.commit(player, .hit);
|
|
2101
2175
|
|
|
2102
2176
|
const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
|
|
2103
2177
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
@@ -2151,7 +2225,6 @@ pub const Effects = struct {
|
|
|
2151
2225
|
if (immune) return log.immune(.{ foe_ident, .None });
|
|
2152
2226
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2153
2227
|
}
|
|
2154
|
-
try options.chance.commit(player, .hit);
|
|
2155
2228
|
|
|
2156
2229
|
foe_stored.status = Status.init(.PAR);
|
|
2157
2230
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
@@ -2219,7 +2292,6 @@ pub const Effects = struct {
|
|
|
2219
2292
|
|
|
2220
2293
|
if (move.effect == .Poison) {
|
|
2221
2294
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2222
|
-
try options.chance.commit(player, .hit);
|
|
2223
2295
|
} else {
|
|
2224
2296
|
if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
|
|
2225
2297
|
return;
|
|
@@ -2288,13 +2360,12 @@ pub const Effects = struct {
|
|
|
2288
2360
|
if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
|
|
2289
2361
|
});
|
|
2290
2362
|
}
|
|
2291
|
-
// If
|
|
2363
|
+
// If moveHit in doMove didn't return true Pokémon Showdown wouldn't be in here
|
|
2292
2364
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2293
|
-
try options.chance.commit(player, .hit);
|
|
2294
2365
|
}
|
|
2295
2366
|
|
|
2296
2367
|
// Sleep Clause Mod
|
|
2297
|
-
if (
|
|
2368
|
+
if (pkmn.options.mod) {
|
|
2298
2369
|
for (foe.pokemon) |p| {
|
|
2299
2370
|
if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
|
|
2300
2371
|
}
|
|
@@ -2302,6 +2373,8 @@ pub const Effects = struct {
|
|
|
2302
2373
|
foe.active.volatiles.Recharging = false;
|
|
2303
2374
|
|
|
2304
2375
|
foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
|
|
2376
|
+
options.chance.observe(.sleep, player.foe(), .started);
|
|
2377
|
+
|
|
2305
2378
|
const last = battle.side(player).last_selected_move;
|
|
2306
2379
|
try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
|
|
2307
2380
|
}
|
|
@@ -2343,9 +2416,7 @@ pub const Effects = struct {
|
|
|
2343
2416
|
// Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
|
|
2344
2417
|
if (showdown) {
|
|
2345
2418
|
if (battle.side(player).last_selected_move == .Teleport) return;
|
|
2346
|
-
|
|
2347
|
-
try options.chance.commit(player, .hit);
|
|
2348
|
-
}
|
|
2419
|
+
_ = try checkHit(battle, player, move, options);
|
|
2349
2420
|
battle.last_damage = 0;
|
|
2350
2421
|
} else {
|
|
2351
2422
|
try options.log.fail(.{ battle.active(player), .None });
|
|
@@ -2360,6 +2431,8 @@ pub const Effects = struct {
|
|
|
2360
2431
|
|
|
2361
2432
|
volatiles.Thrashing = true;
|
|
2362
2433
|
volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
2434
|
+
|
|
2435
|
+
options.chance.observe(.attacking, player, .started);
|
|
2363
2436
|
}
|
|
2364
2437
|
|
|
2365
2438
|
fn transform(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -2398,13 +2471,14 @@ pub const Effects = struct {
|
|
|
2398
2471
|
if (side.active.volatiles.Binding) return;
|
|
2399
2472
|
side.active.volatiles.Binding = true;
|
|
2400
2473
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2401
|
-
// (Pokémon Showdown
|
|
2474
|
+
// (Pokémon Showdown clears Recharging in onTryMove instead)
|
|
2402
2475
|
if (!showdown) {
|
|
2403
2476
|
foe.active.volatiles.Recharging = false;
|
|
2404
2477
|
} else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
|
|
2405
2478
|
|
|
2406
2479
|
side.active.volatiles.attacks =
|
|
2407
2480
|
try Rolls.distribution(battle, .Binding, player, options) - 1;
|
|
2481
|
+
options.chance.observe(.binding, player, .started);
|
|
2408
2482
|
}
|
|
2409
2483
|
|
|
2410
2484
|
fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2521,7 +2595,6 @@ pub const Effects = struct {
|
|
|
2521
2595
|
} else {
|
|
2522
2596
|
// checkHit already checks for Invulnerable
|
|
2523
2597
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2524
|
-
try options.chance.commit(player, .hit);
|
|
2525
2598
|
}
|
|
2526
2599
|
|
|
2527
2600
|
var stats = &foe.active.stats;
|
|
@@ -2635,22 +2708,22 @@ fn isForced(active: anytype) bool {
|
|
|
2635
2708
|
active.volatiles.Thrashing or active.volatiles.Charging;
|
|
2636
2709
|
}
|
|
2637
2710
|
|
|
2638
|
-
fn clearVolatiles(battle: anytype, who: Player,
|
|
2711
|
+
fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
|
|
2639
2712
|
var log = options.log;
|
|
2640
2713
|
var side = battle.side(who);
|
|
2641
2714
|
var volatiles = &side.active.volatiles;
|
|
2642
2715
|
const ident = battle.active(who);
|
|
2643
2716
|
|
|
2644
|
-
options.chance.clearDurations(who, clear);
|
|
2645
|
-
|
|
2646
2717
|
if (volatiles.disable_move != 0) {
|
|
2647
2718
|
volatiles.disable_move = 0;
|
|
2648
2719
|
volatiles.disable_duration = 0;
|
|
2720
|
+
options.chance.observe(.disable, who, .None);
|
|
2649
2721
|
try log.end(.{ ident, .DisableSilent });
|
|
2650
2722
|
}
|
|
2651
2723
|
if (volatiles.Confusion) {
|
|
2652
2724
|
// volatiles.confusion is left unchanged
|
|
2653
2725
|
volatiles.Confusion = false;
|
|
2726
|
+
options.chance.observe(.confusion, who, .None);
|
|
2654
2727
|
try log.end(.{ ident, .ConfusionSilent });
|
|
2655
2728
|
}
|
|
2656
2729
|
if (volatiles.Mist) {
|
|
@@ -2685,10 +2758,25 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
|
|
|
2685
2758
|
}
|
|
2686
2759
|
}
|
|
2687
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
|
+
|
|
2688
2774
|
pub const Rolls = struct {
|
|
2689
2775
|
fn speedTie(battle: anytype, options: anytype) !bool {
|
|
2690
2776
|
const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
|
|
2691
2777
|
player == .P1
|
|
2778
|
+
else if (@hasDecl(@TypeOf(battle.rng), "speedTie"))
|
|
2779
|
+
battle.rng.speedTie()
|
|
2692
2780
|
else if (showdown)
|
|
2693
2781
|
battle.rng.range(u8, 0, 2) == 0
|
|
2694
2782
|
else
|
|
@@ -2698,21 +2786,31 @@ pub const Rolls = struct {
|
|
|
2698
2786
|
return p1;
|
|
2699
2787
|
}
|
|
2700
2788
|
|
|
2701
|
-
fn criticalHit(
|
|
2789
|
+
fn criticalHit(
|
|
2790
|
+
battle: anytype,
|
|
2791
|
+
player: Player,
|
|
2792
|
+
rate: u8,
|
|
2793
|
+
spurious: bool,
|
|
2794
|
+
options: anytype,
|
|
2795
|
+
) !bool {
|
|
2702
2796
|
const crit = if (options.calc.overridden(player, .critical_hit)) |val|
|
|
2703
2797
|
val == .true
|
|
2798
|
+
else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
|
|
2799
|
+
battle.rng.criticalHit(player, rate)
|
|
2704
2800
|
else if (showdown)
|
|
2705
2801
|
battle.rng.chance(u8, rate, 256)
|
|
2706
2802
|
else
|
|
2707
2803
|
std.math.rotl(u8, battle.rng.next(), 3) < rate;
|
|
2708
2804
|
|
|
2709
|
-
try options.chance.criticalHit(player, crit, rate);
|
|
2805
|
+
if (!spurious) try options.chance.criticalHit(player, crit, rate);
|
|
2710
2806
|
return crit;
|
|
2711
2807
|
}
|
|
2712
2808
|
|
|
2713
2809
|
fn damage(battle: anytype, player: Player, options: anytype) !u8 {
|
|
2714
2810
|
const roll = if (options.calc.overridden(player, .damage)) |val|
|
|
2715
2811
|
val
|
|
2812
|
+
else if (@hasDecl(@TypeOf(battle.rng), "damage"))
|
|
2813
|
+
battle.rng.damage(player)
|
|
2716
2814
|
else roll: {
|
|
2717
2815
|
if (showdown) break :roll battle.rng.range(u8, 217, 256);
|
|
2718
2816
|
while (true) {
|
|
@@ -2726,21 +2824,27 @@ pub const Rolls = struct {
|
|
|
2726
2824
|
return roll;
|
|
2727
2825
|
}
|
|
2728
2826
|
|
|
2729
|
-
|
|
2827
|
+
const uN = if (pkmn.options.miss) u8 else u9;
|
|
2828
|
+
fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) !bool {
|
|
2829
|
+
const acc: uN = if (pkmn.options.miss) accuracy else @as(uN, accuracy) + 1;
|
|
2730
2830
|
const ok = if (options.calc.overridden(player, .hit)) |val|
|
|
2731
2831
|
val == .true
|
|
2832
|
+
else if (@hasDecl(@TypeOf(battle.rng), "hit"))
|
|
2833
|
+
battle.rng.hit(player, acc)
|
|
2732
2834
|
else if (showdown)
|
|
2733
|
-
battle.rng.chance(
|
|
2835
|
+
battle.rng.chance(uN, acc, 256)
|
|
2734
2836
|
else
|
|
2735
|
-
battle.rng.next() <
|
|
2837
|
+
battle.rng.next() < acc;
|
|
2736
2838
|
|
|
2737
|
-
options.chance.hit(ok,
|
|
2839
|
+
try options.chance.hit(player, ok, acc);
|
|
2738
2840
|
return ok;
|
|
2739
2841
|
}
|
|
2740
2842
|
|
|
2741
2843
|
fn confused(battle: anytype, player: Player, options: anytype) !bool {
|
|
2742
2844
|
const cfz = if (options.calc.overridden(player, .confused)) |val|
|
|
2743
2845
|
val == .true
|
|
2846
|
+
else if (@hasDecl(@TypeOf(battle.rng), "confused"))
|
|
2847
|
+
battle.rng.confused(player)
|
|
2744
2848
|
else if (showdown)
|
|
2745
2849
|
!battle.rng.chance(u8, 128, 256)
|
|
2746
2850
|
else
|
|
@@ -2753,6 +2857,8 @@ pub const Rolls = struct {
|
|
|
2753
2857
|
fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
|
|
2754
2858
|
const par = if (options.calc.overridden(player, .paralyzed)) |val|
|
|
2755
2859
|
val == .true
|
|
2860
|
+
else if (@hasDecl(@TypeOf(battle.rng), "paralyzed"))
|
|
2861
|
+
battle.rng.paralyzed(player)
|
|
2756
2862
|
else if (showdown)
|
|
2757
2863
|
battle.rng.chance(u8, 63, 256)
|
|
2758
2864
|
else
|
|
@@ -2765,6 +2871,8 @@ pub const Rolls = struct {
|
|
|
2765
2871
|
fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
|
|
2766
2872
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2767
2873
|
val == .true
|
|
2874
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2875
|
+
battle.rng.secondaryChance(player, 25)
|
|
2768
2876
|
else if (showdown)
|
|
2769
2877
|
battle.rng.chance(u8, 25, 256)
|
|
2770
2878
|
else
|
|
@@ -2779,6 +2887,8 @@ pub const Rolls = struct {
|
|
|
2779
2887
|
|
|
2780
2888
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2781
2889
|
val == .true
|
|
2890
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2891
|
+
battle.rng.secondaryChance(player, rate)
|
|
2782
2892
|
else if (showdown)
|
|
2783
2893
|
battle.rng.chance(u8, rate, 256)
|
|
2784
2894
|
else
|
|
@@ -2793,6 +2903,8 @@ pub const Rolls = struct {
|
|
|
2793
2903
|
|
|
2794
2904
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2795
2905
|
val == .true
|
|
2906
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2907
|
+
battle.rng.secondaryChance(player, rate)
|
|
2796
2908
|
else if (showdown)
|
|
2797
2909
|
battle.rng.chance(u8, rate, 256)
|
|
2798
2910
|
else
|
|
@@ -2805,6 +2917,8 @@ pub const Rolls = struct {
|
|
|
2805
2917
|
fn unboost(battle: anytype, player: Player, options: anytype) !bool {
|
|
2806
2918
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2807
2919
|
val == .true
|
|
2920
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2921
|
+
battle.rng.secondaryChance(player, 85)
|
|
2808
2922
|
else if (showdown)
|
|
2809
2923
|
battle.rng.chance(u8, 85, 256)
|
|
2810
2924
|
else
|
|
@@ -2817,6 +2931,8 @@ pub const Rolls = struct {
|
|
|
2817
2931
|
fn metronome(battle: anytype, player: Player, options: anytype) !Move {
|
|
2818
2932
|
const move: Move = if (options.calc.overridden(player, .metronome)) |val|
|
|
2819
2933
|
val
|
|
2934
|
+
else if (@hasDecl(@TypeOf(battle.rng), "metronome"))
|
|
2935
|
+
battle.rng.metronome(player)
|
|
2820
2936
|
else if (showdown)
|
|
2821
2937
|
Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
|
|
2822
2938
|
else move: {
|
|
@@ -2835,6 +2951,8 @@ pub const Rolls = struct {
|
|
|
2835
2951
|
fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
|
|
2836
2952
|
const power = if (options.calc.overridden(player, .psywave)) |val|
|
|
2837
2953
|
val - 1
|
|
2954
|
+
else if (@hasDecl(@TypeOf(battle.rng), "psywave"))
|
|
2955
|
+
battle.rng.psywave(player)
|
|
2838
2956
|
else if (showdown)
|
|
2839
2957
|
battle.rng.range(u8, 0, max)
|
|
2840
2958
|
else power: {
|
|
@@ -2850,8 +2968,11 @@ pub const Rolls = struct {
|
|
|
2850
2968
|
}
|
|
2851
2969
|
|
|
2852
2970
|
fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2853
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2854
|
-
@intCast(val)
|
|
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"))
|
|
2975
|
+
battle.rng.sleepDuration(player)
|
|
2855
2976
|
else if (showdown)
|
|
2856
2977
|
battle.rng.range(u3, 1, 8)
|
|
2857
2978
|
else duration: {
|
|
@@ -2862,46 +2983,55 @@ pub const Rolls = struct {
|
|
|
2862
2983
|
};
|
|
2863
2984
|
|
|
2864
2985
|
assert(duration >= 1 and duration <= 7);
|
|
2865
|
-
options.chance.duration(
|
|
2986
|
+
options.chance.duration(player, duration);
|
|
2866
2987
|
return duration;
|
|
2867
2988
|
}
|
|
2868
2989
|
|
|
2869
2990
|
fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
|
|
2870
|
-
const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
|
|
2871
|
-
val
|
|
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"))
|
|
2995
|
+
battle.rng.disableDuration(player)
|
|
2872
2996
|
else if (showdown)
|
|
2873
2997
|
battle.rng.range(u4, 1, 9)
|
|
2874
2998
|
else
|
|
2875
2999
|
@intCast((battle.rng.next() & 7) + 1);
|
|
2876
3000
|
|
|
2877
3001
|
assert(duration >= 1 and duration <= 8);
|
|
2878
|
-
options.chance.duration(
|
|
3002
|
+
options.chance.duration(player, duration);
|
|
2879
3003
|
return duration;
|
|
2880
3004
|
}
|
|
2881
3005
|
|
|
2882
|
-
fn confusionDuration(battle: anytype, player: Player,
|
|
2883
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2884
|
-
@intCast(val)
|
|
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)
|
|
2885
3012
|
else if (showdown)
|
|
2886
3013
|
battle.rng.range(u3, 2, 6)
|
|
2887
3014
|
else
|
|
2888
3015
|
@intCast((battle.rng.next() & 3) + 2);
|
|
2889
3016
|
|
|
2890
3017
|
assert(duration >= 2 and duration <= 5);
|
|
2891
|
-
options.chance.duration(
|
|
3018
|
+
options.chance.duration(player, duration);
|
|
2892
3019
|
return duration;
|
|
2893
3020
|
}
|
|
2894
3021
|
|
|
2895
3022
|
fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2896
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2897
|
-
@intCast(val)
|
|
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"))
|
|
3027
|
+
battle.rng.attackingDuration(player)
|
|
2898
3028
|
else if (showdown)
|
|
2899
3029
|
battle.rng.range(u3, 2, 4)
|
|
2900
3030
|
else
|
|
2901
3031
|
@intCast((battle.rng.next() & 1) + 2);
|
|
2902
3032
|
|
|
2903
3033
|
assert(duration >= 2 and duration <= 3);
|
|
2904
|
-
options.chance.duration(
|
|
3034
|
+
options.chance.duration(player, duration);
|
|
2905
3035
|
return duration;
|
|
2906
3036
|
}
|
|
2907
3037
|
|
|
@@ -2914,8 +3044,11 @@ pub const Rolls = struct {
|
|
|
2914
3044
|
options: anytype,
|
|
2915
3045
|
) !u3 {
|
|
2916
3046
|
const roll = if (effect == .Binding) .duration else .multi_hit;
|
|
2917
|
-
const n: u3 = if (options.calc.overridden(player, roll)) |val|
|
|
2918
|
-
@intCast(val)
|
|
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"))
|
|
3051
|
+
battle.rng.distribution(player)
|
|
2919
3052
|
else if (showdown)
|
|
2920
3053
|
DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
|
|
2921
3054
|
else n: {
|
|
@@ -2925,7 +3058,7 @@ pub const Rolls = struct {
|
|
|
2925
3058
|
|
|
2926
3059
|
assert(n >= 2 and n <= 5);
|
|
2927
3060
|
if (effect == .Binding) {
|
|
2928
|
-
options.chance.duration(
|
|
3061
|
+
options.chance.duration(player, n);
|
|
2929
3062
|
} else {
|
|
2930
3063
|
try options.chance.multiHit(player, n);
|
|
2931
3064
|
}
|
|
@@ -2940,15 +3073,15 @@ pub const Rolls = struct {
|
|
|
2940
3073
|
options: anytype,
|
|
2941
3074
|
) !u4 {
|
|
2942
3075
|
// TODO: consider throwing error instead of rerolling?
|
|
2943
|
-
const
|
|
3076
|
+
const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
|
|
2944
3077
|
if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
|
|
2945
3078
|
val
|
|
2946
3079
|
else
|
|
2947
3080
|
null
|
|
2948
3081
|
else
|
|
2949
|
-
null
|
|
2950
|
-
|
|
2951
|
-
|
|
3082
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
|
|
3083
|
+
battle.rng.moveSlot(player, moves, check_pp)
|
|
3084
|
+
else slot: {
|
|
2952
3085
|
if (showdown) {
|
|
2953
3086
|
if (check_pp == 0) {
|
|
2954
3087
|
var i: usize = moves.len;
|
|
@@ -3012,11 +3145,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3012
3145
|
},
|
|
3013
3146
|
.Switch => {
|
|
3014
3147
|
const side = battle.side(player);
|
|
3015
|
-
var slot:
|
|
3148
|
+
var slot: usize = 2;
|
|
3016
3149
|
while (slot <= 6) : (slot += 1) {
|
|
3017
3150
|
const id = side.order[slot - 1];
|
|
3018
3151
|
if (id == 0 or side.pokemon[id - 1].hp == 0) continue;
|
|
3019
|
-
out[n] = .{ .type = .Switch, .data = slot };
|
|
3152
|
+
out[n] = .{ .type = .Switch, .data = @intCast(slot) };
|
|
3020
3153
|
n += 1;
|
|
3021
3154
|
}
|
|
3022
3155
|
if (n == 0) {
|
|
@@ -3032,7 +3165,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3032
3165
|
const stored = side.stored();
|
|
3033
3166
|
|
|
3034
3167
|
// While players are not given any input options on the cartridge in these cases,
|
|
3035
|
-
// Pokémon Showdown instead produces a list with a single move that must be chosen
|
|
3168
|
+
// Pokémon Showdown instead produces a list with a single move that must be chosen
|
|
3036
3169
|
//
|
|
3037
3170
|
// Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
|
|
3038
3171
|
// here when not in compatibility mode, but the engine needs to be able to differentiate
|
|
@@ -3040,18 +3173,18 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3040
3173
|
// passing due to being forced into using a move. Instead of introducing another option
|
|
3041
3174
|
// we simply repurpose Move with no move slot, even though pedantically this is not
|
|
3042
3175
|
// strictly correct as the player would not have been presented the option to move or
|
|
3043
|
-
// switch at all
|
|
3176
|
+
// switch at all
|
|
3044
3177
|
if (isForced(active)) {
|
|
3045
3178
|
out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
|
|
3046
3179
|
n += 1;
|
|
3047
3180
|
return n;
|
|
3048
3181
|
}
|
|
3049
3182
|
|
|
3050
|
-
var slot:
|
|
3183
|
+
var slot: usize = 2;
|
|
3051
3184
|
while (slot <= 6) : (slot += 1) {
|
|
3052
3185
|
const id = side.order[slot - 1];
|
|
3053
3186
|
if (id == 0 or side.pokemon[id - 1].hp == 0) continue;
|
|
3054
|
-
out[n] = .{ .type = .Switch, .data = slot };
|
|
3187
|
+
out[n] = .{ .type = .Switch, .data = @intCast(slot) };
|
|
3055
3188
|
n += 1;
|
|
3056
3189
|
}
|
|
3057
3190
|
|
|
@@ -3059,7 +3192,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3059
3192
|
// On the cartridge, all of these happen after "FIGHT" (indicating you are not
|
|
3060
3193
|
// switching) but before you are allowed to select a move. Pokémon Showdown instead
|
|
3061
3194
|
// either disables all other moves in the case of limited or requires you to select a
|
|
3062
|
-
// move normally if sleeping/frozen/bound
|
|
3195
|
+
// move normally if sleeping/frozen/bound
|
|
3063
3196
|
if (!showdown and (limited or foe.active.volatiles.Binding or
|
|
3064
3197
|
Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
|
|
3065
3198
|
{
|
|
@@ -3072,7 +3205,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3072
3205
|
// Pokémon Showdown handles Bide and Binding moves by checking if the move in question
|
|
3073
3206
|
// is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
|
|
3074
3207
|
// Move will not result in forcing the subsequent use unless the user also had the
|
|
3075
|
-
// proc-ed move in their moveset) and disabling all other moves
|
|
3208
|
+
// proc-ed move in their moveset) and disabling all other moves
|
|
3076
3209
|
if (limited) {
|
|
3077
3210
|
assert(showdown);
|
|
3078
3211
|
assert(side.last_selected_move != .None);
|
|
@@ -3084,7 +3217,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3084
3217
|
const struggle =
|
|
3085
3218
|
m.id == .Bide and (m.pp == 0 or active.volatiles.disable_move == slot);
|
|
3086
3219
|
const s = if (struggle) 0 else slot;
|
|
3087
|
-
out[n] = .{ .type = .Move, .data = s };
|
|
3220
|
+
out[n] = .{ .type = .Move, .data = @intCast(s) };
|
|
3088
3221
|
n += 1;
|
|
3089
3222
|
return n;
|
|
3090
3223
|
}
|
|
@@ -3098,13 +3231,13 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3098
3231
|
if (m.id == .None) break;
|
|
3099
3232
|
if (m.pp == 0) continue;
|
|
3100
3233
|
if (active.volatiles.disable_move == slot) continue;
|
|
3101
|
-
out[n] = .{ .type = .Move, .data = slot };
|
|
3234
|
+
out[n] = .{ .type = .Move, .data = @intCast(slot) };
|
|
3102
3235
|
n += 1;
|
|
3103
3236
|
}
|
|
3104
3237
|
// Struggle (Pokémon Showdown would use 'move 1' here)
|
|
3105
3238
|
if (n == before) {
|
|
3106
3239
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
3107
|
-
if (!
|
|
3240
|
+
if (!pkmn.options.mod) {
|
|
3108
3241
|
while (slot <= 4) : (slot += 1) {
|
|
3109
3242
|
if (active.moves[slot - 1].pp != 0) return n;
|
|
3110
3243
|
}
|