@pkmn/engine 0.1.0-dev.80019d10 → 0.1.0-dev.8058a1d0
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 +59 -24
- 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 -5
- package/build/pkg/data.js.map +1 -1
- package/build/pkg/gen1.d.ts +15 -18
- package/build/pkg/gen1.js +28 -19
- package/build/pkg/gen1.js.map +1 -1
- package/build/pkg/index.d.ts +42 -91
- 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 +259 -112
- package/package.json +61 -132
- package/src/bin/install-pkmn-engine +47 -21
- package/src/bin/pkmn-debug +1 -1
- package/src/lib/bindings/c.zig +200 -0
- package/src/lib/{binding → bindings}/node.zig +15 -32
- package/src/lib/bindings/wasm.zig +50 -0
- package/src/lib/common/array.zig +70 -0
- package/src/lib/common/data.zig +4 -4
- package/src/lib/common/debug.zig +33 -28
- package/src/lib/common/js.zig +3 -7
- package/src/lib/common/optional.zig +12 -18
- package/src/lib/common/options.zig +6 -0
- package/src/lib/common/protocol.zig +256 -224
- package/src/lib/common/rational.zig +340 -253
- package/src/lib/common/rng.zig +7 -9
- package/src/lib/common/util.zig +33 -1
- package/src/lib/gen1/calc.zig +724 -175
- package/src/lib/gen1/chance.zig +453 -291
- 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 +29 -32
- package/src/lib/gen1/helpers.zig +91 -549
- package/src/lib/gen1/mechanics.zig +417 -294
- package/src/lib/gen2/calc.zig +35 -28
- package/src/lib/gen2/chance.zig +85 -503
- package/src/lib/gen2/data/items.zig +1 -3
- package/src/lib/gen2/data/moves.zig +2 -4
- package/src/lib/gen2/data/species.zig +1 -3
- package/src/lib/gen2/data/types.zig +7 -5
- package/src/lib/gen2/data.zig +27 -29
- package/src/lib/gen2/generated.zig +19 -26
- package/src/lib/gen2/helpers.zig +45 -26
- package/src/lib/gen2/mechanics.zig +234 -180
- 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;
|
|
@@ -71,8 +63,8 @@ pub fn update(battle: anytype, c1: Choice, c2: Choice, options: anytype) !Result
|
|
|
71
63
|
var p1 = battle.side(.P1);
|
|
72
64
|
var p2 = battle.side(.P2);
|
|
73
65
|
|
|
74
|
-
|
|
75
|
-
|
|
66
|
+
const r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
|
|
67
|
+
const r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
|
|
76
68
|
|
|
77
69
|
if (try turnOrder(battle, c1, c2, options) == .P1) {
|
|
78
70
|
if (try doTurn(battle, .P1, c1, r1, s1, .P2, c2, r2, s2, options)) |r| return r;
|
|
@@ -90,13 +82,13 @@ fn start(battle: anytype, options: anytype) !Result {
|
|
|
90
82
|
const p1 = battle.side(.P1);
|
|
91
83
|
const p2 = battle.side(.P2);
|
|
92
84
|
|
|
93
|
-
|
|
85
|
+
const p1_slot = findFirstAlive(p1);
|
|
94
86
|
assert(!showdown or p1_slot == 1);
|
|
95
|
-
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,14 +659,8 @@ fn beforeMove(
|
|
|
635
659
|
volatiles.state +%= battle.last_damage;
|
|
636
660
|
}
|
|
637
661
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
if (options.calc.modify(player, .attacking)) |extend| {
|
|
641
|
-
if (!extend) volatiles.attacks = 0;
|
|
642
|
-
} else {
|
|
643
|
-
volatiles.attacks -= 1;
|
|
644
|
-
}
|
|
645
|
-
try options.chance.attacking(player, volatiles.attacks);
|
|
662
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
663
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
646
664
|
|
|
647
665
|
if (volatiles.attacks != 0) {
|
|
648
666
|
try log.activate(.{ ident, .Bide });
|
|
@@ -676,39 +694,40 @@ fn beforeMove(
|
|
|
676
694
|
}
|
|
677
695
|
|
|
678
696
|
if (volatiles.Thrashing) {
|
|
679
|
-
|
|
697
|
+
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
680
698
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
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);
|
|
687
708
|
|
|
688
|
-
|
|
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)
|
|
689
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);
|
|
690
719
|
}
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
if (handleThrashing(battle, active, player, options)) {
|
|
697
|
-
try log.start(.{ battle.active(player), .ConfusionSilent });
|
|
698
|
-
}
|
|
699
|
-
}
|
|
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
|
+
|
|
700
725
|
return .skip_can;
|
|
701
726
|
}
|
|
702
727
|
|
|
703
728
|
if (volatiles.Binding) {
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
if (options.calc.modify(player, .binding)) |extend| {
|
|
707
|
-
if (!extend) volatiles.attacks = 0;
|
|
708
|
-
} else {
|
|
709
|
-
volatiles.attacks -= 1;
|
|
710
|
-
}
|
|
711
|
-
try options.chance.binding(player, volatiles.attacks);
|
|
729
|
+
volatiles.attacks = decrement(.binding, player, options, volatiles.attacks);
|
|
730
|
+
try options.chance.binding(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
712
731
|
|
|
713
732
|
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
714
733
|
if (showdown or battle.last_damage != 0) {
|
|
@@ -738,7 +757,6 @@ fn canMove(
|
|
|
738
757
|
const player_ident = battle.active(player);
|
|
739
758
|
const move = Move.get(side.last_selected_move);
|
|
740
759
|
|
|
741
|
-
var skip = skip_pp;
|
|
742
760
|
if (side.active.volatiles.Charging) {
|
|
743
761
|
side.active.volatiles.Charging = false;
|
|
744
762
|
side.active.volatiles.Invulnerable = false;
|
|
@@ -750,7 +768,7 @@ fn canMove(
|
|
|
750
768
|
}
|
|
751
769
|
|
|
752
770
|
side.last_used_move = side.last_selected_move;
|
|
753
|
-
if (!
|
|
771
|
+
if (!skip_pp) decrementPP(side, mslot, auto);
|
|
754
772
|
|
|
755
773
|
const target = if (move.target == .Self) player else player.foe();
|
|
756
774
|
try options.log.move(.{ player_ident, side.last_selected_move, battle.active(target), from });
|
|
@@ -763,9 +781,14 @@ fn canMove(
|
|
|
763
781
|
|
|
764
782
|
if (move.effect == .Thrashing) {
|
|
765
783
|
Effects.thrashing(battle, player, options);
|
|
766
|
-
} else if (
|
|
767
|
-
// Pokémon Showdown handles this after hit/miss checks and damage calculation
|
|
768
|
-
|
|
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
|
+
}
|
|
769
792
|
}
|
|
770
793
|
|
|
771
794
|
return true;
|
|
@@ -802,7 +825,7 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
|
|
|
802
825
|
assert(active.move(mslot).pp == side.stored().move(mslot).pp);
|
|
803
826
|
}
|
|
804
827
|
|
|
805
|
-
fn incrementPP(side: *Side, mslot: u4) void {
|
|
828
|
+
fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
|
|
806
829
|
var active = &side.active;
|
|
807
830
|
const volatiles = &active.volatiles;
|
|
808
831
|
|
|
@@ -810,8 +833,11 @@ fn incrementPP(side: *Side, mslot: u4) void {
|
|
|
810
833
|
// GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
|
|
811
834
|
if (showdown and volatiles.Transform) return;
|
|
812
835
|
|
|
836
|
+
const n = options.calc.overridden(player, .pp) orelse 1;
|
|
837
|
+
if (volatiles.Transform and !options.chance.pp(player, n)) return;
|
|
838
|
+
|
|
813
839
|
assert(mslot > 0 and mslot <= 4);
|
|
814
|
-
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;
|
|
815
841
|
}
|
|
816
842
|
|
|
817
843
|
// Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
|
|
@@ -852,20 +878,20 @@ fn doMove(
|
|
|
852
878
|
var miss = showdown and miss: {
|
|
853
879
|
immune = move.target != .Self and !status and !counter and
|
|
854
880
|
(@intFromEnum(move.type.effectiveness(foe.active.types.type1)) == 0 or
|
|
855
|
-
|
|
881
|
+
@intFromEnum(move.type.effectiveness(foe.active.types.type2)) == 0);
|
|
856
882
|
if (immune and move.effect != .Binding) break :miss true;
|
|
857
883
|
if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) {
|
|
858
884
|
battle.last_damage = 0;
|
|
859
885
|
break :miss true;
|
|
860
886
|
}
|
|
861
887
|
break :miss (move.target != .Self and
|
|
862
|
-
!moveHit(battle, player, move, &immune, &mist, options));
|
|
888
|
+
!try moveHit(battle, player, move, &immune, &mist, options));
|
|
863
889
|
};
|
|
864
890
|
assert(!immune or miss or (showdown and move.effect == .Binding));
|
|
865
891
|
|
|
866
892
|
var late = showdown and move.effect != .Explode;
|
|
867
893
|
const skip = status or immune;
|
|
868
|
-
if ((!showdown or (!skip or counter)) and !miss)
|
|
894
|
+
if ((!showdown or (!skip or counter)) and !miss) damage: {
|
|
869
895
|
if (showdown and move.effect.isMulti()) {
|
|
870
896
|
try Effects.multiHit(battle, player, move, options);
|
|
871
897
|
hits = side.active.volatiles.attacks;
|
|
@@ -887,9 +913,10 @@ fn doMove(
|
|
|
887
913
|
ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
|
|
888
914
|
// This can overflow after adjustDamage, but will still be sufficient to OHKO
|
|
889
915
|
battle.last_damage = if (ohko) 65535 else 0;
|
|
890
|
-
if (showdown) break :
|
|
891
|
-
} else if (!calcDamage(battle, player, player.foe(), move, crit)) {
|
|
892
|
-
|
|
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;
|
|
893
920
|
}
|
|
894
921
|
if (battle.last_damage == 0) {
|
|
895
922
|
immune = true;
|
|
@@ -902,11 +929,12 @@ fn doMove(
|
|
|
902
929
|
}
|
|
903
930
|
}
|
|
904
931
|
|
|
905
|
-
var
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
932
|
+
var missed: bool = undefined;
|
|
933
|
+
const zero = battle.last_damage == 0;
|
|
934
|
+
if (!showdown and !skip) {
|
|
935
|
+
missed = !try moveHit(battle, player, move, &immune, &mist, options);
|
|
936
|
+
miss = missed or zero;
|
|
937
|
+
}
|
|
910
938
|
|
|
911
939
|
assert(showdown or miss or battle.last_damage > 0 or skip);
|
|
912
940
|
assert((!showdown and miss) or !(ohko and immune));
|
|
@@ -933,11 +961,32 @@ fn doMove(
|
|
|
933
961
|
try log.immune(.{ foe_ident, .OHKO });
|
|
934
962
|
} else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
|
|
935
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
|
+
}
|
|
936
985
|
} else if (mist) {
|
|
937
986
|
if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
|
|
938
987
|
try log.fail(.{ foe_ident, .None });
|
|
939
988
|
} else {
|
|
940
|
-
if (showdown
|
|
989
|
+
if (!showdown) try options.chance.commit(player, if (zero and !missed) .hit else .miss);
|
|
941
990
|
try log.lastmiss(.{});
|
|
942
991
|
try log.miss(.{battle.active(player)});
|
|
943
992
|
}
|
|
@@ -952,17 +1001,15 @@ fn doMove(
|
|
|
952
1001
|
try buildRage(battle, player.foe(), options);
|
|
953
1002
|
} else if (showdown and move.effect == .Disable) {
|
|
954
1003
|
try buildRage(battle, player.foe(), options);
|
|
955
|
-
} else if (!showdown and immune and move.effect == .Binding) {
|
|
956
|
-
try options.chance.commit(player, .binding);
|
|
957
1004
|
}
|
|
958
1005
|
return null;
|
|
959
|
-
} else {
|
|
1006
|
+
} else if (!showdown) {
|
|
960
1007
|
try options.chance.commit(player, .hit);
|
|
961
1008
|
}
|
|
962
1009
|
|
|
963
1010
|
// On the cartridge MultiHit doesn't get set up until after damage has been applied for the
|
|
964
1011
|
// first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
|
|
965
|
-
// it up above before damage calculation)
|
|
1012
|
+
// it up above before damage calculation)
|
|
966
1013
|
if (!showdown and move.effect.isMulti()) {
|
|
967
1014
|
try Effects.multiHit(battle, player, move, options);
|
|
968
1015
|
hits = side.active.volatiles.attacks;
|
|
@@ -972,7 +1019,7 @@ fn doMove(
|
|
|
972
1019
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
973
1020
|
|
|
974
1021
|
var nullified = false;
|
|
975
|
-
var hit:
|
|
1022
|
+
var hit: usize = 0;
|
|
976
1023
|
while (hit < hits) {
|
|
977
1024
|
if (hit == 0) {
|
|
978
1025
|
if (crit) try log.crit(.{battle.active(player.foe())});
|
|
@@ -1000,33 +1047,31 @@ fn doMove(
|
|
|
1000
1047
|
if (showdown and move.effect == .Twineedle and !nullified and foe.stored().hp > 0) {
|
|
1001
1048
|
try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
1002
1049
|
}
|
|
1003
|
-
try log.hitcount(.{ battle.active(player.foe()), hit });
|
|
1004
|
-
} else if (showdown) {
|
|
1050
|
+
try log.hitcount(.{ battle.active(player.foe()), @as(u4, @intCast(hit)) });
|
|
1051
|
+
} else if (showdown and move.effect == .Binding) {
|
|
1005
1052
|
// This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
try buildRage(battle, player.foe(), options);
|
|
1017
|
-
}
|
|
1018
|
-
return null;
|
|
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);
|
|
1019
1063
|
}
|
|
1064
|
+
return null;
|
|
1020
1065
|
}
|
|
1021
1066
|
}
|
|
1022
1067
|
|
|
1023
1068
|
// Substitute being broken nullifies the move's effect completely so even
|
|
1024
|
-
// if an effect was intended to "always happen" it will still get skipped
|
|
1069
|
+
// if an effect was intended to "always happen" it will still get skipped
|
|
1025
1070
|
if (nullified) return null;
|
|
1026
1071
|
|
|
1027
1072
|
// On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
|
|
1028
1073
|
// but this is only done to setup the MultiHit looping in the first place. Moving the MultiHit
|
|
1029
|
-
// setup before the loop means we can avoid having to waste time doing no-op handler searches
|
|
1074
|
+
// setup before the loop means we can avoid having to waste time doing no-op handler searches
|
|
1030
1075
|
if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
|
|
1031
1076
|
|
|
1032
1077
|
if (foe.stored().hp == 0) return null;
|
|
@@ -1041,7 +1086,7 @@ fn doMove(
|
|
|
1041
1086
|
// below). For Twineedle we change the data to that of one with PoisonChance1 given its
|
|
1042
1087
|
// MultiHit behavior is complete after the loop above, though Pokémon Showdown handles the
|
|
1043
1088
|
// Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
|
|
1044
|
-
// puts the |-status| message before |-hitcount| instead of after
|
|
1089
|
+
// puts the |-status| message before |-hitcount| instead of after
|
|
1045
1090
|
if (move.effect == .Twineedle) {
|
|
1046
1091
|
if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
1047
1092
|
} else if (move.effect == .Disable) {
|
|
@@ -1060,12 +1105,14 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
|
|
|
1060
1105
|
const side = battle.side(player);
|
|
1061
1106
|
|
|
1062
1107
|
// Base speed is used for the critical hit calculation, even when Transform-ed
|
|
1063
|
-
var rate =
|
|
1108
|
+
var rate: u16 = Species.chance(side.stored().species);
|
|
1064
1109
|
// GLITCH: Focus Energy reduces critical hit rate instead of increasing it
|
|
1065
1110
|
rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
|
|
1066
1111
|
rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
|
|
1067
1112
|
|
|
1068
|
-
|
|
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);
|
|
1069
1116
|
}
|
|
1070
1117
|
|
|
1071
1118
|
fn calcDamage(
|
|
@@ -1074,6 +1121,7 @@ fn calcDamage(
|
|
|
1074
1121
|
target_player: Player,
|
|
1075
1122
|
m: ?Move.Data,
|
|
1076
1123
|
crit: bool,
|
|
1124
|
+
options: anytype,
|
|
1077
1125
|
) bool {
|
|
1078
1126
|
// Confusion (indicated when m == null) just needs a 40 BP physical move
|
|
1079
1127
|
const cfz = m == null;
|
|
@@ -1085,28 +1133,8 @@ fn calcDamage(
|
|
|
1085
1133
|
|
|
1086
1134
|
const special = move.type.special();
|
|
1087
1135
|
|
|
1088
|
-
|
|
1089
|
-
var
|
|
1090
|
-
if (crit)
|
|
1091
|
-
if (special) side.stored().stats.spc
|
|
1092
|
-
else side.stored().stats.atk
|
|
1093
|
-
else
|
|
1094
|
-
if (special) side.active.stats.spc
|
|
1095
|
-
else side.active.stats.atk;
|
|
1096
|
-
var def: u32 =
|
|
1097
|
-
if (crit)
|
|
1098
|
-
if (special) target.stored().stats.spc
|
|
1099
|
-
else target.stored().stats.def
|
|
1100
|
-
else
|
|
1101
|
-
// GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
|
|
1102
|
-
if (special)
|
|
1103
|
-
target.active.stats.spc *
|
|
1104
|
-
@as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
|
|
1105
|
-
// Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
|
|
1106
|
-
else
|
|
1107
|
-
target.active.stats.def *
|
|
1108
|
-
@as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
|
|
1109
|
-
// zig fmt: on
|
|
1136
|
+
var atk = attack(side, crit, special);
|
|
1137
|
+
var def = defense(target, crit, special, cfz);
|
|
1110
1138
|
|
|
1111
1139
|
// Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
|
|
1112
1140
|
// will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
|
|
@@ -1114,12 +1142,12 @@ fn calcDamage(
|
|
|
1114
1142
|
if ((!showdown or !cfz) and (atk > 255 or def > 255)) {
|
|
1115
1143
|
atk = @max((atk / 4) & 255, 1);
|
|
1116
1144
|
// GLITCH: not adjusted to be a min of 1 on cartridge (can lead to division-by-zero freeze)
|
|
1117
|
-
def = @max((def / 4) & 255, if (
|
|
1145
|
+
def = @max((def / 4) & 255, if (pkmn.options.mod) 1 else 0);
|
|
1118
1146
|
}
|
|
1119
1147
|
|
|
1120
|
-
const lvl =
|
|
1148
|
+
const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
|
|
1121
1149
|
|
|
1122
|
-
def =
|
|
1150
|
+
def = if (move.effect == .Explode) @max(def / 2, 1) else def;
|
|
1123
1151
|
|
|
1124
1152
|
if (def == 0) return false;
|
|
1125
1153
|
|
|
@@ -1131,11 +1159,41 @@ fn calcDamage(
|
|
|
1131
1159
|
d = @min(997, d);
|
|
1132
1160
|
d += 2;
|
|
1133
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
|
+
|
|
1134
1172
|
battle.last_damage = @intCast(d);
|
|
1135
1173
|
|
|
1136
1174
|
return true;
|
|
1137
1175
|
}
|
|
1138
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
|
+
|
|
1139
1197
|
fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
1140
1198
|
const side = battle.side(player);
|
|
1141
1199
|
const foe = battle.foe(player);
|
|
@@ -1180,7 +1238,6 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1180
1238
|
const foe = battle.foe(player);
|
|
1181
1239
|
|
|
1182
1240
|
if (!try checkHit(battle, player, move, options)) return null;
|
|
1183
|
-
try options.chance.commit(player, .hit);
|
|
1184
1241
|
|
|
1185
1242
|
battle.last_damage = switch (side.last_selected_move) {
|
|
1186
1243
|
.SuperFang => @max(foe.stored().hp / 2, 1),
|
|
@@ -1191,13 +1248,13 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1191
1248
|
.Psywave => power: {
|
|
1192
1249
|
const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
|
|
1193
1250
|
// GLITCH: Psywave infinite glitch loop
|
|
1194
|
-
if (!
|
|
1251
|
+
if (!pkmn.options.mod and max <= 1) return .Error;
|
|
1195
1252
|
break :power try Rolls.psywave(battle, player, max, options);
|
|
1196
1253
|
},
|
|
1197
1254
|
else => unreachable,
|
|
1198
1255
|
};
|
|
1199
1256
|
|
|
1200
|
-
if (battle.last_damage == 0) return if (
|
|
1257
|
+
if (battle.last_damage == 0) return if (pkmn.options.mod) null else .Error;
|
|
1201
1258
|
|
|
1202
1259
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1203
1260
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
@@ -1217,15 +1274,15 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1217
1274
|
// Pretend Splash was used as a stand-in when no move has been used to fail below with 0 BP
|
|
1218
1275
|
const foe_last_selected_move =
|
|
1219
1276
|
Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1277
|
+
.Splash
|
|
1278
|
+
else
|
|
1279
|
+
foe.last_selected_move);
|
|
1223
1280
|
|
|
1224
1281
|
const used = battle.lastMove(player.foe()).counterable != 0;
|
|
1225
1282
|
const selected = foe_last_selected_move.bp > 0 and
|
|
1226
1283
|
foe.last_selected_move != .Counter and
|
|
1227
1284
|
(foe_last_selected_move.type == .Normal or
|
|
1228
|
-
|
|
1285
|
+
foe_last_selected_move.type == .Fighting);
|
|
1229
1286
|
|
|
1230
1287
|
if (!used and !selected) {
|
|
1231
1288
|
try options.log.fail(.{ battle.active(player), .None });
|
|
@@ -1234,16 +1291,19 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1234
1291
|
|
|
1235
1292
|
if (!used or !selected) {
|
|
1236
1293
|
// GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
|
|
1237
|
-
if (!
|
|
1294
|
+
if (!pkmn.options.mod) return .Error;
|
|
1238
1295
|
try options.log.fail(.{ battle.active(player), .None });
|
|
1239
1296
|
return null;
|
|
1240
1297
|
}
|
|
1241
1298
|
|
|
1242
|
-
|
|
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;
|
|
1243
1304
|
|
|
1244
|
-
// Pokémon Showdown calls
|
|
1305
|
+
// Pokémon Showdown calls moveHit before Counter
|
|
1245
1306
|
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
1246
|
-
try options.chance.commit(player, .hit);
|
|
1247
1307
|
|
|
1248
1308
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1249
1309
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
@@ -1272,7 +1332,7 @@ fn applyDamage(
|
|
|
1272
1332
|
subbed.active.volatiles.Substitute = false;
|
|
1273
1333
|
// battle.last_damage is not updated with the amount of HP the Substitute had
|
|
1274
1334
|
try options.log.end(.{ battle.active(sub_player), .Substitute });
|
|
1275
|
-
options.calc.capped(
|
|
1335
|
+
if (reason != .Confusion) options.calc.capped(sub_player);
|
|
1276
1336
|
return true;
|
|
1277
1337
|
} else {
|
|
1278
1338
|
// Safe to truncate since less than subbed.volatiles.substitute which is a u8
|
|
@@ -1306,7 +1366,7 @@ fn mirrorMove(
|
|
|
1306
1366
|
try options.log.fail(.{ battle.active(player), .None });
|
|
1307
1367
|
return null;
|
|
1308
1368
|
} else if (!showdown or foe.last_used_move != .Struggle) {
|
|
1309
|
-
incrementPP(side, mslot);
|
|
1369
|
+
incrementPP(side, player, mslot, options);
|
|
1310
1370
|
}
|
|
1311
1371
|
|
|
1312
1372
|
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
|
|
@@ -1327,7 +1387,7 @@ fn metronome(
|
|
|
1327
1387
|
var side = battle.side(player);
|
|
1328
1388
|
|
|
1329
1389
|
side.last_selected_move = try Rolls.metronome(battle, player, options);
|
|
1330
|
-
incrementPP(side, mslot);
|
|
1390
|
+
incrementPP(side, player, mslot, options);
|
|
1331
1391
|
|
|
1332
1392
|
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
|
|
1333
1393
|
return null;
|
|
@@ -1339,7 +1399,10 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
|
|
|
1339
1399
|
var immune = false;
|
|
1340
1400
|
var mist = false;
|
|
1341
1401
|
|
|
1342
|
-
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
|
+
}
|
|
1343
1406
|
|
|
1344
1407
|
assert(!immune);
|
|
1345
1408
|
if (mist) {
|
|
@@ -1348,11 +1411,11 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
|
|
|
1348
1411
|
try options.log.activate(.{ foe_ident, .Mist });
|
|
1349
1412
|
try options.log.fail(.{ foe_ident, .None });
|
|
1350
1413
|
} else {
|
|
1351
|
-
try options.chance.commit(player, .miss);
|
|
1352
1414
|
try options.log.lastmiss(.{});
|
|
1353
1415
|
try options.log.miss(.{battle.active(player)});
|
|
1354
1416
|
}
|
|
1355
1417
|
|
|
1418
|
+
if (!showdown) try options.chance.commit(player, .miss);
|
|
1356
1419
|
return false;
|
|
1357
1420
|
}
|
|
1358
1421
|
|
|
@@ -1363,7 +1426,7 @@ fn moveHit(
|
|
|
1363
1426
|
immune: *bool,
|
|
1364
1427
|
mist: *bool,
|
|
1365
1428
|
options: anytype,
|
|
1366
|
-
) bool {
|
|
1429
|
+
) !bool {
|
|
1367
1430
|
var side = battle.side(player);
|
|
1368
1431
|
const foe = battle.foe(player);
|
|
1369
1432
|
|
|
@@ -1383,7 +1446,16 @@ fn moveHit(
|
|
|
1383
1446
|
break :miss true;
|
|
1384
1447
|
}
|
|
1385
1448
|
if (!showdown) {
|
|
1386
|
-
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
|
+
}
|
|
1387
1459
|
if (foe.active.volatiles.Invulnerable) break :miss true;
|
|
1388
1460
|
}
|
|
1389
1461
|
|
|
@@ -1402,7 +1474,7 @@ fn moveHit(
|
|
|
1402
1474
|
const overwritten = overwrite and state > 0;
|
|
1403
1475
|
assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
|
|
1404
1476
|
|
|
1405
|
-
var accuracy = if (overwritten) state else
|
|
1477
|
+
var accuracy: u16 = if (overwritten) state else move.accuracy;
|
|
1406
1478
|
var boost = BOOSTS[@as(u4, @intCast(@as(i8, side.active.boosts.accuracy) + 6))];
|
|
1407
1479
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1408
1480
|
boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
|
|
@@ -1414,7 +1486,7 @@ fn moveHit(
|
|
|
1414
1486
|
if (overwrite) side.active.volatiles.state = accuracy;
|
|
1415
1487
|
|
|
1416
1488
|
// GLITCH: max accuracy is 255 so 1/256 chance of miss
|
|
1417
|
-
break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
|
|
1489
|
+
break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
|
|
1418
1490
|
};
|
|
1419
1491
|
|
|
1420
1492
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1425,13 +1497,17 @@ fn moveHit(
|
|
|
1425
1497
|
|
|
1426
1498
|
if (!miss) return true;
|
|
1427
1499
|
if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
|
|
1500
|
+
|
|
1428
1501
|
side.active.volatiles.Binding = false;
|
|
1502
|
+
options.chance.observe(.binding, player, .None);
|
|
1503
|
+
|
|
1429
1504
|
return false;
|
|
1430
1505
|
}
|
|
1431
1506
|
|
|
1432
1507
|
fn checkFaint(
|
|
1433
1508
|
battle: anytype,
|
|
1434
1509
|
player: Player,
|
|
1510
|
+
cap: bool,
|
|
1435
1511
|
options: anytype,
|
|
1436
1512
|
) @TypeOf(options.log).Error!?Result {
|
|
1437
1513
|
const side = battle.side(player);
|
|
@@ -1444,21 +1520,21 @@ fn checkFaint(
|
|
|
1444
1520
|
const foe_out = foe_fainted and findFirstAlive(foe) == 0;
|
|
1445
1521
|
const more = player_out or foe_out;
|
|
1446
1522
|
|
|
1447
|
-
if (try faint(battle, player, !(more or foe_fainted), options)) |r| return r;
|
|
1448
|
-
if (foe_fainted) if (try faint(battle, player.foe(), !more, options)) |r| return r;
|
|
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;
|
|
1449
1525
|
|
|
1450
1526
|
assert(!side.active.volatiles.MultiHit);
|
|
1451
1527
|
assert(!foe.active.volatiles.MultiHit);
|
|
1452
1528
|
|
|
1453
1529
|
if (player_out and foe_out) {
|
|
1454
1530
|
try options.log.tie(.{});
|
|
1455
|
-
return
|
|
1531
|
+
return .Tie;
|
|
1456
1532
|
} else if (player_out) {
|
|
1457
1533
|
try options.log.win(.{player.foe()});
|
|
1458
|
-
return if (player == .P1)
|
|
1534
|
+
return if (player == .P1) .Lose else .Win;
|
|
1459
1535
|
} else if (foe_out) {
|
|
1460
1536
|
try options.log.win(.{player});
|
|
1461
|
-
return if (player == .P1)
|
|
1537
|
+
return if (player == .P1) .Win else .Lose;
|
|
1462
1538
|
}
|
|
1463
1539
|
|
|
1464
1540
|
const foe_choice: Choice.Type = if (foe_fainted) .Switch else .Pass;
|
|
@@ -1466,7 +1542,7 @@ fn checkFaint(
|
|
|
1466
1542
|
return .{ .p1 = foe_choice, .p2 = .Switch };
|
|
1467
1543
|
}
|
|
1468
1544
|
|
|
1469
|
-
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 {
|
|
1470
1546
|
var side = battle.side(player);
|
|
1471
1547
|
var foe = battle.foe(player);
|
|
1472
1548
|
assert(side.stored().hp == 0);
|
|
@@ -1475,8 +1551,8 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
|
|
|
1475
1551
|
assert(!foe_volatiles.MultiHit);
|
|
1476
1552
|
if (foe_volatiles.Bide) {
|
|
1477
1553
|
assert(!foe_volatiles.Thrashing and !foe_volatiles.Rage);
|
|
1478
|
-
foe_volatiles.state = if (
|
|
1479
|
-
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;
|
|
1480
1556
|
}
|
|
1481
1557
|
|
|
1482
1558
|
// Clearing these is not strictly necessary as provided the battle hasn't ended the side that
|
|
@@ -1500,7 +1576,9 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
|
|
|
1500
1576
|
}
|
|
1501
1577
|
|
|
1502
1578
|
try options.log.faint(.{ battle.active(player), done });
|
|
1503
|
-
|
|
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);
|
|
1504
1582
|
return null;
|
|
1505
1583
|
}
|
|
1506
1584
|
|
|
@@ -1564,21 +1642,21 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
|
|
|
1564
1642
|
|
|
1565
1643
|
battle.turn += 1;
|
|
1566
1644
|
|
|
1567
|
-
if (
|
|
1645
|
+
if (pkmn.options.mod and pkmn.options.ebc and checkEBC(battle)) {
|
|
1568
1646
|
try options.log.tie(.{});
|
|
1569
|
-
return
|
|
1647
|
+
return .Tie;
|
|
1570
1648
|
}
|
|
1571
1649
|
|
|
1572
|
-
if (
|
|
1650
|
+
if (pkmn.options.mod and battle.turn >= 1000) {
|
|
1573
1651
|
try options.log.tie(.{});
|
|
1574
|
-
return
|
|
1652
|
+
return .Tie;
|
|
1575
1653
|
} else if (battle.turn >= 65535) {
|
|
1576
|
-
return
|
|
1654
|
+
return .Error;
|
|
1577
1655
|
}
|
|
1578
1656
|
|
|
1579
1657
|
try options.log.turn(.{battle.turn});
|
|
1580
1658
|
|
|
1581
|
-
return
|
|
1659
|
+
return .Default;
|
|
1582
1660
|
}
|
|
1583
1661
|
|
|
1584
1662
|
fn checkEBC(battle: anytype) bool {
|
|
@@ -1660,17 +1738,6 @@ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
|
|
|
1660
1738
|
}
|
|
1661
1739
|
}
|
|
1662
1740
|
|
|
1663
|
-
fn handleThrashing(battle: anytype, active: *ActivePokemon, player: Player, options: anytype) bool {
|
|
1664
|
-
var volatiles = &active.volatiles;
|
|
1665
|
-
assert(volatiles.Thrashing);
|
|
1666
|
-
if (volatiles.attacks > 0) return false;
|
|
1667
|
-
|
|
1668
|
-
volatiles.Thrashing = false;
|
|
1669
|
-
volatiles.Confusion = true;
|
|
1670
|
-
volatiles.confusion = Rolls.confusionDuration(battle, player, true, options);
|
|
1671
|
-
return true;
|
|
1672
|
-
}
|
|
1673
|
-
|
|
1674
1741
|
fn onBegin(
|
|
1675
1742
|
battle: anytype,
|
|
1676
1743
|
player: Player,
|
|
@@ -1679,7 +1746,6 @@ fn onBegin(
|
|
|
1679
1746
|
residual: *bool,
|
|
1680
1747
|
options: anytype,
|
|
1681
1748
|
) !void {
|
|
1682
|
-
assert(move.effect.onBegin());
|
|
1683
1749
|
return switch (move.effect) {
|
|
1684
1750
|
.Confusion => Effects.confusion(battle, player, move, options),
|
|
1685
1751
|
.Conversion => Effects.conversion(battle, player, options),
|
|
@@ -1702,7 +1768,6 @@ fn onBegin(
|
|
|
1702
1768
|
}
|
|
1703
1769
|
|
|
1704
1770
|
fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1705
|
-
assert(move.effect.onEnd());
|
|
1706
1771
|
return switch (move.effect) {
|
|
1707
1772
|
.Bide => Effects.bide(battle, player, options),
|
|
1708
1773
|
// zig fmt: off
|
|
@@ -1724,7 +1789,6 @@ fn alwaysHappens(
|
|
|
1724
1789
|
residual: *bool,
|
|
1725
1790
|
options: anytype,
|
|
1726
1791
|
) !void {
|
|
1727
|
-
assert(move.effect.alwaysHappens());
|
|
1728
1792
|
return switch (move.effect) {
|
|
1729
1793
|
.DrainHP, .DreamEater => Effects.drainHP(battle, player, options),
|
|
1730
1794
|
.Explode => Effects.explode(battle, player),
|
|
@@ -1760,8 +1824,10 @@ pub const Effects = struct {
|
|
|
1760
1824
|
|
|
1761
1825
|
side.active.volatiles.Bide = true;
|
|
1762
1826
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1827
|
+
|
|
1763
1828
|
side.active.volatiles.state = 0;
|
|
1764
1829
|
side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
1830
|
+
options.chance.observe(.attacking, player, .started);
|
|
1765
1831
|
|
|
1766
1832
|
try options.log.start(.{ battle.active(player), .Bide });
|
|
1767
1833
|
}
|
|
@@ -1823,12 +1889,14 @@ pub const Effects = struct {
|
|
|
1823
1889
|
if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
1824
1890
|
if (!try checkHit(battle, player, move, options)) return;
|
|
1825
1891
|
}
|
|
1826
|
-
try options.chance.commit(player, .hit);
|
|
1827
1892
|
}
|
|
1828
1893
|
|
|
1829
1894
|
if (foe.active.volatiles.Confusion) return;
|
|
1830
1895
|
foe.active.volatiles.Confusion = true;
|
|
1831
|
-
|
|
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());
|
|
1832
1900
|
|
|
1833
1901
|
try options.log.start(.{ battle.active(player.foe()), .Confusion });
|
|
1834
1902
|
}
|
|
@@ -1856,10 +1924,7 @@ pub const Effects = struct {
|
|
|
1856
1924
|
const foe_ident = battle.active(player.foe());
|
|
1857
1925
|
|
|
1858
1926
|
// Pokémon Showdown handles hit/miss earlier in doMove
|
|
1859
|
-
if (!showdown)
|
|
1860
|
-
if (!try checkHit(battle, player, move, options)) return null;
|
|
1861
|
-
try options.chance.commit(player, .hit);
|
|
1862
|
-
}
|
|
1927
|
+
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
1863
1928
|
|
|
1864
1929
|
if (volatiles.disable_move != 0) {
|
|
1865
1930
|
try options.log.fail(.{ foe_ident, .None });
|
|
@@ -1877,18 +1942,18 @@ pub const Effects = struct {
|
|
|
1877
1942
|
|
|
1878
1943
|
// Technically this is still considered simply a "miss" on the cartridge,
|
|
1879
1944
|
// but diverging from Pokémon Showdown here would mostly just be pedantic
|
|
1880
|
-
if (n == 0) {
|
|
1945
|
+
if (n == 0 or (pkmn.options.mod and err)) {
|
|
1881
1946
|
try options.log.fail(.{ foe_ident, .None });
|
|
1882
1947
|
return null;
|
|
1883
1948
|
} else if (err) {
|
|
1884
1949
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
1885
|
-
|
|
1886
|
-
return Result.Error;
|
|
1950
|
+
return .Error;
|
|
1887
1951
|
}
|
|
1888
1952
|
|
|
1889
1953
|
volatiles.disable_move =
|
|
1890
1954
|
@intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
|
|
1891
1955
|
volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
|
|
1956
|
+
options.chance.observe(.disable, player.foe(), .started);
|
|
1892
1957
|
|
|
1893
1958
|
const id = foe.active.move(volatiles.disable_move).id;
|
|
1894
1959
|
try options.log.start(.{ foe_ident, .Disable, id });
|
|
@@ -1959,7 +2024,7 @@ pub const Effects = struct {
|
|
|
1959
2024
|
if (foe.active.types.includes(move.type)) return;
|
|
1960
2025
|
if (!try Rolls.secondaryChance(battle, player, true, options)) return;
|
|
1961
2026
|
// Freeze Clause Mod
|
|
1962
|
-
if (
|
|
2027
|
+
if (pkmn.options.mod) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
1963
2028
|
|
|
1964
2029
|
foe_stored.status = Status.init(.FRZ);
|
|
1965
2030
|
// GLITCH: Hyper Beam recharging status is not cleared
|
|
@@ -1972,7 +2037,7 @@ pub const Effects = struct {
|
|
|
1972
2037
|
var side = battle.side(player);
|
|
1973
2038
|
var foe = battle.foe(player);
|
|
1974
2039
|
|
|
1975
|
-
|
|
2040
|
+
const side_stored = side.stored();
|
|
1976
2041
|
var foe_stored = foe.stored();
|
|
1977
2042
|
|
|
1978
2043
|
const player_ident = battle.active(player);
|
|
@@ -1995,22 +2060,24 @@ pub const Effects = struct {
|
|
|
1995
2060
|
if (p != player and Status.any(s.stored().status)) {
|
|
1996
2061
|
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
1997
2062
|
s.stored().status = 0;
|
|
2063
|
+
options.chance.observe(.sleep, p, .None);
|
|
1998
2064
|
} else if (showdown and s.stored().status == Status.TOX) {
|
|
1999
2065
|
s.stored().status = Status.init(.PSN);
|
|
2000
2066
|
try log.status(.{ battle.active(p), s.stored().status, .None });
|
|
2001
2067
|
}
|
|
2002
|
-
try clearVolatiles(battle, p,
|
|
2068
|
+
try clearVolatiles(battle, p, options);
|
|
2003
2069
|
}
|
|
2004
2070
|
} else {
|
|
2005
2071
|
if (Status.any(foe_stored.status)) {
|
|
2006
2072
|
if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
|
|
2007
2073
|
foe.last_selected_move = .SKIP_TURN;
|
|
2074
|
+
options.chance.observe(.sleep, player.foe(), .None);
|
|
2008
2075
|
}
|
|
2009
2076
|
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
2010
2077
|
foe_stored.status = 0;
|
|
2011
2078
|
}
|
|
2012
|
-
try clearVolatiles(battle, player,
|
|
2013
|
-
try clearVolatiles(battle, player.foe(),
|
|
2079
|
+
try clearVolatiles(battle, player, options);
|
|
2080
|
+
try clearVolatiles(battle, player.foe(), options);
|
|
2014
2081
|
}
|
|
2015
2082
|
}
|
|
2016
2083
|
|
|
@@ -2061,7 +2128,6 @@ pub const Effects = struct {
|
|
|
2061
2128
|
}
|
|
2062
2129
|
}
|
|
2063
2130
|
|
|
2064
|
-
try options.chance.commit(player, .hit);
|
|
2065
2131
|
foe.active.volatiles.LeechSeed = true;
|
|
2066
2132
|
|
|
2067
2133
|
try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
|
|
@@ -2102,12 +2168,10 @@ pub const Effects = struct {
|
|
|
2102
2168
|
if (!has_mimic) {
|
|
2103
2169
|
// Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
|
|
2104
2170
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2105
|
-
try options.chance.commit(player, .hit);
|
|
2106
2171
|
return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
2107
2172
|
}
|
|
2108
2173
|
}
|
|
2109
2174
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2110
|
-
try options.chance.commit(player, .hit);
|
|
2111
2175
|
|
|
2112
2176
|
const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
|
|
2113
2177
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
@@ -2161,7 +2225,6 @@ pub const Effects = struct {
|
|
|
2161
2225
|
if (immune) return log.immune(.{ foe_ident, .None });
|
|
2162
2226
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2163
2227
|
}
|
|
2164
|
-
try options.chance.commit(player, .hit);
|
|
2165
2228
|
|
|
2166
2229
|
foe_stored.status = Status.init(.PAR);
|
|
2167
2230
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
@@ -2229,7 +2292,6 @@ pub const Effects = struct {
|
|
|
2229
2292
|
|
|
2230
2293
|
if (move.effect == .Poison) {
|
|
2231
2294
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2232
|
-
try options.chance.commit(player, .hit);
|
|
2233
2295
|
} else {
|
|
2234
2296
|
if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
|
|
2235
2297
|
return;
|
|
@@ -2298,13 +2360,12 @@ pub const Effects = struct {
|
|
|
2298
2360
|
if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
|
|
2299
2361
|
});
|
|
2300
2362
|
}
|
|
2301
|
-
// If
|
|
2363
|
+
// If moveHit in doMove didn't return true Pokémon Showdown wouldn't be in here
|
|
2302
2364
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2303
|
-
try options.chance.commit(player, .hit);
|
|
2304
2365
|
}
|
|
2305
2366
|
|
|
2306
2367
|
// Sleep Clause Mod
|
|
2307
|
-
if (
|
|
2368
|
+
if (pkmn.options.mod) {
|
|
2308
2369
|
for (foe.pokemon) |p| {
|
|
2309
2370
|
if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
|
|
2310
2371
|
}
|
|
@@ -2312,6 +2373,8 @@ pub const Effects = struct {
|
|
|
2312
2373
|
foe.active.volatiles.Recharging = false;
|
|
2313
2374
|
|
|
2314
2375
|
foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
|
|
2376
|
+
options.chance.observe(.sleep, player.foe(), .started);
|
|
2377
|
+
|
|
2315
2378
|
const last = battle.side(player).last_selected_move;
|
|
2316
2379
|
try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
|
|
2317
2380
|
}
|
|
@@ -2353,9 +2416,7 @@ pub const Effects = struct {
|
|
|
2353
2416
|
// Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
|
|
2354
2417
|
if (showdown) {
|
|
2355
2418
|
if (battle.side(player).last_selected_move == .Teleport) return;
|
|
2356
|
-
|
|
2357
|
-
try options.chance.commit(player, .hit);
|
|
2358
|
-
}
|
|
2419
|
+
_ = try checkHit(battle, player, move, options);
|
|
2359
2420
|
battle.last_damage = 0;
|
|
2360
2421
|
} else {
|
|
2361
2422
|
try options.log.fail(.{ battle.active(player), .None });
|
|
@@ -2370,6 +2431,8 @@ pub const Effects = struct {
|
|
|
2370
2431
|
|
|
2371
2432
|
volatiles.Thrashing = true;
|
|
2372
2433
|
volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
2434
|
+
|
|
2435
|
+
options.chance.observe(.attacking, player, .started);
|
|
2373
2436
|
}
|
|
2374
2437
|
|
|
2375
2438
|
fn transform(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -2408,13 +2471,14 @@ pub const Effects = struct {
|
|
|
2408
2471
|
if (side.active.volatiles.Binding) return;
|
|
2409
2472
|
side.active.volatiles.Binding = true;
|
|
2410
2473
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2411
|
-
// (Pokémon Showdown
|
|
2474
|
+
// (Pokémon Showdown clears Recharging in onTryMove instead)
|
|
2412
2475
|
if (!showdown) {
|
|
2413
2476
|
foe.active.volatiles.Recharging = false;
|
|
2414
2477
|
} else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
|
|
2415
2478
|
|
|
2416
2479
|
side.active.volatiles.attacks =
|
|
2417
2480
|
try Rolls.distribution(battle, .Binding, player, options) - 1;
|
|
2481
|
+
options.chance.observe(.binding, player, .started);
|
|
2418
2482
|
}
|
|
2419
2483
|
|
|
2420
2484
|
fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2441,7 +2505,7 @@ pub const Effects = struct {
|
|
|
2441
2505
|
}
|
|
2442
2506
|
return log.fail(.{ ident, .None });
|
|
2443
2507
|
}
|
|
2444
|
-
|
|
2508
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
|
|
2445
2509
|
const stat = unmodifiedStats(battle, side).atk;
|
|
2446
2510
|
stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2447
2511
|
try log.boost(.{ ident, reason, n });
|
|
@@ -2461,7 +2525,7 @@ pub const Effects = struct {
|
|
|
2461
2525
|
}
|
|
2462
2526
|
return log.fail(.{ ident, .None });
|
|
2463
2527
|
}
|
|
2464
|
-
|
|
2528
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
|
|
2465
2529
|
const stat = unmodifiedStats(battle, side).def;
|
|
2466
2530
|
stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2467
2531
|
try log.boost(.{ ident, .Defense, n });
|
|
@@ -2478,7 +2542,7 @@ pub const Effects = struct {
|
|
|
2478
2542
|
}
|
|
2479
2543
|
return log.fail(.{ ident, .None });
|
|
2480
2544
|
}
|
|
2481
|
-
|
|
2545
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
|
|
2482
2546
|
const stat = unmodifiedStats(battle, side).spe;
|
|
2483
2547
|
stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2484
2548
|
try log.boost(.{ ident, .Speed, 2 });
|
|
@@ -2498,7 +2562,7 @@ pub const Effects = struct {
|
|
|
2498
2562
|
}
|
|
2499
2563
|
return log.fail(.{ ident, .None });
|
|
2500
2564
|
}
|
|
2501
|
-
|
|
2565
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
|
|
2502
2566
|
const stat = unmodifiedStats(battle, side).spc;
|
|
2503
2567
|
stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2504
2568
|
try log.boost(.{ ident, .SpecialAttack, n });
|
|
@@ -2531,7 +2595,6 @@ pub const Effects = struct {
|
|
|
2531
2595
|
} else {
|
|
2532
2596
|
// checkHit already checks for Invulnerable
|
|
2533
2597
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2534
|
-
try options.chance.commit(player, .hit);
|
|
2535
2598
|
}
|
|
2536
2599
|
|
|
2537
2600
|
var stats = &foe.active.stats;
|
|
@@ -2551,7 +2614,7 @@ pub const Effects = struct {
|
|
|
2551
2614
|
}
|
|
2552
2615
|
return log.fail(.{ foe_ident, .None });
|
|
2553
2616
|
}
|
|
2554
|
-
|
|
2617
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
|
|
2555
2618
|
const stat = unmodifiedStats(battle, foe).atk;
|
|
2556
2619
|
stats.atk = @max(1, stat * mod[0] / mod[1]);
|
|
2557
2620
|
try log.boost(.{ foe_ident, .Attack, -1 });
|
|
@@ -2569,7 +2632,7 @@ pub const Effects = struct {
|
|
|
2569
2632
|
}
|
|
2570
2633
|
return log.fail(.{ foe_ident, .None });
|
|
2571
2634
|
}
|
|
2572
|
-
|
|
2635
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
|
|
2573
2636
|
const stat = unmodifiedStats(battle, foe).def;
|
|
2574
2637
|
stats.def = @max(1, stat * mod[0] / mod[1]);
|
|
2575
2638
|
try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
|
|
@@ -2586,7 +2649,7 @@ pub const Effects = struct {
|
|
|
2586
2649
|
}
|
|
2587
2650
|
return log.fail(.{ foe_ident, .None });
|
|
2588
2651
|
}
|
|
2589
|
-
|
|
2652
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
|
|
2590
2653
|
const stat = unmodifiedStats(battle, foe).spe;
|
|
2591
2654
|
stats.spe = @max(1, stat * mod[0] / mod[1]);
|
|
2592
2655
|
try log.boost(.{ foe_ident, .Speed, -1 });
|
|
@@ -2606,7 +2669,7 @@ pub const Effects = struct {
|
|
|
2606
2669
|
}
|
|
2607
2670
|
return log.fail(.{ foe_ident, .None });
|
|
2608
2671
|
}
|
|
2609
|
-
|
|
2672
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
|
|
2610
2673
|
const stat = unmodifiedStats(battle, foe).spc;
|
|
2611
2674
|
stats.spc = @max(1, stat * mod[0] / mod[1]);
|
|
2612
2675
|
try log.boost(.{ foe_ident, .SpecialAttack, -1 });
|
|
@@ -2645,22 +2708,22 @@ fn isForced(active: anytype) bool {
|
|
|
2645
2708
|
active.volatiles.Thrashing or active.volatiles.Charging;
|
|
2646
2709
|
}
|
|
2647
2710
|
|
|
2648
|
-
fn clearVolatiles(battle: anytype, who: Player,
|
|
2711
|
+
fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
|
|
2649
2712
|
var log = options.log;
|
|
2650
2713
|
var side = battle.side(who);
|
|
2651
2714
|
var volatiles = &side.active.volatiles;
|
|
2652
2715
|
const ident = battle.active(who);
|
|
2653
2716
|
|
|
2654
|
-
options.chance.clearDurations(who, clear);
|
|
2655
|
-
|
|
2656
2717
|
if (volatiles.disable_move != 0) {
|
|
2657
2718
|
volatiles.disable_move = 0;
|
|
2658
2719
|
volatiles.disable_duration = 0;
|
|
2720
|
+
options.chance.observe(.disable, who, .None);
|
|
2659
2721
|
try log.end(.{ ident, .DisableSilent });
|
|
2660
2722
|
}
|
|
2661
2723
|
if (volatiles.Confusion) {
|
|
2662
2724
|
// volatiles.confusion is left unchanged
|
|
2663
2725
|
volatiles.Confusion = false;
|
|
2726
|
+
options.chance.observe(.confusion, who, .None);
|
|
2664
2727
|
try log.end(.{ ident, .ConfusionSilent });
|
|
2665
2728
|
}
|
|
2666
2729
|
if (volatiles.Mist) {
|
|
@@ -2695,10 +2758,25 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
|
|
|
2695
2758
|
}
|
|
2696
2759
|
}
|
|
2697
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
|
+
|
|
2698
2774
|
pub const Rolls = struct {
|
|
2699
2775
|
fn speedTie(battle: anytype, options: anytype) !bool {
|
|
2700
2776
|
const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
|
|
2701
2777
|
player == .P1
|
|
2778
|
+
else if (@hasDecl(@TypeOf(battle.rng), "speedTie"))
|
|
2779
|
+
battle.rng.speedTie()
|
|
2702
2780
|
else if (showdown)
|
|
2703
2781
|
battle.rng.range(u8, 0, 2) == 0
|
|
2704
2782
|
else
|
|
@@ -2708,21 +2786,31 @@ pub const Rolls = struct {
|
|
|
2708
2786
|
return p1;
|
|
2709
2787
|
}
|
|
2710
2788
|
|
|
2711
|
-
fn criticalHit(
|
|
2789
|
+
fn criticalHit(
|
|
2790
|
+
battle: anytype,
|
|
2791
|
+
player: Player,
|
|
2792
|
+
rate: u8,
|
|
2793
|
+
spurious: bool,
|
|
2794
|
+
options: anytype,
|
|
2795
|
+
) !bool {
|
|
2712
2796
|
const crit = if (options.calc.overridden(player, .critical_hit)) |val|
|
|
2713
2797
|
val == .true
|
|
2798
|
+
else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
|
|
2799
|
+
battle.rng.criticalHit(player, rate)
|
|
2714
2800
|
else if (showdown)
|
|
2715
2801
|
battle.rng.chance(u8, rate, 256)
|
|
2716
2802
|
else
|
|
2717
2803
|
std.math.rotl(u8, battle.rng.next(), 3) < rate;
|
|
2718
2804
|
|
|
2719
|
-
try options.chance.criticalHit(player, crit, rate);
|
|
2805
|
+
if (!spurious) try options.chance.criticalHit(player, crit, rate);
|
|
2720
2806
|
return crit;
|
|
2721
2807
|
}
|
|
2722
2808
|
|
|
2723
2809
|
fn damage(battle: anytype, player: Player, options: anytype) !u8 {
|
|
2724
2810
|
const roll = if (options.calc.overridden(player, .damage)) |val|
|
|
2725
2811
|
val
|
|
2812
|
+
else if (@hasDecl(@TypeOf(battle.rng), "damage"))
|
|
2813
|
+
battle.rng.damage(player)
|
|
2726
2814
|
else roll: {
|
|
2727
2815
|
if (showdown) break :roll battle.rng.range(u8, 217, 256);
|
|
2728
2816
|
while (true) {
|
|
@@ -2736,21 +2824,27 @@ pub const Rolls = struct {
|
|
|
2736
2824
|
return roll;
|
|
2737
2825
|
}
|
|
2738
2826
|
|
|
2739
|
-
|
|
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;
|
|
2740
2830
|
const ok = if (options.calc.overridden(player, .hit)) |val|
|
|
2741
2831
|
val == .true
|
|
2832
|
+
else if (@hasDecl(@TypeOf(battle.rng), "hit"))
|
|
2833
|
+
battle.rng.hit(player, acc)
|
|
2742
2834
|
else if (showdown)
|
|
2743
|
-
battle.rng.chance(
|
|
2835
|
+
battle.rng.chance(uN, acc, 256)
|
|
2744
2836
|
else
|
|
2745
|
-
battle.rng.next() <
|
|
2837
|
+
battle.rng.next() < acc;
|
|
2746
2838
|
|
|
2747
|
-
options.chance.hit(ok,
|
|
2839
|
+
try options.chance.hit(player, ok, acc);
|
|
2748
2840
|
return ok;
|
|
2749
2841
|
}
|
|
2750
2842
|
|
|
2751
2843
|
fn confused(battle: anytype, player: Player, options: anytype) !bool {
|
|
2752
2844
|
const cfz = if (options.calc.overridden(player, .confused)) |val|
|
|
2753
2845
|
val == .true
|
|
2846
|
+
else if (@hasDecl(@TypeOf(battle.rng), "confused"))
|
|
2847
|
+
battle.rng.confused(player)
|
|
2754
2848
|
else if (showdown)
|
|
2755
2849
|
!battle.rng.chance(u8, 128, 256)
|
|
2756
2850
|
else
|
|
@@ -2763,6 +2857,8 @@ pub const Rolls = struct {
|
|
|
2763
2857
|
fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
|
|
2764
2858
|
const par = if (options.calc.overridden(player, .paralyzed)) |val|
|
|
2765
2859
|
val == .true
|
|
2860
|
+
else if (@hasDecl(@TypeOf(battle.rng), "paralyzed"))
|
|
2861
|
+
battle.rng.paralyzed(player)
|
|
2766
2862
|
else if (showdown)
|
|
2767
2863
|
battle.rng.chance(u8, 63, 256)
|
|
2768
2864
|
else
|
|
@@ -2775,6 +2871,8 @@ pub const Rolls = struct {
|
|
|
2775
2871
|
fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
|
|
2776
2872
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2777
2873
|
val == .true
|
|
2874
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2875
|
+
battle.rng.secondaryChance(player, 25)
|
|
2778
2876
|
else if (showdown)
|
|
2779
2877
|
battle.rng.chance(u8, 25, 256)
|
|
2780
2878
|
else
|
|
@@ -2789,6 +2887,8 @@ pub const Rolls = struct {
|
|
|
2789
2887
|
|
|
2790
2888
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2791
2889
|
val == .true
|
|
2890
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2891
|
+
battle.rng.secondaryChance(player, rate)
|
|
2792
2892
|
else if (showdown)
|
|
2793
2893
|
battle.rng.chance(u8, rate, 256)
|
|
2794
2894
|
else
|
|
@@ -2803,6 +2903,8 @@ pub const Rolls = struct {
|
|
|
2803
2903
|
|
|
2804
2904
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2805
2905
|
val == .true
|
|
2906
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2907
|
+
battle.rng.secondaryChance(player, rate)
|
|
2806
2908
|
else if (showdown)
|
|
2807
2909
|
battle.rng.chance(u8, rate, 256)
|
|
2808
2910
|
else
|
|
@@ -2815,6 +2917,8 @@ pub const Rolls = struct {
|
|
|
2815
2917
|
fn unboost(battle: anytype, player: Player, options: anytype) !bool {
|
|
2816
2918
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2817
2919
|
val == .true
|
|
2920
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2921
|
+
battle.rng.secondaryChance(player, 85)
|
|
2818
2922
|
else if (showdown)
|
|
2819
2923
|
battle.rng.chance(u8, 85, 256)
|
|
2820
2924
|
else
|
|
@@ -2827,6 +2931,8 @@ pub const Rolls = struct {
|
|
|
2827
2931
|
fn metronome(battle: anytype, player: Player, options: anytype) !Move {
|
|
2828
2932
|
const move: Move = if (options.calc.overridden(player, .metronome)) |val|
|
|
2829
2933
|
val
|
|
2934
|
+
else if (@hasDecl(@TypeOf(battle.rng), "metronome"))
|
|
2935
|
+
battle.rng.metronome(player)
|
|
2830
2936
|
else if (showdown)
|
|
2831
2937
|
Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
|
|
2832
2938
|
else move: {
|
|
@@ -2845,6 +2951,8 @@ pub const Rolls = struct {
|
|
|
2845
2951
|
fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
|
|
2846
2952
|
const power = if (options.calc.overridden(player, .psywave)) |val|
|
|
2847
2953
|
val - 1
|
|
2954
|
+
else if (@hasDecl(@TypeOf(battle.rng), "psywave"))
|
|
2955
|
+
battle.rng.psywave(player)
|
|
2848
2956
|
else if (showdown)
|
|
2849
2957
|
battle.rng.range(u8, 0, max)
|
|
2850
2958
|
else power: {
|
|
@@ -2860,8 +2968,11 @@ pub const Rolls = struct {
|
|
|
2860
2968
|
}
|
|
2861
2969
|
|
|
2862
2970
|
fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2863
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2864
|
-
@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)
|
|
2865
2976
|
else if (showdown)
|
|
2866
2977
|
battle.rng.range(u3, 1, 8)
|
|
2867
2978
|
else duration: {
|
|
@@ -2872,46 +2983,55 @@ pub const Rolls = struct {
|
|
|
2872
2983
|
};
|
|
2873
2984
|
|
|
2874
2985
|
assert(duration >= 1 and duration <= 7);
|
|
2875
|
-
options.chance.duration(
|
|
2986
|
+
options.chance.duration(player, duration);
|
|
2876
2987
|
return duration;
|
|
2877
2988
|
}
|
|
2878
2989
|
|
|
2879
2990
|
fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
|
|
2880
|
-
const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
|
|
2881
|
-
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)
|
|
2882
2996
|
else if (showdown)
|
|
2883
2997
|
battle.rng.range(u4, 1, 9)
|
|
2884
2998
|
else
|
|
2885
2999
|
@intCast((battle.rng.next() & 7) + 1);
|
|
2886
3000
|
|
|
2887
3001
|
assert(duration >= 1 and duration <= 8);
|
|
2888
|
-
options.chance.duration(
|
|
3002
|
+
options.chance.duration(player, duration);
|
|
2889
3003
|
return duration;
|
|
2890
3004
|
}
|
|
2891
3005
|
|
|
2892
|
-
fn confusionDuration(battle: anytype, player: Player,
|
|
2893
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2894
|
-
@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)
|
|
2895
3012
|
else if (showdown)
|
|
2896
3013
|
battle.rng.range(u3, 2, 6)
|
|
2897
3014
|
else
|
|
2898
3015
|
@intCast((battle.rng.next() & 3) + 2);
|
|
2899
3016
|
|
|
2900
3017
|
assert(duration >= 2 and duration <= 5);
|
|
2901
|
-
options.chance.duration(
|
|
3018
|
+
options.chance.duration(player, duration);
|
|
2902
3019
|
return duration;
|
|
2903
3020
|
}
|
|
2904
3021
|
|
|
2905
3022
|
fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2906
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2907
|
-
@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)
|
|
2908
3028
|
else if (showdown)
|
|
2909
3029
|
battle.rng.range(u3, 2, 4)
|
|
2910
3030
|
else
|
|
2911
3031
|
@intCast((battle.rng.next() & 1) + 2);
|
|
2912
3032
|
|
|
2913
3033
|
assert(duration >= 2 and duration <= 3);
|
|
2914
|
-
options.chance.duration(
|
|
3034
|
+
options.chance.duration(player, duration);
|
|
2915
3035
|
return duration;
|
|
2916
3036
|
}
|
|
2917
3037
|
|
|
@@ -2924,8 +3044,11 @@ pub const Rolls = struct {
|
|
|
2924
3044
|
options: anytype,
|
|
2925
3045
|
) !u3 {
|
|
2926
3046
|
const roll = if (effect == .Binding) .duration else .multi_hit;
|
|
2927
|
-
const n: u3 = if (options.calc.overridden(player, roll)) |val|
|
|
2928
|
-
@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)
|
|
2929
3052
|
else if (showdown)
|
|
2930
3053
|
DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
|
|
2931
3054
|
else n: {
|
|
@@ -2935,7 +3058,7 @@ pub const Rolls = struct {
|
|
|
2935
3058
|
|
|
2936
3059
|
assert(n >= 2 and n <= 5);
|
|
2937
3060
|
if (effect == .Binding) {
|
|
2938
|
-
options.chance.duration(
|
|
3061
|
+
options.chance.duration(player, n);
|
|
2939
3062
|
} else {
|
|
2940
3063
|
try options.chance.multiHit(player, n);
|
|
2941
3064
|
}
|
|
@@ -2950,15 +3073,15 @@ pub const Rolls = struct {
|
|
|
2950
3073
|
options: anytype,
|
|
2951
3074
|
) !u4 {
|
|
2952
3075
|
// TODO: consider throwing error instead of rerolling?
|
|
2953
|
-
const
|
|
3076
|
+
const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
|
|
2954
3077
|
if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
|
|
2955
3078
|
val
|
|
2956
3079
|
else
|
|
2957
3080
|
null
|
|
2958
3081
|
else
|
|
2959
|
-
null
|
|
2960
|
-
|
|
2961
|
-
|
|
3082
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
|
|
3083
|
+
battle.rng.moveSlot(player, moves, check_pp)
|
|
3084
|
+
else slot: {
|
|
2962
3085
|
if (showdown) {
|
|
2963
3086
|
if (check_pp == 0) {
|
|
2964
3087
|
var i: usize = moves.len;
|
|
@@ -3022,11 +3145,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3022
3145
|
},
|
|
3023
3146
|
.Switch => {
|
|
3024
3147
|
const side = battle.side(player);
|
|
3025
|
-
var slot:
|
|
3148
|
+
var slot: usize = 2;
|
|
3026
3149
|
while (slot <= 6) : (slot += 1) {
|
|
3027
3150
|
const id = side.order[slot - 1];
|
|
3028
3151
|
if (id == 0 or side.pokemon[id - 1].hp == 0) continue;
|
|
3029
|
-
out[n] = .{ .type = .Switch, .data = slot };
|
|
3152
|
+
out[n] = .{ .type = .Switch, .data = @intCast(slot) };
|
|
3030
3153
|
n += 1;
|
|
3031
3154
|
}
|
|
3032
3155
|
if (n == 0) {
|
|
@@ -3038,11 +3161,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3038
3161
|
const side = battle.side(player);
|
|
3039
3162
|
const foe = battle.foe(player);
|
|
3040
3163
|
|
|
3041
|
-
|
|
3164
|
+
const active = &side.active;
|
|
3042
3165
|
const stored = side.stored();
|
|
3043
3166
|
|
|
3044
3167
|
// While players are not given any input options on the cartridge in these cases,
|
|
3045
|
-
// Pokémon Showdown instead produces a list with a single move that must be chosen
|
|
3168
|
+
// Pokémon Showdown instead produces a list with a single move that must be chosen
|
|
3046
3169
|
//
|
|
3047
3170
|
// Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
|
|
3048
3171
|
// here when not in compatibility mode, but the engine needs to be able to differentiate
|
|
@@ -3050,18 +3173,18 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3050
3173
|
// passing due to being forced into using a move. Instead of introducing another option
|
|
3051
3174
|
// we simply repurpose Move with no move slot, even though pedantically this is not
|
|
3052
3175
|
// strictly correct as the player would not have been presented the option to move or
|
|
3053
|
-
// switch at all
|
|
3176
|
+
// switch at all
|
|
3054
3177
|
if (isForced(active)) {
|
|
3055
3178
|
out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
|
|
3056
3179
|
n += 1;
|
|
3057
3180
|
return n;
|
|
3058
3181
|
}
|
|
3059
3182
|
|
|
3060
|
-
var slot:
|
|
3183
|
+
var slot: usize = 2;
|
|
3061
3184
|
while (slot <= 6) : (slot += 1) {
|
|
3062
3185
|
const id = side.order[slot - 1];
|
|
3063
3186
|
if (id == 0 or side.pokemon[id - 1].hp == 0) continue;
|
|
3064
|
-
out[n] = .{ .type = .Switch, .data = slot };
|
|
3187
|
+
out[n] = .{ .type = .Switch, .data = @intCast(slot) };
|
|
3065
3188
|
n += 1;
|
|
3066
3189
|
}
|
|
3067
3190
|
|
|
@@ -3069,7 +3192,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3069
3192
|
// On the cartridge, all of these happen after "FIGHT" (indicating you are not
|
|
3070
3193
|
// switching) but before you are allowed to select a move. Pokémon Showdown instead
|
|
3071
3194
|
// either disables all other moves in the case of limited or requires you to select a
|
|
3072
|
-
// move normally if sleeping/frozen/bound
|
|
3195
|
+
// move normally if sleeping/frozen/bound
|
|
3073
3196
|
if (!showdown and (limited or foe.active.volatiles.Binding or
|
|
3074
3197
|
Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
|
|
3075
3198
|
{
|
|
@@ -3082,7 +3205,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3082
3205
|
// Pokémon Showdown handles Bide and Binding moves by checking if the move in question
|
|
3083
3206
|
// is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
|
|
3084
3207
|
// Move will not result in forcing the subsequent use unless the user also had the
|
|
3085
|
-
// proc-ed move in their moveset) and disabling all other moves
|
|
3208
|
+
// proc-ed move in their moveset) and disabling all other moves
|
|
3086
3209
|
if (limited) {
|
|
3087
3210
|
assert(showdown);
|
|
3088
3211
|
assert(side.last_selected_move != .None);
|
|
@@ -3094,7 +3217,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3094
3217
|
const struggle =
|
|
3095
3218
|
m.id == .Bide and (m.pp == 0 or active.volatiles.disable_move == slot);
|
|
3096
3219
|
const s = if (struggle) 0 else slot;
|
|
3097
|
-
out[n] = .{ .type = .Move, .data = s };
|
|
3220
|
+
out[n] = .{ .type = .Move, .data = @intCast(s) };
|
|
3098
3221
|
n += 1;
|
|
3099
3222
|
return n;
|
|
3100
3223
|
}
|
|
@@ -3108,13 +3231,13 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3108
3231
|
if (m.id == .None) break;
|
|
3109
3232
|
if (m.pp == 0) continue;
|
|
3110
3233
|
if (active.volatiles.disable_move == slot) continue;
|
|
3111
|
-
out[n] = .{ .type = .Move, .data = slot };
|
|
3234
|
+
out[n] = .{ .type = .Move, .data = @intCast(slot) };
|
|
3112
3235
|
n += 1;
|
|
3113
3236
|
}
|
|
3114
3237
|
// Struggle (Pokémon Showdown would use 'move 1' here)
|
|
3115
3238
|
if (n == before) {
|
|
3116
3239
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
3117
|
-
if (!
|
|
3240
|
+
if (!pkmn.options.mod) {
|
|
3118
3241
|
while (slot <= 4) : (slot += 1) {
|
|
3119
3242
|
if (active.moves[slot - 1].pp != 0) return n;
|
|
3120
3243
|
}
|