@pkmn/engine 0.1.0-dev.12e7292c → 0.1.0-dev.130de754
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -3
- package/README.md +41 -25
- 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 +310 -203
- package/package.json +60 -134
- package/src/bin/install-pkmn-engine +70 -53
- package/src/bin/pkmn-debug +1 -1
- package/src/lib/bindings/c.zig +201 -0
- package/src/lib/{binding → bindings}/node.zig +15 -32
- 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 +46 -30
- package/src/lib/common/js.zig +5 -9
- package/src/lib/common/optional.zig +21 -22
- package/src/lib/common/options.zig +6 -0
- package/src/lib/common/protocol.zig +253 -223
- package/src/lib/common/rational.zig +340 -254
- package/src/lib/common/rng.zig +11 -13
- package/src/lib/common/util.zig +53 -1
- package/src/lib/gen1/calc.zig +724 -175
- package/src/lib/gen1/chance.zig +494 -308
- 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 +49 -49
- package/src/lib/gen1/helpers.zig +115 -562
- package/src/lib/gen1/mechanics.zig +411 -293
- package/src/lib/gen2/calc.zig +35 -28
- package/src/lib/gen2/chance.zig +122 -518
- 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 +39 -38
- package/src/lib/gen2/generated.zig +16 -26
- package/src/lib/gen2/helpers.zig +68 -39
- package/src/lib/gen2/mechanics.zig +217 -174
- package/src/lib/napi.h +2 -0
- package/src/lib/node.zig +14 -0
- package/src/lib/pkmn.zig +123 -29
- package/src/lib/wasm.zig +12 -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,28 @@
|
|
|
1
|
-
const
|
|
2
|
-
|
|
3
|
-
const pkmn = @import("../pkmn.zig");
|
|
1
|
+
const DEBUG = @import("../common/debug.zig").print;
|
|
4
2
|
|
|
3
|
+
const chance = @import("chance.zig");
|
|
5
4
|
const common = @import("../common/data.zig");
|
|
6
|
-
const
|
|
5
|
+
const data = @import("data.zig");
|
|
6
|
+
const pkmn = @import("../pkmn.zig");
|
|
7
7
|
const protocol = @import("../common/protocol.zig");
|
|
8
8
|
const rng = @import("../common/rng.zig");
|
|
9
|
-
|
|
10
|
-
const data = @import("data.zig");
|
|
9
|
+
const std = @import("std");
|
|
11
10
|
|
|
12
11
|
const assert = std.debug.assert;
|
|
13
|
-
|
|
14
|
-
const expectEqual = std.testing.expectEqual;
|
|
15
|
-
|
|
16
|
-
const showdown = pkmn.options.showdown;
|
|
17
|
-
|
|
18
|
-
const Choice = common.Choice;
|
|
19
|
-
const ID = common.ID;
|
|
20
|
-
const Player = common.Player;
|
|
21
|
-
const Result = common.Result;
|
|
22
|
-
|
|
23
12
|
const Boost = protocol.Boost;
|
|
13
|
+
const Choice = common.Choice;
|
|
24
14
|
const Damage = protocol.Damage;
|
|
15
|
+
const Effectiveness = data.Effectiveness;
|
|
16
|
+
const expectEqual = std.testing.expectEqual;
|
|
25
17
|
const Fail = protocol.Fail;
|
|
26
|
-
const Heal = protocol.Heal;
|
|
27
|
-
|
|
28
18
|
const Gen12 = rng.Gen12;
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
const Effectiveness = data.Effectiveness;
|
|
19
|
+
const Heal = protocol.Heal;
|
|
20
|
+
const ID = common.ID;
|
|
32
21
|
const Move = data.Move;
|
|
33
22
|
const MoveSlot = data.MoveSlot;
|
|
34
|
-
const
|
|
23
|
+
const Player = common.Player;
|
|
24
|
+
const Result = common.Result;
|
|
25
|
+
const showdown = pkmn.options.showdown;
|
|
35
26
|
const Side = data.Side;
|
|
36
27
|
const Species = data.Species;
|
|
37
28
|
const Stats = data.Stats;
|
|
@@ -71,8 +62,8 @@ pub fn update(battle: anytype, c1: Choice, c2: Choice, options: anytype) !Result
|
|
|
71
62
|
var p1 = battle.side(.P1);
|
|
72
63
|
var p2 = battle.side(.P2);
|
|
73
64
|
|
|
74
|
-
|
|
75
|
-
|
|
65
|
+
const r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
|
|
66
|
+
const r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
|
|
76
67
|
|
|
77
68
|
if (try turnOrder(battle, c1, c2, options) == .P1) {
|
|
78
69
|
if (try doTurn(battle, .P1, c1, r1, s1, .P2, c2, r2, s2, options)) |r| return r;
|
|
@@ -90,13 +81,13 @@ fn start(battle: anytype, options: anytype) !Result {
|
|
|
90
81
|
const p1 = battle.side(.P1);
|
|
91
82
|
const p2 = battle.side(.P2);
|
|
92
83
|
|
|
93
|
-
|
|
84
|
+
const p1_slot = findFirstAlive(p1);
|
|
94
85
|
assert(!showdown or p1_slot == 1);
|
|
95
|
-
if (p1_slot == 0) return if (findFirstAlive(p2) == 0)
|
|
86
|
+
if (p1_slot == 0) return if (findFirstAlive(p2) == 0) .Tie else .Lose;
|
|
96
87
|
|
|
97
|
-
|
|
88
|
+
const p2_slot = findFirstAlive(p2);
|
|
98
89
|
assert(!showdown or p2_slot == 1);
|
|
99
|
-
if (p2_slot == 0) return
|
|
90
|
+
if (p2_slot == 0) return .Win;
|
|
100
91
|
|
|
101
92
|
try switchIn(battle, .P1, p1_slot, true, options);
|
|
102
93
|
try switchIn(battle, .P2, p2_slot, true, options);
|
|
@@ -166,7 +157,7 @@ fn selectMove(
|
|
|
166
157
|
// to satisfy the conditions of the Desync Clause Mod. However, because Binding is still
|
|
167
158
|
// set the selected move will not actually be used, it will just be reported as having
|
|
168
159
|
// been used (this differs from how Pokémon Showdown works, but its impossible to
|
|
169
|
-
// replicate the incorrect behavior with the correct mechanisms)
|
|
160
|
+
// replicate the incorrect behavior with the correct mechanisms)
|
|
170
161
|
saveMove(battle, player, choice);
|
|
171
162
|
} else {
|
|
172
163
|
assert(choice.data == 0);
|
|
@@ -174,7 +165,10 @@ fn selectMove(
|
|
|
174
165
|
if (foe_choice.type == .Switch) {
|
|
175
166
|
const last = side.active.move(battle.lastMove(player).index);
|
|
176
167
|
if (last.id == .Metronome) side.last_selected_move = last.id;
|
|
177
|
-
if (last.id == .MirrorMove)
|
|
168
|
+
if (last.id == .MirrorMove) {
|
|
169
|
+
if (!pkmn.options.mod) return .Error;
|
|
170
|
+
side.last_selected_move = last.id;
|
|
171
|
+
}
|
|
178
172
|
}
|
|
179
173
|
}
|
|
180
174
|
return null;
|
|
@@ -253,11 +247,11 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: a
|
|
|
253
247
|
active.moves = incoming.moves;
|
|
254
248
|
|
|
255
249
|
statusModify(incoming.status, &active.stats);
|
|
250
|
+
options.chance.switched(player, slot);
|
|
256
251
|
|
|
257
252
|
foe.active.volatiles.Binding = false;
|
|
258
253
|
|
|
259
254
|
try options.log.switched(.{ battle.active(player), incoming });
|
|
260
|
-
options.chance.switched(player, side.order[0], out);
|
|
261
255
|
|
|
262
256
|
if (showdown and incoming.status == Status.TOX) {
|
|
263
257
|
incoming.status = Status.init(.PSN);
|
|
@@ -347,13 +341,13 @@ fn doTurn(
|
|
|
347
341
|
)) |r| return r;
|
|
348
342
|
if (!replace) {
|
|
349
343
|
if (player_choice.type != .Switch) {
|
|
350
|
-
if (try checkFaint(battle, foe_player, options)) |r| return r;
|
|
344
|
+
if (try checkFaint(battle, foe_player, true, options)) |r| return r;
|
|
351
345
|
}
|
|
352
346
|
if (residual) try handleResidual(battle, player, options);
|
|
353
|
-
if (try checkFaint(battle, player, options)) |r| return r;
|
|
347
|
+
if (try checkFaint(battle, player, false, options)) |r| return r;
|
|
354
348
|
} else if (foe_choice.type == .Pass) return null;
|
|
355
349
|
|
|
356
|
-
options.chance.clearPending();
|
|
350
|
+
if (!showdown) options.chance.clearPending();
|
|
357
351
|
|
|
358
352
|
residual = true;
|
|
359
353
|
replace = battle.side(foe_player).stored().hp == 0;
|
|
@@ -369,10 +363,10 @@ fn doTurn(
|
|
|
369
363
|
)) |r| return r;
|
|
370
364
|
if (!replace) {
|
|
371
365
|
if (!calc and foe_choice.type != .Switch) {
|
|
372
|
-
if (try checkFaint(battle, player, options)) |r| return r;
|
|
366
|
+
if (try checkFaint(battle, player, true, options)) |r| return r;
|
|
373
367
|
}
|
|
374
368
|
if (residual) try handleResidual(battle, foe_player, options);
|
|
375
|
-
if (try checkFaint(battle, foe_player, options)) |r| return r;
|
|
369
|
+
if (try checkFaint(battle, foe_player, false, options)) |r| return r;
|
|
376
370
|
}
|
|
377
371
|
|
|
378
372
|
// Flinch is bugged on Pokémon Showdown because it gets implemented with a duration which causes
|
|
@@ -425,19 +419,19 @@ fn executeMove(
|
|
|
425
419
|
mslot = @intCast(battle.lastMove(player).index);
|
|
426
420
|
const stored = side.stored();
|
|
427
421
|
// GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
|
|
428
|
-
auto = isForced(&side.active) or
|
|
422
|
+
auto = pkmn.options.mod or isForced(&side.active) or
|
|
429
423
|
side.active.volatiles.Binding or side.active.volatiles.Bide or
|
|
430
424
|
side.last_selected_move == .HyperBeam or
|
|
431
425
|
Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
|
|
432
426
|
// If it wasn't Hyper Beam or the continuation of a move effect then we must have just
|
|
433
427
|
// thawed, in which case we will desync unless the last_selected_move happened to be at
|
|
434
|
-
// index 1 and the current Pokémon has the same move in its first slot
|
|
428
|
+
// index 1 and the current Pokémon has the same move in its first slot
|
|
435
429
|
if (!auto) {
|
|
436
430
|
// side.active.moves(slot) is safe to check even though the slot in question might not
|
|
437
431
|
// technically be from this Pokémon because it must be exactly 1 to not desync and
|
|
438
432
|
// every Pokémon must have at least one move
|
|
439
433
|
if (mslot != 1 or side.active.move(mslot).id != side.last_selected_move) {
|
|
440
|
-
return
|
|
434
|
+
return .Error;
|
|
441
435
|
} else {
|
|
442
436
|
auto = true;
|
|
443
437
|
}
|
|
@@ -455,7 +449,26 @@ fn executeMove(
|
|
|
455
449
|
.skip_can => skip_can = true,
|
|
456
450
|
.skip_pp => skip_pp = true,
|
|
457
451
|
.ok => {},
|
|
458
|
-
.err => return
|
|
452
|
+
.err => return .Error,
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Pokémon Showdown incorrectly implements PP deduction when handling the Hyper Beam automatic
|
|
456
|
+
// selection glitch - if the move was proced due to Metronome / Mirror Move it either subtracts
|
|
457
|
+
// from the Hyper Beam slot if present or skips PP deduction entirely...
|
|
458
|
+
if (showdown and !skip_pp and side.last_selected_move == .HyperBeam and
|
|
459
|
+
side.last_selected_move != side.active.move(mslot).id)
|
|
460
|
+
{
|
|
461
|
+
assert(mslot == 1);
|
|
462
|
+
const has_beam = has_beam: {
|
|
463
|
+
for (side.active.moves, 0..) |m, i| {
|
|
464
|
+
if (m.id == .HyperBeam) {
|
|
465
|
+
mslot = @intCast(i + 1);
|
|
466
|
+
break :has_beam true;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
break :has_beam false;
|
|
470
|
+
};
|
|
471
|
+
if (!has_beam) skip_pp = true;
|
|
459
472
|
}
|
|
460
473
|
|
|
461
474
|
const can = skip_can or
|
|
@@ -484,17 +497,22 @@ fn beforeMove(
|
|
|
484
497
|
|
|
485
498
|
if (Status.is(stored.status, .SLP)) {
|
|
486
499
|
const before = stored.status;
|
|
500
|
+
const slf = Status.is(stored.status, .EXT);
|
|
487
501
|
// Even if the EXT bit is set this will still correctly modify the sleep duration
|
|
488
|
-
if (options.calc.
|
|
489
|
-
|
|
502
|
+
if (options.calc.overridden(player, .sleep)) |obs| switch (obs) {
|
|
503
|
+
.started, .ended => stored.status = 0,
|
|
504
|
+
.continuing => if (Status.duration(stored.status) > 1) {
|
|
505
|
+
stored.status -= 1;
|
|
506
|
+
},
|
|
507
|
+
else => unreachable,
|
|
490
508
|
} else {
|
|
491
509
|
stored.status -= 1;
|
|
492
510
|
}
|
|
493
511
|
|
|
494
512
|
const duration = Status.duration(stored.status);
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
513
|
+
try options.chance.sleep(player, if (slf)
|
|
514
|
+
.None
|
|
515
|
+
else if (duration == 0) .ended else .continuing);
|
|
498
516
|
|
|
499
517
|
if (duration == 0) {
|
|
500
518
|
try log.curestatus(.{ ident, before, .Message });
|
|
@@ -532,12 +550,12 @@ fn beforeMove(
|
|
|
532
550
|
}
|
|
533
551
|
|
|
534
552
|
if (volatiles.disable_duration > 0) {
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
553
|
+
volatiles.disable_duration =
|
|
554
|
+
decrement(.disable, player, options, volatiles.disable_duration);
|
|
555
|
+
try options.chance.disable(
|
|
556
|
+
player,
|
|
557
|
+
if (volatiles.disable_duration == 0) .ended else .continuing,
|
|
558
|
+
);
|
|
541
559
|
|
|
542
560
|
if (volatiles.disable_duration == 0) {
|
|
543
561
|
volatiles.disable_move = 0;
|
|
@@ -550,19 +568,24 @@ fn beforeMove(
|
|
|
550
568
|
// This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
|
|
551
569
|
// before the side had a selected a move - we simply need to assume this leads to a desync
|
|
552
570
|
if (side.last_selected_move == .None) {
|
|
553
|
-
assert(!
|
|
571
|
+
assert(!pkmn.options.mod);
|
|
554
572
|
return .err;
|
|
555
573
|
}
|
|
556
574
|
|
|
557
575
|
if (volatiles.Confusion) {
|
|
558
576
|
assert(volatiles.confusion > 0);
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
if (
|
|
577
|
+
if (options.calc.overridden(player, .confusion)) |obs| switch (obs) {
|
|
578
|
+
.started, .ended => volatiles.confusion = 0,
|
|
579
|
+
.continuing => if (volatiles.confusion > 1) {
|
|
580
|
+
volatiles.confusion -= 1;
|
|
581
|
+
},
|
|
582
|
+
.overwritten => {},
|
|
583
|
+
else => unreachable,
|
|
562
584
|
} else {
|
|
563
585
|
volatiles.confusion -= 1;
|
|
564
586
|
}
|
|
565
|
-
|
|
587
|
+
|
|
588
|
+
try options.chance.confusion(player, if (volatiles.confusion == 0) .ended else .continuing);
|
|
566
589
|
|
|
567
590
|
if (volatiles.confusion == 0) {
|
|
568
591
|
volatiles.Confusion = false;
|
|
@@ -580,7 +603,9 @@ fn beforeMove(
|
|
|
580
603
|
volatiles.Charging = false;
|
|
581
604
|
volatiles.Binding = false;
|
|
582
605
|
volatiles.Invulnerable = false;
|
|
583
|
-
|
|
606
|
+
|
|
607
|
+
options.chance.observe(.attacking, player, .None);
|
|
608
|
+
options.chance.observe(.binding, player, .None);
|
|
584
609
|
{
|
|
585
610
|
// This feels (and is) disgusting but the cartridge literally just overwrites
|
|
586
611
|
// the opponent's defense with the user's defense and resets it after. As a
|
|
@@ -588,19 +613,14 @@ fn beforeMove(
|
|
|
588
613
|
const def = foe.active.stats.def;
|
|
589
614
|
foe.active.stats.def = active.stats.def;
|
|
590
615
|
defer foe.active.stats.def = def;
|
|
591
|
-
if (!calcDamage(battle, player, player.foe(), null, false))
|
|
616
|
+
if (!calcDamage(battle, player, player.foe(), null, false, options)) {
|
|
617
|
+
return .err;
|
|
618
|
+
}
|
|
592
619
|
}
|
|
593
|
-
// 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
620
|
|
|
601
621
|
const uncapped = battle.last_damage;
|
|
602
622
|
// Skipping adjustDamage / randomizeDamage / checkHit
|
|
603
|
-
_ = try applyDamage(battle, player,
|
|
623
|
+
_ = try applyDamage(battle, player, player.foe(), .Confusion, options);
|
|
604
624
|
// Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
|
|
605
625
|
if (showdown) battle.last_damage = uncapped;
|
|
606
626
|
|
|
@@ -617,7 +637,10 @@ fn beforeMove(
|
|
|
617
637
|
volatiles.Thrashing = false;
|
|
618
638
|
volatiles.Charging = false;
|
|
619
639
|
volatiles.Binding = false;
|
|
620
|
-
|
|
640
|
+
|
|
641
|
+
options.chance.observe(.attacking, player, .None);
|
|
642
|
+
options.chance.observe(.binding, player, .None);
|
|
643
|
+
|
|
621
644
|
// GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
|
|
622
645
|
try log.cant(.{ ident, .Paralysis });
|
|
623
646
|
return .done;
|
|
@@ -635,14 +658,8 @@ fn beforeMove(
|
|
|
635
658
|
volatiles.state +%= battle.last_damage;
|
|
636
659
|
}
|
|
637
660
|
|
|
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);
|
|
661
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
662
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
646
663
|
|
|
647
664
|
if (volatiles.attacks != 0) {
|
|
648
665
|
try log.activate(.{ ident, .Bide });
|
|
@@ -676,39 +693,40 @@ fn beforeMove(
|
|
|
676
693
|
}
|
|
677
694
|
|
|
678
695
|
if (volatiles.Thrashing) {
|
|
679
|
-
|
|
696
|
+
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
680
697
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
698
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
699
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
700
|
+
|
|
701
|
+
if (volatiles.attacks == 0) {
|
|
702
|
+
const overwritten = volatiles.Confusion;
|
|
703
|
+
|
|
704
|
+
volatiles.Thrashing = false;
|
|
705
|
+
volatiles.Confusion = true;
|
|
706
|
+
volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
687
707
|
|
|
688
|
-
|
|
708
|
+
// On Pokémon Showdown this could leak information to the opponent that thrashing has
|
|
709
|
+
// ended (the engine's protocol is always from the omniscient perspective and clients
|
|
710
|
+
// can be expected to recognize this pattern and pass the information only to the
|
|
711
|
+
// correct player)
|
|
689
712
|
try log.start(.{ battle.active(player), .ConfusionSilent });
|
|
713
|
+
options.chance.observe(.confusion, player, if (overwritten)
|
|
714
|
+
if (pkmn.options.overwrite) .overwritten else .continuing
|
|
715
|
+
else
|
|
716
|
+
.started);
|
|
717
|
+
if (!overwritten) options.calc.confusion(player);
|
|
690
718
|
}
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
if (handleThrashing(battle, active, player, options)) {
|
|
697
|
-
try log.start(.{ battle.active(player), .ConfusionSilent });
|
|
698
|
-
}
|
|
699
|
-
}
|
|
719
|
+
|
|
720
|
+
// This shouldn't actually set last_used_move, but Pokémon Showdown sets last
|
|
721
|
+
// used in useMove and doesn't have the notion of skipping canMove semantics
|
|
722
|
+
if (showdown) side.last_used_move = side.last_selected_move;
|
|
723
|
+
|
|
700
724
|
return .skip_can;
|
|
701
725
|
}
|
|
702
726
|
|
|
703
727
|
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);
|
|
728
|
+
volatiles.attacks = decrement(.binding, player, options, volatiles.attacks);
|
|
729
|
+
try options.chance.binding(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
712
730
|
|
|
713
731
|
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
714
732
|
if (showdown or battle.last_damage != 0) {
|
|
@@ -738,7 +756,6 @@ fn canMove(
|
|
|
738
756
|
const player_ident = battle.active(player);
|
|
739
757
|
const move = Move.get(side.last_selected_move);
|
|
740
758
|
|
|
741
|
-
var skip = skip_pp;
|
|
742
759
|
if (side.active.volatiles.Charging) {
|
|
743
760
|
side.active.volatiles.Charging = false;
|
|
744
761
|
side.active.volatiles.Invulnerable = false;
|
|
@@ -750,7 +767,7 @@ fn canMove(
|
|
|
750
767
|
}
|
|
751
768
|
|
|
752
769
|
side.last_used_move = side.last_selected_move;
|
|
753
|
-
if (!
|
|
770
|
+
if (!skip_pp) decrementPP(side, mslot, auto);
|
|
754
771
|
|
|
755
772
|
const target = if (move.target == .Self) player else player.foe();
|
|
756
773
|
try options.log.move(.{ player_ident, side.last_selected_move, battle.active(target), from });
|
|
@@ -763,9 +780,14 @@ fn canMove(
|
|
|
763
780
|
|
|
764
781
|
if (move.effect == .Thrashing) {
|
|
765
782
|
Effects.thrashing(battle, player, options);
|
|
766
|
-
} else if (
|
|
767
|
-
// Pokémon Showdown handles this after hit/miss checks and damage calculation
|
|
768
|
-
|
|
783
|
+
} else if (move.effect == .Binding) {
|
|
784
|
+
// Pokémon Showdown handles this after hit/miss checks and damage calculation, though clears
|
|
785
|
+
// Recharging in onTryMove (required for the Hyper Beam automatic selection glitch)
|
|
786
|
+
if (showdown) {
|
|
787
|
+
battle.foe(player).active.volatiles.Recharging = false;
|
|
788
|
+
} else {
|
|
789
|
+
try Effects.binding(battle, player, false, options);
|
|
790
|
+
}
|
|
769
791
|
}
|
|
770
792
|
|
|
771
793
|
return true;
|
|
@@ -802,7 +824,7 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
|
|
|
802
824
|
assert(active.move(mslot).pp == side.stored().move(mslot).pp);
|
|
803
825
|
}
|
|
804
826
|
|
|
805
|
-
fn incrementPP(side: *Side, mslot: u4) void {
|
|
827
|
+
fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
|
|
806
828
|
var active = &side.active;
|
|
807
829
|
const volatiles = &active.volatiles;
|
|
808
830
|
|
|
@@ -810,8 +832,11 @@ fn incrementPP(side: *Side, mslot: u4) void {
|
|
|
810
832
|
// GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
|
|
811
833
|
if (showdown and volatiles.Transform) return;
|
|
812
834
|
|
|
835
|
+
const n = options.calc.overridden(player, .pp) orelse 1;
|
|
836
|
+
if (volatiles.Transform and !options.chance.pp(player, n)) return;
|
|
837
|
+
|
|
813
838
|
assert(mslot > 0 and mslot <= 4);
|
|
814
|
-
side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +%
|
|
839
|
+
side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% n;
|
|
815
840
|
}
|
|
816
841
|
|
|
817
842
|
// Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
|
|
@@ -852,20 +877,20 @@ fn doMove(
|
|
|
852
877
|
var miss = showdown and miss: {
|
|
853
878
|
immune = move.target != .Self and !status and !counter and
|
|
854
879
|
(@intFromEnum(move.type.effectiveness(foe.active.types.type1)) == 0 or
|
|
855
|
-
|
|
880
|
+
@intFromEnum(move.type.effectiveness(foe.active.types.type2)) == 0);
|
|
856
881
|
if (immune and move.effect != .Binding) break :miss true;
|
|
857
882
|
if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) {
|
|
858
883
|
battle.last_damage = 0;
|
|
859
884
|
break :miss true;
|
|
860
885
|
}
|
|
861
886
|
break :miss (move.target != .Self and
|
|
862
|
-
!moveHit(battle, player, move, &immune, &mist, options));
|
|
887
|
+
!try moveHit(battle, player, move, &immune, &mist, options));
|
|
863
888
|
};
|
|
864
889
|
assert(!immune or miss or (showdown and move.effect == .Binding));
|
|
865
890
|
|
|
866
891
|
var late = showdown and move.effect != .Explode;
|
|
867
892
|
const skip = status or immune;
|
|
868
|
-
if ((!showdown or (!skip or counter)) and !miss)
|
|
893
|
+
if ((!showdown or (!skip or counter)) and !miss) damage: {
|
|
869
894
|
if (showdown and move.effect.isMulti()) {
|
|
870
895
|
try Effects.multiHit(battle, player, move, options);
|
|
871
896
|
hits = side.active.volatiles.attacks;
|
|
@@ -887,9 +912,10 @@ fn doMove(
|
|
|
887
912
|
ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
|
|
888
913
|
// This can overflow after adjustDamage, but will still be sufficient to OHKO
|
|
889
914
|
battle.last_damage = if (ohko) 65535 else 0;
|
|
890
|
-
if (showdown) break :
|
|
891
|
-
} else if (!calcDamage(battle, player, player.foe(), move, crit)) {
|
|
892
|
-
|
|
915
|
+
if (showdown) break :damage; // skip adjustDamage / randomizeDamage
|
|
916
|
+
} else if (!calcDamage(battle, player, player.foe(), move, crit, options)) {
|
|
917
|
+
if (!showdown) try options.chance.commit(player, .err);
|
|
918
|
+
return .Error;
|
|
893
919
|
}
|
|
894
920
|
if (battle.last_damage == 0) {
|
|
895
921
|
immune = true;
|
|
@@ -902,11 +928,12 @@ fn doMove(
|
|
|
902
928
|
}
|
|
903
929
|
}
|
|
904
930
|
|
|
905
|
-
var
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
931
|
+
var missed: bool = undefined;
|
|
932
|
+
const zero = battle.last_damage == 0;
|
|
933
|
+
if (!showdown and !skip) {
|
|
934
|
+
missed = !try moveHit(battle, player, move, &immune, &mist, options);
|
|
935
|
+
miss = missed or zero;
|
|
936
|
+
}
|
|
910
937
|
|
|
911
938
|
assert(showdown or miss or battle.last_damage > 0 or skip);
|
|
912
939
|
assert((!showdown and miss) or !(ohko and immune));
|
|
@@ -933,11 +960,32 @@ fn doMove(
|
|
|
933
960
|
try log.immune(.{ foe_ident, .OHKO });
|
|
934
961
|
} else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
|
|
935
962
|
try log.immune(.{ foe_ident, .None });
|
|
963
|
+
if (!showdown) {
|
|
964
|
+
if (move.effect == .Binding) {
|
|
965
|
+
// Rather obnoxiously, the cartridge sets the Binding volatile in canMove and
|
|
966
|
+
// then simply clears it later on if the move happens to miss in moveHit.
|
|
967
|
+
// However, moveHit can return true and we'll still end up in the outer miss
|
|
968
|
+
// handling block because attacking into an immune Pokemon does zero damage
|
|
969
|
+
// (considered missing)
|
|
970
|
+
assert(zero);
|
|
971
|
+
// Thus we still need to actually save the hit result (but not the crit roll).
|
|
972
|
+
// Using .miss for this commit would accomplish what we want, but is somewhat
|
|
973
|
+
// misleading as its entirely possible the move actually did hit (and the target
|
|
974
|
+
// is now trapped) so we use the .binding value instead which is effectively
|
|
975
|
+
// identical, just doesn't claim to know about the actual results of the hit
|
|
976
|
+
// roll)
|
|
977
|
+
try options.chance.commit(player, .binding);
|
|
978
|
+
} else {
|
|
979
|
+
// The .glitch option allows for conditionally committing if we are in a
|
|
980
|
+
// division-by-zero freeze alternative timeline scenario
|
|
981
|
+
try options.chance.commit(player, .glitch);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
936
984
|
} else if (mist) {
|
|
937
985
|
if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
|
|
938
986
|
try log.fail(.{ foe_ident, .None });
|
|
939
987
|
} else {
|
|
940
|
-
if (showdown
|
|
988
|
+
if (!showdown) try options.chance.commit(player, if (zero and !missed) .hit else .miss);
|
|
941
989
|
try log.lastmiss(.{});
|
|
942
990
|
try log.miss(.{battle.active(player)});
|
|
943
991
|
}
|
|
@@ -952,17 +1000,15 @@ fn doMove(
|
|
|
952
1000
|
try buildRage(battle, player.foe(), options);
|
|
953
1001
|
} else if (showdown and move.effect == .Disable) {
|
|
954
1002
|
try buildRage(battle, player.foe(), options);
|
|
955
|
-
} else if (!showdown and immune and move.effect == .Binding) {
|
|
956
|
-
try options.chance.commit(player, .binding);
|
|
957
1003
|
}
|
|
958
1004
|
return null;
|
|
959
|
-
} else {
|
|
1005
|
+
} else if (!showdown) {
|
|
960
1006
|
try options.chance.commit(player, .hit);
|
|
961
1007
|
}
|
|
962
1008
|
|
|
963
1009
|
// On the cartridge MultiHit doesn't get set up until after damage has been applied for the
|
|
964
1010
|
// first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
|
|
965
|
-
// it up above before damage calculation)
|
|
1011
|
+
// it up above before damage calculation)
|
|
966
1012
|
if (!showdown and move.effect.isMulti()) {
|
|
967
1013
|
try Effects.multiHit(battle, player, move, options);
|
|
968
1014
|
hits = side.active.volatiles.attacks;
|
|
@@ -972,7 +1018,7 @@ fn doMove(
|
|
|
972
1018
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
973
1019
|
|
|
974
1020
|
var nullified = false;
|
|
975
|
-
var hit:
|
|
1021
|
+
var hit: usize = 0;
|
|
976
1022
|
while (hit < hits) {
|
|
977
1023
|
if (hit == 0) {
|
|
978
1024
|
if (crit) try log.crit(.{battle.active(player.foe())});
|
|
@@ -1000,33 +1046,31 @@ fn doMove(
|
|
|
1000
1046
|
if (showdown and move.effect == .Twineedle and !nullified and foe.stored().hp > 0) {
|
|
1001
1047
|
try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
1002
1048
|
}
|
|
1003
|
-
try log.hitcount(.{ battle.active(player.foe()), hit });
|
|
1004
|
-
} else if (showdown) {
|
|
1049
|
+
try log.hitcount(.{ battle.active(player.foe()), @as(u4, @intCast(hit)) });
|
|
1050
|
+
} else if (showdown and move.effect == .Binding) {
|
|
1005
1051
|
// 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;
|
|
1052
|
+
try Effects.binding(battle, player, rewrap, options);
|
|
1053
|
+
if (immune) {
|
|
1054
|
+
battle.last_damage = 0;
|
|
1055
|
+
assert(foe.stored().hp > 0);
|
|
1056
|
+
// Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
|
|
1057
|
+
if (sub) {
|
|
1058
|
+
try log.activate(.{ battle.active(player.foe()), .Substitute });
|
|
1059
|
+
} else {
|
|
1060
|
+
try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
|
|
1061
|
+
try buildRage(battle, player.foe(), options);
|
|
1019
1062
|
}
|
|
1063
|
+
return null;
|
|
1020
1064
|
}
|
|
1021
1065
|
}
|
|
1022
1066
|
|
|
1023
1067
|
// 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
|
|
1068
|
+
// if an effect was intended to "always happen" it will still get skipped
|
|
1025
1069
|
if (nullified) return null;
|
|
1026
1070
|
|
|
1027
1071
|
// On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
|
|
1028
1072
|
// 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
|
|
1073
|
+
// setup before the loop means we can avoid having to waste time doing no-op handler searches
|
|
1030
1074
|
if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
|
|
1031
1075
|
|
|
1032
1076
|
if (foe.stored().hp == 0) return null;
|
|
@@ -1041,7 +1085,7 @@ fn doMove(
|
|
|
1041
1085
|
// below). For Twineedle we change the data to that of one with PoisonChance1 given its
|
|
1042
1086
|
// MultiHit behavior is complete after the loop above, though Pokémon Showdown handles the
|
|
1043
1087
|
// Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
|
|
1044
|
-
// puts the |-status| message before |-hitcount| instead of after
|
|
1088
|
+
// puts the |-status| message before |-hitcount| instead of after
|
|
1045
1089
|
if (move.effect == .Twineedle) {
|
|
1046
1090
|
if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
1047
1091
|
} else if (move.effect == .Disable) {
|
|
@@ -1060,12 +1104,14 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
|
|
|
1060
1104
|
const side = battle.side(player);
|
|
1061
1105
|
|
|
1062
1106
|
// Base speed is used for the critical hit calculation, even when Transform-ed
|
|
1063
|
-
var rate =
|
|
1107
|
+
var rate: u16 = Species.chance(side.stored().species);
|
|
1064
1108
|
// GLITCH: Focus Energy reduces critical hit rate instead of increasing it
|
|
1065
1109
|
rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
|
|
1066
1110
|
rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
|
|
1067
1111
|
|
|
1068
|
-
|
|
1112
|
+
const spurious =
|
|
1113
|
+
!showdown and ((move.bp == 0 and move.effect != .OHKO) or move.target == .Depends);
|
|
1114
|
+
return Rolls.criticalHit(battle, player, @intCast(rate), spurious, options);
|
|
1069
1115
|
}
|
|
1070
1116
|
|
|
1071
1117
|
fn calcDamage(
|
|
@@ -1074,6 +1120,7 @@ fn calcDamage(
|
|
|
1074
1120
|
target_player: Player,
|
|
1075
1121
|
m: ?Move.Data,
|
|
1076
1122
|
crit: bool,
|
|
1123
|
+
options: anytype,
|
|
1077
1124
|
) bool {
|
|
1078
1125
|
// Confusion (indicated when m == null) just needs a 40 BP physical move
|
|
1079
1126
|
const cfz = m == null;
|
|
@@ -1085,28 +1132,8 @@ fn calcDamage(
|
|
|
1085
1132
|
|
|
1086
1133
|
const special = move.type.special();
|
|
1087
1134
|
|
|
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
|
|
1135
|
+
var atk = attack(side, crit, special);
|
|
1136
|
+
var def = defense(target, crit, special, cfz);
|
|
1110
1137
|
|
|
1111
1138
|
// Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
|
|
1112
1139
|
// will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
|
|
@@ -1114,12 +1141,12 @@ fn calcDamage(
|
|
|
1114
1141
|
if ((!showdown or !cfz) and (atk > 255 or def > 255)) {
|
|
1115
1142
|
atk = @max((atk / 4) & 255, 1);
|
|
1116
1143
|
// 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 (
|
|
1144
|
+
def = @max((def / 4) & 255, if (pkmn.options.mod) 1 else 0);
|
|
1118
1145
|
}
|
|
1119
1146
|
|
|
1120
|
-
const lvl =
|
|
1147
|
+
const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
|
|
1121
1148
|
|
|
1122
|
-
def =
|
|
1149
|
+
def = if (move.effect == .Explode) @max(def / 2, 1) else def;
|
|
1123
1150
|
|
|
1124
1151
|
if (def == 0) return false;
|
|
1125
1152
|
|
|
@@ -1131,11 +1158,41 @@ fn calcDamage(
|
|
|
1131
1158
|
d = @min(997, d);
|
|
1132
1159
|
d += 2;
|
|
1133
1160
|
|
|
1161
|
+
// Whether or not a critical hit occurred can change whether a division-by-zero freeze is
|
|
1162
|
+
// encountered so we need to make sure that in the event of a miss we commit the status of the
|
|
1163
|
+
// crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
|
|
1164
|
+
// that results in counter intuitive probabilities in the common case
|
|
1165
|
+
if (pkmn.options.chance and !pkmn.options.mod and move.effect != .Explode) {
|
|
1166
|
+
atk = attack(side, !crit, special);
|
|
1167
|
+
def = defense(target, !crit, special, false);
|
|
1168
|
+
if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1134
1171
|
battle.last_damage = @intCast(d);
|
|
1135
1172
|
|
|
1136
1173
|
return true;
|
|
1137
1174
|
}
|
|
1138
1175
|
|
|
1176
|
+
fn attack(side: *Side, crit: bool, special: bool) u32 {
|
|
1177
|
+
return if (crit)
|
|
1178
|
+
if (special) side.stored().stats.spc else side.stored().stats.atk
|
|
1179
|
+
else if (special) side.active.stats.spc else side.active.stats.atk;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
fn defense(target: *Side, crit: bool, special: bool, cfz: bool) u32 {
|
|
1183
|
+
return if (crit)
|
|
1184
|
+
if (special) target.stored().stats.spc else target.stored().stats.def
|
|
1185
|
+
else
|
|
1186
|
+
// GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
|
|
1187
|
+
if (special)
|
|
1188
|
+
target.active.stats.spc *
|
|
1189
|
+
@as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
|
|
1190
|
+
else
|
|
1191
|
+
// Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
|
|
1192
|
+
target.active.stats.def *
|
|
1193
|
+
@as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1139
1196
|
fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
1140
1197
|
const side = battle.side(player);
|
|
1141
1198
|
const foe = battle.foe(player);
|
|
@@ -1180,7 +1237,6 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1180
1237
|
const foe = battle.foe(player);
|
|
1181
1238
|
|
|
1182
1239
|
if (!try checkHit(battle, player, move, options)) return null;
|
|
1183
|
-
try options.chance.commit(player, .hit);
|
|
1184
1240
|
|
|
1185
1241
|
battle.last_damage = switch (side.last_selected_move) {
|
|
1186
1242
|
.SuperFang => @max(foe.stored().hp / 2, 1),
|
|
@@ -1191,13 +1247,13 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1191
1247
|
.Psywave => power: {
|
|
1192
1248
|
const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
|
|
1193
1249
|
// GLITCH: Psywave infinite glitch loop
|
|
1194
|
-
if (!
|
|
1250
|
+
if (!pkmn.options.mod and max <= 1) return .Error;
|
|
1195
1251
|
break :power try Rolls.psywave(battle, player, max, options);
|
|
1196
1252
|
},
|
|
1197
1253
|
else => unreachable,
|
|
1198
1254
|
};
|
|
1199
1255
|
|
|
1200
|
-
if (battle.last_damage == 0) return if (
|
|
1256
|
+
if (battle.last_damage == 0) return if (pkmn.options.mod) null else .Error;
|
|
1201
1257
|
|
|
1202
1258
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1203
1259
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
@@ -1217,15 +1273,15 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1217
1273
|
// Pretend Splash was used as a stand-in when no move has been used to fail below with 0 BP
|
|
1218
1274
|
const foe_last_selected_move =
|
|
1219
1275
|
Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1276
|
+
.Splash
|
|
1277
|
+
else
|
|
1278
|
+
foe.last_selected_move);
|
|
1223
1279
|
|
|
1224
1280
|
const used = battle.lastMove(player.foe()).counterable != 0;
|
|
1225
1281
|
const selected = foe_last_selected_move.bp > 0 and
|
|
1226
1282
|
foe.last_selected_move != .Counter and
|
|
1227
1283
|
(foe_last_selected_move.type == .Normal or
|
|
1228
|
-
|
|
1284
|
+
foe_last_selected_move.type == .Fighting);
|
|
1229
1285
|
|
|
1230
1286
|
if (!used and !selected) {
|
|
1231
1287
|
try options.log.fail(.{ battle.active(player), .None });
|
|
@@ -1234,16 +1290,15 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1234
1290
|
|
|
1235
1291
|
if (!used or !selected) {
|
|
1236
1292
|
// GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
|
|
1237
|
-
if (!
|
|
1293
|
+
if (!pkmn.options.mod) return .Error;
|
|
1238
1294
|
try options.log.fail(.{ battle.active(player), .None });
|
|
1239
1295
|
return null;
|
|
1240
1296
|
}
|
|
1241
1297
|
|
|
1242
1298
|
battle.last_damage *|= 2;
|
|
1243
1299
|
|
|
1244
|
-
// Pokémon Showdown calls
|
|
1300
|
+
// Pokémon Showdown calls moveHit before Counter
|
|
1245
1301
|
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
1246
|
-
try options.chance.commit(player, .hit);
|
|
1247
1302
|
|
|
1248
1303
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1249
1304
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
@@ -1272,7 +1327,7 @@ fn applyDamage(
|
|
|
1272
1327
|
subbed.active.volatiles.Substitute = false;
|
|
1273
1328
|
// battle.last_damage is not updated with the amount of HP the Substitute had
|
|
1274
1329
|
try options.log.end(.{ battle.active(sub_player), .Substitute });
|
|
1275
|
-
options.calc.capped(
|
|
1330
|
+
if (reason != .Confusion) options.calc.capped(sub_player);
|
|
1276
1331
|
return true;
|
|
1277
1332
|
} else {
|
|
1278
1333
|
// Safe to truncate since less than subbed.volatiles.substitute which is a u8
|
|
@@ -1306,7 +1361,7 @@ fn mirrorMove(
|
|
|
1306
1361
|
try options.log.fail(.{ battle.active(player), .None });
|
|
1307
1362
|
return null;
|
|
1308
1363
|
} else if (!showdown or foe.last_used_move != .Struggle) {
|
|
1309
|
-
incrementPP(side, mslot);
|
|
1364
|
+
incrementPP(side, player, mslot, options);
|
|
1310
1365
|
}
|
|
1311
1366
|
|
|
1312
1367
|
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
|
|
@@ -1327,7 +1382,7 @@ fn metronome(
|
|
|
1327
1382
|
var side = battle.side(player);
|
|
1328
1383
|
|
|
1329
1384
|
side.last_selected_move = try Rolls.metronome(battle, player, options);
|
|
1330
|
-
incrementPP(side, mslot);
|
|
1385
|
+
incrementPP(side, player, mslot, options);
|
|
1331
1386
|
|
|
1332
1387
|
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
|
|
1333
1388
|
return null;
|
|
@@ -1339,7 +1394,10 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
|
|
|
1339
1394
|
var immune = false;
|
|
1340
1395
|
var mist = false;
|
|
1341
1396
|
|
|
1342
|
-
if (moveHit(battle, player, move, &immune, &mist, options))
|
|
1397
|
+
if (try moveHit(battle, player, move, &immune, &mist, options)) {
|
|
1398
|
+
if (!showdown) try options.chance.commit(player, .hit);
|
|
1399
|
+
return true;
|
|
1400
|
+
}
|
|
1343
1401
|
|
|
1344
1402
|
assert(!immune);
|
|
1345
1403
|
if (mist) {
|
|
@@ -1348,11 +1406,11 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
|
|
|
1348
1406
|
try options.log.activate(.{ foe_ident, .Mist });
|
|
1349
1407
|
try options.log.fail(.{ foe_ident, .None });
|
|
1350
1408
|
} else {
|
|
1351
|
-
try options.chance.commit(player, .miss);
|
|
1352
1409
|
try options.log.lastmiss(.{});
|
|
1353
1410
|
try options.log.miss(.{battle.active(player)});
|
|
1354
1411
|
}
|
|
1355
1412
|
|
|
1413
|
+
if (!showdown) try options.chance.commit(player, .miss);
|
|
1356
1414
|
return false;
|
|
1357
1415
|
}
|
|
1358
1416
|
|
|
@@ -1363,7 +1421,7 @@ fn moveHit(
|
|
|
1363
1421
|
immune: *bool,
|
|
1364
1422
|
mist: *bool,
|
|
1365
1423
|
options: anytype,
|
|
1366
|
-
) bool {
|
|
1424
|
+
) !bool {
|
|
1367
1425
|
var side = battle.side(player);
|
|
1368
1426
|
const foe = battle.foe(player);
|
|
1369
1427
|
|
|
@@ -1383,7 +1441,16 @@ fn moveHit(
|
|
|
1383
1441
|
break :miss true;
|
|
1384
1442
|
}
|
|
1385
1443
|
if (!showdown) {
|
|
1386
|
-
if (move.effect == .Swift)
|
|
1444
|
+
if (move.effect == .Swift) {
|
|
1445
|
+
// If Swift could cause a division-by-zero freeze (which doesn't exist on Pokémon
|
|
1446
|
+
// Showdown, hence why the block above doesn't include this code), we need the
|
|
1447
|
+
// damage roll to be committed. However, the commit code will not save this roll if
|
|
1448
|
+
// the glitch occurs and the move was not marked as having hit. To work around this
|
|
1449
|
+
// we flag that the move hit (as Swift trivially does), but pass the probability of
|
|
1450
|
+
// the hit as zero to ensure it doesn't actually get saved.
|
|
1451
|
+
try options.chance.hit(player, true, 0);
|
|
1452
|
+
return true;
|
|
1453
|
+
}
|
|
1387
1454
|
if (foe.active.volatiles.Invulnerable) break :miss true;
|
|
1388
1455
|
}
|
|
1389
1456
|
|
|
@@ -1402,7 +1469,7 @@ fn moveHit(
|
|
|
1402
1469
|
const overwritten = overwrite and state > 0;
|
|
1403
1470
|
assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
|
|
1404
1471
|
|
|
1405
|
-
var accuracy = if (overwritten) state else
|
|
1472
|
+
var accuracy: u16 = if (overwritten) state else move.accuracy;
|
|
1406
1473
|
var boost = BOOSTS[@as(u4, @intCast(@as(i8, side.active.boosts.accuracy) + 6))];
|
|
1407
1474
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1408
1475
|
boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
|
|
@@ -1414,7 +1481,7 @@ fn moveHit(
|
|
|
1414
1481
|
if (overwrite) side.active.volatiles.state = accuracy;
|
|
1415
1482
|
|
|
1416
1483
|
// GLITCH: max accuracy is 255 so 1/256 chance of miss
|
|
1417
|
-
break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
|
|
1484
|
+
break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
|
|
1418
1485
|
};
|
|
1419
1486
|
|
|
1420
1487
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1425,13 +1492,17 @@ fn moveHit(
|
|
|
1425
1492
|
|
|
1426
1493
|
if (!miss) return true;
|
|
1427
1494
|
if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
|
|
1495
|
+
|
|
1428
1496
|
side.active.volatiles.Binding = false;
|
|
1497
|
+
options.chance.observe(.binding, player, .None);
|
|
1498
|
+
|
|
1429
1499
|
return false;
|
|
1430
1500
|
}
|
|
1431
1501
|
|
|
1432
1502
|
fn checkFaint(
|
|
1433
1503
|
battle: anytype,
|
|
1434
1504
|
player: Player,
|
|
1505
|
+
cap: bool,
|
|
1435
1506
|
options: anytype,
|
|
1436
1507
|
) @TypeOf(options.log).Error!?Result {
|
|
1437
1508
|
const side = battle.side(player);
|
|
@@ -1444,21 +1515,21 @@ fn checkFaint(
|
|
|
1444
1515
|
const foe_out = foe_fainted and findFirstAlive(foe) == 0;
|
|
1445
1516
|
const more = player_out or foe_out;
|
|
1446
1517
|
|
|
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;
|
|
1518
|
+
if (try faint(battle, player, !(more or foe_fainted), cap, options)) |r| return r;
|
|
1519
|
+
if (foe_fainted) if (try faint(battle, player.foe(), !more, false, options)) |r| return r;
|
|
1449
1520
|
|
|
1450
1521
|
assert(!side.active.volatiles.MultiHit);
|
|
1451
1522
|
assert(!foe.active.volatiles.MultiHit);
|
|
1452
1523
|
|
|
1453
1524
|
if (player_out and foe_out) {
|
|
1454
1525
|
try options.log.tie(.{});
|
|
1455
|
-
return
|
|
1526
|
+
return .Tie;
|
|
1456
1527
|
} else if (player_out) {
|
|
1457
1528
|
try options.log.win(.{player.foe()});
|
|
1458
|
-
return if (player == .P1)
|
|
1529
|
+
return if (player == .P1) .Lose else .Win;
|
|
1459
1530
|
} else if (foe_out) {
|
|
1460
1531
|
try options.log.win(.{player});
|
|
1461
|
-
return if (player == .P1)
|
|
1532
|
+
return if (player == .P1) .Win else .Lose;
|
|
1462
1533
|
}
|
|
1463
1534
|
|
|
1464
1535
|
const foe_choice: Choice.Type = if (foe_fainted) .Switch else .Pass;
|
|
@@ -1466,7 +1537,7 @@ fn checkFaint(
|
|
|
1466
1537
|
return .{ .p1 = foe_choice, .p2 = .Switch };
|
|
1467
1538
|
}
|
|
1468
1539
|
|
|
1469
|
-
fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
|
|
1540
|
+
fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytype) !?Result {
|
|
1470
1541
|
var side = battle.side(player);
|
|
1471
1542
|
var foe = battle.foe(player);
|
|
1472
1543
|
assert(side.stored().hp == 0);
|
|
@@ -1475,8 +1546,8 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
|
|
|
1475
1546
|
assert(!foe_volatiles.MultiHit);
|
|
1476
1547
|
if (foe_volatiles.Bide) {
|
|
1477
1548
|
assert(!foe_volatiles.Thrashing and !foe_volatiles.Rage);
|
|
1478
|
-
foe_volatiles.state = if (
|
|
1479
|
-
if (foe_volatiles.state != 0) return
|
|
1549
|
+
foe_volatiles.state = if (pkmn.options.mod) 0 else foe_volatiles.state & 255;
|
|
1550
|
+
if (foe_volatiles.state != 0) return .Error;
|
|
1480
1551
|
}
|
|
1481
1552
|
|
|
1482
1553
|
// Clearing these is not strictly necessary as provided the battle hasn't ended the side that
|
|
@@ -1500,7 +1571,9 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
|
|
|
1500
1571
|
}
|
|
1501
1572
|
|
|
1502
1573
|
try options.log.faint(.{ battle.active(player), done });
|
|
1503
|
-
|
|
1574
|
+
// We can only safely set capped if the faint occured as the direct result of an opponent's
|
|
1575
|
+
// move (as opposed to due to residual damage, recoil, Explosion, etc)
|
|
1576
|
+
if (cap) options.calc.capped(player);
|
|
1504
1577
|
return null;
|
|
1505
1578
|
}
|
|
1506
1579
|
|
|
@@ -1564,21 +1637,21 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
|
|
|
1564
1637
|
|
|
1565
1638
|
battle.turn += 1;
|
|
1566
1639
|
|
|
1567
|
-
if (
|
|
1640
|
+
if (pkmn.options.mod and pkmn.options.ebc and checkEBC(battle)) {
|
|
1568
1641
|
try options.log.tie(.{});
|
|
1569
|
-
return
|
|
1642
|
+
return .Tie;
|
|
1570
1643
|
}
|
|
1571
1644
|
|
|
1572
|
-
if (
|
|
1645
|
+
if (pkmn.options.mod and battle.turn >= 1000) {
|
|
1573
1646
|
try options.log.tie(.{});
|
|
1574
|
-
return
|
|
1647
|
+
return .Tie;
|
|
1575
1648
|
} else if (battle.turn >= 65535) {
|
|
1576
|
-
return
|
|
1649
|
+
return .Error;
|
|
1577
1650
|
}
|
|
1578
1651
|
|
|
1579
1652
|
try options.log.turn(.{battle.turn});
|
|
1580
1653
|
|
|
1581
|
-
return
|
|
1654
|
+
return .Default;
|
|
1582
1655
|
}
|
|
1583
1656
|
|
|
1584
1657
|
fn checkEBC(battle: anytype) bool {
|
|
@@ -1660,17 +1733,6 @@ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
|
|
|
1660
1733
|
}
|
|
1661
1734
|
}
|
|
1662
1735
|
|
|
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
1736
|
fn onBegin(
|
|
1675
1737
|
battle: anytype,
|
|
1676
1738
|
player: Player,
|
|
@@ -1679,7 +1741,6 @@ fn onBegin(
|
|
|
1679
1741
|
residual: *bool,
|
|
1680
1742
|
options: anytype,
|
|
1681
1743
|
) !void {
|
|
1682
|
-
assert(move.effect.onBegin());
|
|
1683
1744
|
return switch (move.effect) {
|
|
1684
1745
|
.Confusion => Effects.confusion(battle, player, move, options),
|
|
1685
1746
|
.Conversion => Effects.conversion(battle, player, options),
|
|
@@ -1702,7 +1763,6 @@ fn onBegin(
|
|
|
1702
1763
|
}
|
|
1703
1764
|
|
|
1704
1765
|
fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1705
|
-
assert(move.effect.onEnd());
|
|
1706
1766
|
return switch (move.effect) {
|
|
1707
1767
|
.Bide => Effects.bide(battle, player, options),
|
|
1708
1768
|
// zig fmt: off
|
|
@@ -1724,7 +1784,6 @@ fn alwaysHappens(
|
|
|
1724
1784
|
residual: *bool,
|
|
1725
1785
|
options: anytype,
|
|
1726
1786
|
) !void {
|
|
1727
|
-
assert(move.effect.alwaysHappens());
|
|
1728
1787
|
return switch (move.effect) {
|
|
1729
1788
|
.DrainHP, .DreamEater => Effects.drainHP(battle, player, options),
|
|
1730
1789
|
.Explode => Effects.explode(battle, player),
|
|
@@ -1760,8 +1819,10 @@ pub const Effects = struct {
|
|
|
1760
1819
|
|
|
1761
1820
|
side.active.volatiles.Bide = true;
|
|
1762
1821
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1822
|
+
|
|
1763
1823
|
side.active.volatiles.state = 0;
|
|
1764
1824
|
side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
1825
|
+
options.chance.observe(.attacking, player, .started);
|
|
1765
1826
|
|
|
1766
1827
|
try options.log.start(.{ battle.active(player), .Bide });
|
|
1767
1828
|
}
|
|
@@ -1823,12 +1884,14 @@ pub const Effects = struct {
|
|
|
1823
1884
|
if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
1824
1885
|
if (!try checkHit(battle, player, move, options)) return;
|
|
1825
1886
|
}
|
|
1826
|
-
try options.chance.commit(player, .hit);
|
|
1827
1887
|
}
|
|
1828
1888
|
|
|
1829
1889
|
if (foe.active.volatiles.Confusion) return;
|
|
1830
1890
|
foe.active.volatiles.Confusion = true;
|
|
1831
|
-
|
|
1891
|
+
|
|
1892
|
+
foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
1893
|
+
options.chance.observe(.confusion, player.foe(), .started);
|
|
1894
|
+
options.calc.confusion(player.foe());
|
|
1832
1895
|
|
|
1833
1896
|
try options.log.start(.{ battle.active(player.foe()), .Confusion });
|
|
1834
1897
|
}
|
|
@@ -1856,10 +1919,7 @@ pub const Effects = struct {
|
|
|
1856
1919
|
const foe_ident = battle.active(player.foe());
|
|
1857
1920
|
|
|
1858
1921
|
// 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
|
-
}
|
|
1922
|
+
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
1863
1923
|
|
|
1864
1924
|
if (volatiles.disable_move != 0) {
|
|
1865
1925
|
try options.log.fail(.{ foe_ident, .None });
|
|
@@ -1877,18 +1937,18 @@ pub const Effects = struct {
|
|
|
1877
1937
|
|
|
1878
1938
|
// Technically this is still considered simply a "miss" on the cartridge,
|
|
1879
1939
|
// but diverging from Pokémon Showdown here would mostly just be pedantic
|
|
1880
|
-
if (n == 0) {
|
|
1940
|
+
if (n == 0 or (pkmn.options.mod and err)) {
|
|
1881
1941
|
try options.log.fail(.{ foe_ident, .None });
|
|
1882
1942
|
return null;
|
|
1883
1943
|
} else if (err) {
|
|
1884
1944
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
1885
|
-
|
|
1886
|
-
return Result.Error;
|
|
1945
|
+
return .Error;
|
|
1887
1946
|
}
|
|
1888
1947
|
|
|
1889
1948
|
volatiles.disable_move =
|
|
1890
1949
|
@intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
|
|
1891
1950
|
volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
|
|
1951
|
+
options.chance.observe(.disable, player.foe(), .started);
|
|
1892
1952
|
|
|
1893
1953
|
const id = foe.active.move(volatiles.disable_move).id;
|
|
1894
1954
|
try options.log.start(.{ foe_ident, .Disable, id });
|
|
@@ -1959,7 +2019,7 @@ pub const Effects = struct {
|
|
|
1959
2019
|
if (foe.active.types.includes(move.type)) return;
|
|
1960
2020
|
if (!try Rolls.secondaryChance(battle, player, true, options)) return;
|
|
1961
2021
|
// Freeze Clause Mod
|
|
1962
|
-
if (
|
|
2022
|
+
if (pkmn.options.mod) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
1963
2023
|
|
|
1964
2024
|
foe_stored.status = Status.init(.FRZ);
|
|
1965
2025
|
// GLITCH: Hyper Beam recharging status is not cleared
|
|
@@ -1972,7 +2032,7 @@ pub const Effects = struct {
|
|
|
1972
2032
|
var side = battle.side(player);
|
|
1973
2033
|
var foe = battle.foe(player);
|
|
1974
2034
|
|
|
1975
|
-
|
|
2035
|
+
const side_stored = side.stored();
|
|
1976
2036
|
var foe_stored = foe.stored();
|
|
1977
2037
|
|
|
1978
2038
|
const player_ident = battle.active(player);
|
|
@@ -1995,22 +2055,24 @@ pub const Effects = struct {
|
|
|
1995
2055
|
if (p != player and Status.any(s.stored().status)) {
|
|
1996
2056
|
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
1997
2057
|
s.stored().status = 0;
|
|
2058
|
+
options.chance.observe(.sleep, p, .None);
|
|
1998
2059
|
} else if (showdown and s.stored().status == Status.TOX) {
|
|
1999
2060
|
s.stored().status = Status.init(.PSN);
|
|
2000
2061
|
try log.status(.{ battle.active(p), s.stored().status, .None });
|
|
2001
2062
|
}
|
|
2002
|
-
try clearVolatiles(battle, p,
|
|
2063
|
+
try clearVolatiles(battle, p, options);
|
|
2003
2064
|
}
|
|
2004
2065
|
} else {
|
|
2005
2066
|
if (Status.any(foe_stored.status)) {
|
|
2006
2067
|
if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
|
|
2007
2068
|
foe.last_selected_move = .SKIP_TURN;
|
|
2069
|
+
options.chance.observe(.sleep, player.foe(), .None);
|
|
2008
2070
|
}
|
|
2009
2071
|
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
2010
2072
|
foe_stored.status = 0;
|
|
2011
2073
|
}
|
|
2012
|
-
try clearVolatiles(battle, player,
|
|
2013
|
-
try clearVolatiles(battle, player.foe(),
|
|
2074
|
+
try clearVolatiles(battle, player, options);
|
|
2075
|
+
try clearVolatiles(battle, player.foe(), options);
|
|
2014
2076
|
}
|
|
2015
2077
|
}
|
|
2016
2078
|
|
|
@@ -2061,7 +2123,6 @@ pub const Effects = struct {
|
|
|
2061
2123
|
}
|
|
2062
2124
|
}
|
|
2063
2125
|
|
|
2064
|
-
try options.chance.commit(player, .hit);
|
|
2065
2126
|
foe.active.volatiles.LeechSeed = true;
|
|
2066
2127
|
|
|
2067
2128
|
try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
|
|
@@ -2102,12 +2163,10 @@ pub const Effects = struct {
|
|
|
2102
2163
|
if (!has_mimic) {
|
|
2103
2164
|
// Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
|
|
2104
2165
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2105
|
-
try options.chance.commit(player, .hit);
|
|
2106
2166
|
return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
2107
2167
|
}
|
|
2108
2168
|
}
|
|
2109
2169
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2110
|
-
try options.chance.commit(player, .hit);
|
|
2111
2170
|
|
|
2112
2171
|
const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
|
|
2113
2172
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
@@ -2161,7 +2220,6 @@ pub const Effects = struct {
|
|
|
2161
2220
|
if (immune) return log.immune(.{ foe_ident, .None });
|
|
2162
2221
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2163
2222
|
}
|
|
2164
|
-
try options.chance.commit(player, .hit);
|
|
2165
2223
|
|
|
2166
2224
|
foe_stored.status = Status.init(.PAR);
|
|
2167
2225
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
@@ -2229,7 +2287,6 @@ pub const Effects = struct {
|
|
|
2229
2287
|
|
|
2230
2288
|
if (move.effect == .Poison) {
|
|
2231
2289
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2232
|
-
try options.chance.commit(player, .hit);
|
|
2233
2290
|
} else {
|
|
2234
2291
|
if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
|
|
2235
2292
|
return;
|
|
@@ -2298,13 +2355,12 @@ pub const Effects = struct {
|
|
|
2298
2355
|
if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
|
|
2299
2356
|
});
|
|
2300
2357
|
}
|
|
2301
|
-
// If
|
|
2358
|
+
// If moveHit in doMove didn't return true Pokémon Showdown wouldn't be in here
|
|
2302
2359
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2303
|
-
try options.chance.commit(player, .hit);
|
|
2304
2360
|
}
|
|
2305
2361
|
|
|
2306
2362
|
// Sleep Clause Mod
|
|
2307
|
-
if (
|
|
2363
|
+
if (pkmn.options.mod) {
|
|
2308
2364
|
for (foe.pokemon) |p| {
|
|
2309
2365
|
if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
|
|
2310
2366
|
}
|
|
@@ -2312,6 +2368,8 @@ pub const Effects = struct {
|
|
|
2312
2368
|
foe.active.volatiles.Recharging = false;
|
|
2313
2369
|
|
|
2314
2370
|
foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
|
|
2371
|
+
options.chance.observe(.sleep, player.foe(), .started);
|
|
2372
|
+
|
|
2315
2373
|
const last = battle.side(player).last_selected_move;
|
|
2316
2374
|
try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
|
|
2317
2375
|
}
|
|
@@ -2353,9 +2411,7 @@ pub const Effects = struct {
|
|
|
2353
2411
|
// Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
|
|
2354
2412
|
if (showdown) {
|
|
2355
2413
|
if (battle.side(player).last_selected_move == .Teleport) return;
|
|
2356
|
-
|
|
2357
|
-
try options.chance.commit(player, .hit);
|
|
2358
|
-
}
|
|
2414
|
+
_ = try checkHit(battle, player, move, options);
|
|
2359
2415
|
battle.last_damage = 0;
|
|
2360
2416
|
} else {
|
|
2361
2417
|
try options.log.fail(.{ battle.active(player), .None });
|
|
@@ -2370,6 +2426,8 @@ pub const Effects = struct {
|
|
|
2370
2426
|
|
|
2371
2427
|
volatiles.Thrashing = true;
|
|
2372
2428
|
volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
2429
|
+
|
|
2430
|
+
options.chance.observe(.attacking, player, .started);
|
|
2373
2431
|
}
|
|
2374
2432
|
|
|
2375
2433
|
fn transform(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -2408,13 +2466,14 @@ pub const Effects = struct {
|
|
|
2408
2466
|
if (side.active.volatiles.Binding) return;
|
|
2409
2467
|
side.active.volatiles.Binding = true;
|
|
2410
2468
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2411
|
-
// (Pokémon Showdown
|
|
2469
|
+
// (Pokémon Showdown clears Recharging in onTryMove instead)
|
|
2412
2470
|
if (!showdown) {
|
|
2413
2471
|
foe.active.volatiles.Recharging = false;
|
|
2414
2472
|
} else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
|
|
2415
2473
|
|
|
2416
2474
|
side.active.volatiles.attacks =
|
|
2417
2475
|
try Rolls.distribution(battle, .Binding, player, options) - 1;
|
|
2476
|
+
options.chance.observe(.binding, player, .started);
|
|
2418
2477
|
}
|
|
2419
2478
|
|
|
2420
2479
|
fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2441,7 +2500,7 @@ pub const Effects = struct {
|
|
|
2441
2500
|
}
|
|
2442
2501
|
return log.fail(.{ ident, .None });
|
|
2443
2502
|
}
|
|
2444
|
-
|
|
2503
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
|
|
2445
2504
|
const stat = unmodifiedStats(battle, side).atk;
|
|
2446
2505
|
stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2447
2506
|
try log.boost(.{ ident, reason, n });
|
|
@@ -2461,7 +2520,7 @@ pub const Effects = struct {
|
|
|
2461
2520
|
}
|
|
2462
2521
|
return log.fail(.{ ident, .None });
|
|
2463
2522
|
}
|
|
2464
|
-
|
|
2523
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
|
|
2465
2524
|
const stat = unmodifiedStats(battle, side).def;
|
|
2466
2525
|
stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2467
2526
|
try log.boost(.{ ident, .Defense, n });
|
|
@@ -2478,7 +2537,7 @@ pub const Effects = struct {
|
|
|
2478
2537
|
}
|
|
2479
2538
|
return log.fail(.{ ident, .None });
|
|
2480
2539
|
}
|
|
2481
|
-
|
|
2540
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
|
|
2482
2541
|
const stat = unmodifiedStats(battle, side).spe;
|
|
2483
2542
|
stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2484
2543
|
try log.boost(.{ ident, .Speed, 2 });
|
|
@@ -2498,7 +2557,7 @@ pub const Effects = struct {
|
|
|
2498
2557
|
}
|
|
2499
2558
|
return log.fail(.{ ident, .None });
|
|
2500
2559
|
}
|
|
2501
|
-
|
|
2560
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
|
|
2502
2561
|
const stat = unmodifiedStats(battle, side).spc;
|
|
2503
2562
|
stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2504
2563
|
try log.boost(.{ ident, .SpecialAttack, n });
|
|
@@ -2531,7 +2590,6 @@ pub const Effects = struct {
|
|
|
2531
2590
|
} else {
|
|
2532
2591
|
// checkHit already checks for Invulnerable
|
|
2533
2592
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2534
|
-
try options.chance.commit(player, .hit);
|
|
2535
2593
|
}
|
|
2536
2594
|
|
|
2537
2595
|
var stats = &foe.active.stats;
|
|
@@ -2551,7 +2609,7 @@ pub const Effects = struct {
|
|
|
2551
2609
|
}
|
|
2552
2610
|
return log.fail(.{ foe_ident, .None });
|
|
2553
2611
|
}
|
|
2554
|
-
|
|
2612
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
|
|
2555
2613
|
const stat = unmodifiedStats(battle, foe).atk;
|
|
2556
2614
|
stats.atk = @max(1, stat * mod[0] / mod[1]);
|
|
2557
2615
|
try log.boost(.{ foe_ident, .Attack, -1 });
|
|
@@ -2569,7 +2627,7 @@ pub const Effects = struct {
|
|
|
2569
2627
|
}
|
|
2570
2628
|
return log.fail(.{ foe_ident, .None });
|
|
2571
2629
|
}
|
|
2572
|
-
|
|
2630
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
|
|
2573
2631
|
const stat = unmodifiedStats(battle, foe).def;
|
|
2574
2632
|
stats.def = @max(1, stat * mod[0] / mod[1]);
|
|
2575
2633
|
try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
|
|
@@ -2586,7 +2644,7 @@ pub const Effects = struct {
|
|
|
2586
2644
|
}
|
|
2587
2645
|
return log.fail(.{ foe_ident, .None });
|
|
2588
2646
|
}
|
|
2589
|
-
|
|
2647
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
|
|
2590
2648
|
const stat = unmodifiedStats(battle, foe).spe;
|
|
2591
2649
|
stats.spe = @max(1, stat * mod[0] / mod[1]);
|
|
2592
2650
|
try log.boost(.{ foe_ident, .Speed, -1 });
|
|
@@ -2606,7 +2664,7 @@ pub const Effects = struct {
|
|
|
2606
2664
|
}
|
|
2607
2665
|
return log.fail(.{ foe_ident, .None });
|
|
2608
2666
|
}
|
|
2609
|
-
|
|
2667
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
|
|
2610
2668
|
const stat = unmodifiedStats(battle, foe).spc;
|
|
2611
2669
|
stats.spc = @max(1, stat * mod[0] / mod[1]);
|
|
2612
2670
|
try log.boost(.{ foe_ident, .SpecialAttack, -1 });
|
|
@@ -2645,22 +2703,22 @@ fn isForced(active: anytype) bool {
|
|
|
2645
2703
|
active.volatiles.Thrashing or active.volatiles.Charging;
|
|
2646
2704
|
}
|
|
2647
2705
|
|
|
2648
|
-
fn clearVolatiles(battle: anytype, who: Player,
|
|
2706
|
+
fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
|
|
2649
2707
|
var log = options.log;
|
|
2650
2708
|
var side = battle.side(who);
|
|
2651
2709
|
var volatiles = &side.active.volatiles;
|
|
2652
2710
|
const ident = battle.active(who);
|
|
2653
2711
|
|
|
2654
|
-
options.chance.clearDurations(who, clear);
|
|
2655
|
-
|
|
2656
2712
|
if (volatiles.disable_move != 0) {
|
|
2657
2713
|
volatiles.disable_move = 0;
|
|
2658
2714
|
volatiles.disable_duration = 0;
|
|
2715
|
+
options.chance.observe(.disable, who, .None);
|
|
2659
2716
|
try log.end(.{ ident, .DisableSilent });
|
|
2660
2717
|
}
|
|
2661
2718
|
if (volatiles.Confusion) {
|
|
2662
2719
|
// volatiles.confusion is left unchanged
|
|
2663
2720
|
volatiles.Confusion = false;
|
|
2721
|
+
options.chance.observe(.confusion, who, .None);
|
|
2664
2722
|
try log.end(.{ ident, .ConfusionSilent });
|
|
2665
2723
|
}
|
|
2666
2724
|
if (volatiles.Mist) {
|
|
@@ -2695,10 +2753,25 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
|
|
|
2695
2753
|
}
|
|
2696
2754
|
}
|
|
2697
2755
|
|
|
2756
|
+
fn decrement(
|
|
2757
|
+
comptime field: chance.Action.Field,
|
|
2758
|
+
player: Player,
|
|
2759
|
+
options: anytype,
|
|
2760
|
+
n: anytype,
|
|
2761
|
+
) @TypeOf(n) {
|
|
2762
|
+
return if (options.calc.overridden(player, field)) |obs| switch (obs) {
|
|
2763
|
+
.started, .ended => 0,
|
|
2764
|
+
.continuing => if (n > 1) n - 1 else n,
|
|
2765
|
+
else => unreachable,
|
|
2766
|
+
} else n - 1;
|
|
2767
|
+
}
|
|
2768
|
+
|
|
2698
2769
|
pub const Rolls = struct {
|
|
2699
2770
|
fn speedTie(battle: anytype, options: anytype) !bool {
|
|
2700
2771
|
const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
|
|
2701
2772
|
player == .P1
|
|
2773
|
+
else if (@hasDecl(@TypeOf(battle.rng), "speedTie"))
|
|
2774
|
+
battle.rng.speedTie()
|
|
2702
2775
|
else if (showdown)
|
|
2703
2776
|
battle.rng.range(u8, 0, 2) == 0
|
|
2704
2777
|
else
|
|
@@ -2708,21 +2781,31 @@ pub const Rolls = struct {
|
|
|
2708
2781
|
return p1;
|
|
2709
2782
|
}
|
|
2710
2783
|
|
|
2711
|
-
fn criticalHit(
|
|
2784
|
+
fn criticalHit(
|
|
2785
|
+
battle: anytype,
|
|
2786
|
+
player: Player,
|
|
2787
|
+
rate: u8,
|
|
2788
|
+
spurious: bool,
|
|
2789
|
+
options: anytype,
|
|
2790
|
+
) !bool {
|
|
2712
2791
|
const crit = if (options.calc.overridden(player, .critical_hit)) |val|
|
|
2713
2792
|
val == .true
|
|
2793
|
+
else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
|
|
2794
|
+
battle.rng.criticalHit(player, rate)
|
|
2714
2795
|
else if (showdown)
|
|
2715
2796
|
battle.rng.chance(u8, rate, 256)
|
|
2716
2797
|
else
|
|
2717
2798
|
std.math.rotl(u8, battle.rng.next(), 3) < rate;
|
|
2718
2799
|
|
|
2719
|
-
try options.chance.criticalHit(player, crit, rate);
|
|
2800
|
+
if (!spurious) try options.chance.criticalHit(player, crit, rate);
|
|
2720
2801
|
return crit;
|
|
2721
2802
|
}
|
|
2722
2803
|
|
|
2723
2804
|
fn damage(battle: anytype, player: Player, options: anytype) !u8 {
|
|
2724
2805
|
const roll = if (options.calc.overridden(player, .damage)) |val|
|
|
2725
2806
|
val
|
|
2807
|
+
else if (@hasDecl(@TypeOf(battle.rng), "damage"))
|
|
2808
|
+
battle.rng.damage(player)
|
|
2726
2809
|
else roll: {
|
|
2727
2810
|
if (showdown) break :roll battle.rng.range(u8, 217, 256);
|
|
2728
2811
|
while (true) {
|
|
@@ -2736,21 +2819,27 @@ pub const Rolls = struct {
|
|
|
2736
2819
|
return roll;
|
|
2737
2820
|
}
|
|
2738
2821
|
|
|
2739
|
-
|
|
2822
|
+
const uN = if (pkmn.options.miss) u8 else u9;
|
|
2823
|
+
fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) !bool {
|
|
2824
|
+
const acc: uN = if (pkmn.options.miss) accuracy else @as(uN, accuracy) + 1;
|
|
2740
2825
|
const ok = if (options.calc.overridden(player, .hit)) |val|
|
|
2741
2826
|
val == .true
|
|
2827
|
+
else if (@hasDecl(@TypeOf(battle.rng), "hit"))
|
|
2828
|
+
battle.rng.hit(player, acc)
|
|
2742
2829
|
else if (showdown)
|
|
2743
|
-
battle.rng.chance(
|
|
2830
|
+
battle.rng.chance(uN, acc, 256)
|
|
2744
2831
|
else
|
|
2745
|
-
battle.rng.next() <
|
|
2832
|
+
battle.rng.next() < acc;
|
|
2746
2833
|
|
|
2747
|
-
options.chance.hit(ok,
|
|
2834
|
+
try options.chance.hit(player, ok, acc);
|
|
2748
2835
|
return ok;
|
|
2749
2836
|
}
|
|
2750
2837
|
|
|
2751
2838
|
fn confused(battle: anytype, player: Player, options: anytype) !bool {
|
|
2752
2839
|
const cfz = if (options.calc.overridden(player, .confused)) |val|
|
|
2753
2840
|
val == .true
|
|
2841
|
+
else if (@hasDecl(@TypeOf(battle.rng), "confused"))
|
|
2842
|
+
battle.rng.confused(player)
|
|
2754
2843
|
else if (showdown)
|
|
2755
2844
|
!battle.rng.chance(u8, 128, 256)
|
|
2756
2845
|
else
|
|
@@ -2763,6 +2852,8 @@ pub const Rolls = struct {
|
|
|
2763
2852
|
fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
|
|
2764
2853
|
const par = if (options.calc.overridden(player, .paralyzed)) |val|
|
|
2765
2854
|
val == .true
|
|
2855
|
+
else if (@hasDecl(@TypeOf(battle.rng), "paralyzed"))
|
|
2856
|
+
battle.rng.paralyzed(player)
|
|
2766
2857
|
else if (showdown)
|
|
2767
2858
|
battle.rng.chance(u8, 63, 256)
|
|
2768
2859
|
else
|
|
@@ -2775,6 +2866,8 @@ pub const Rolls = struct {
|
|
|
2775
2866
|
fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
|
|
2776
2867
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2777
2868
|
val == .true
|
|
2869
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2870
|
+
battle.rng.secondaryChance(player, 25)
|
|
2778
2871
|
else if (showdown)
|
|
2779
2872
|
battle.rng.chance(u8, 25, 256)
|
|
2780
2873
|
else
|
|
@@ -2789,6 +2882,8 @@ pub const Rolls = struct {
|
|
|
2789
2882
|
|
|
2790
2883
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2791
2884
|
val == .true
|
|
2885
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2886
|
+
battle.rng.secondaryChance(player, rate)
|
|
2792
2887
|
else if (showdown)
|
|
2793
2888
|
battle.rng.chance(u8, rate, 256)
|
|
2794
2889
|
else
|
|
@@ -2803,6 +2898,8 @@ pub const Rolls = struct {
|
|
|
2803
2898
|
|
|
2804
2899
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2805
2900
|
val == .true
|
|
2901
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2902
|
+
battle.rng.secondaryChance(player, rate)
|
|
2806
2903
|
else if (showdown)
|
|
2807
2904
|
battle.rng.chance(u8, rate, 256)
|
|
2808
2905
|
else
|
|
@@ -2815,6 +2912,8 @@ pub const Rolls = struct {
|
|
|
2815
2912
|
fn unboost(battle: anytype, player: Player, options: anytype) !bool {
|
|
2816
2913
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2817
2914
|
val == .true
|
|
2915
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2916
|
+
battle.rng.secondaryChance(player, 85)
|
|
2818
2917
|
else if (showdown)
|
|
2819
2918
|
battle.rng.chance(u8, 85, 256)
|
|
2820
2919
|
else
|
|
@@ -2827,6 +2926,8 @@ pub const Rolls = struct {
|
|
|
2827
2926
|
fn metronome(battle: anytype, player: Player, options: anytype) !Move {
|
|
2828
2927
|
const move: Move = if (options.calc.overridden(player, .metronome)) |val|
|
|
2829
2928
|
val
|
|
2929
|
+
else if (@hasDecl(@TypeOf(battle.rng), "metronome"))
|
|
2930
|
+
battle.rng.metronome(player)
|
|
2830
2931
|
else if (showdown)
|
|
2831
2932
|
Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
|
|
2832
2933
|
else move: {
|
|
@@ -2845,6 +2946,8 @@ pub const Rolls = struct {
|
|
|
2845
2946
|
fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
|
|
2846
2947
|
const power = if (options.calc.overridden(player, .psywave)) |val|
|
|
2847
2948
|
val - 1
|
|
2949
|
+
else if (@hasDecl(@TypeOf(battle.rng), "psywave"))
|
|
2950
|
+
battle.rng.psywave(player)
|
|
2848
2951
|
else if (showdown)
|
|
2849
2952
|
battle.rng.range(u8, 0, max)
|
|
2850
2953
|
else power: {
|
|
@@ -2860,8 +2963,11 @@ pub const Rolls = struct {
|
|
|
2860
2963
|
}
|
|
2861
2964
|
|
|
2862
2965
|
fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2863
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2864
|
-
@intCast(val)
|
|
2966
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2967
|
+
if (val >= 1 and val <= 7) @as(u3, @intCast(val)) else null
|
|
2968
|
+
else
|
|
2969
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
|
|
2970
|
+
battle.rng.sleepDuration(player)
|
|
2865
2971
|
else if (showdown)
|
|
2866
2972
|
battle.rng.range(u3, 1, 8)
|
|
2867
2973
|
else duration: {
|
|
@@ -2872,46 +2978,55 @@ pub const Rolls = struct {
|
|
|
2872
2978
|
};
|
|
2873
2979
|
|
|
2874
2980
|
assert(duration >= 1 and duration <= 7);
|
|
2875
|
-
options.chance.duration(
|
|
2981
|
+
options.chance.duration(player, duration);
|
|
2876
2982
|
return duration;
|
|
2877
2983
|
}
|
|
2878
2984
|
|
|
2879
2985
|
fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
|
|
2880
|
-
const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
|
|
2881
|
-
val
|
|
2986
|
+
const duration: u4 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2987
|
+
if (val >= 1 and val <= 8) val else null
|
|
2988
|
+
else
|
|
2989
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
|
|
2990
|
+
battle.rng.disableDuration(player)
|
|
2882
2991
|
else if (showdown)
|
|
2883
2992
|
battle.rng.range(u4, 1, 9)
|
|
2884
2993
|
else
|
|
2885
2994
|
@intCast((battle.rng.next() & 7) + 1);
|
|
2886
2995
|
|
|
2887
2996
|
assert(duration >= 1 and duration <= 8);
|
|
2888
|
-
options.chance.duration(
|
|
2997
|
+
options.chance.duration(player, duration);
|
|
2889
2998
|
return duration;
|
|
2890
2999
|
}
|
|
2891
3000
|
|
|
2892
|
-
fn confusionDuration(battle: anytype, player: Player,
|
|
2893
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2894
|
-
@intCast(val)
|
|
3001
|
+
fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
3002
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
3003
|
+
if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
|
|
3004
|
+
else
|
|
3005
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
|
|
3006
|
+
battle.rng.confusionDuration(player)
|
|
2895
3007
|
else if (showdown)
|
|
2896
3008
|
battle.rng.range(u3, 2, 6)
|
|
2897
3009
|
else
|
|
2898
3010
|
@intCast((battle.rng.next() & 3) + 2);
|
|
2899
3011
|
|
|
2900
3012
|
assert(duration >= 2 and duration <= 5);
|
|
2901
|
-
options.chance.duration(
|
|
3013
|
+
options.chance.duration(player, duration);
|
|
2902
3014
|
return duration;
|
|
2903
3015
|
}
|
|
2904
3016
|
|
|
2905
3017
|
fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2906
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2907
|
-
@intCast(val)
|
|
3018
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
3019
|
+
if (val >= 2 and val <= 3) @as(u3, @intCast(val)) else null
|
|
3020
|
+
else
|
|
3021
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
|
|
3022
|
+
battle.rng.attackingDuration(player)
|
|
2908
3023
|
else if (showdown)
|
|
2909
3024
|
battle.rng.range(u3, 2, 4)
|
|
2910
3025
|
else
|
|
2911
3026
|
@intCast((battle.rng.next() & 1) + 2);
|
|
2912
3027
|
|
|
2913
3028
|
assert(duration >= 2 and duration <= 3);
|
|
2914
|
-
options.chance.duration(
|
|
3029
|
+
options.chance.duration(player, duration);
|
|
2915
3030
|
return duration;
|
|
2916
3031
|
}
|
|
2917
3032
|
|
|
@@ -2924,8 +3039,11 @@ pub const Rolls = struct {
|
|
|
2924
3039
|
options: anytype,
|
|
2925
3040
|
) !u3 {
|
|
2926
3041
|
const roll = if (effect == .Binding) .duration else .multi_hit;
|
|
2927
|
-
const n: u3 = if (options.calc.overridden(player, roll)) |val|
|
|
2928
|
-
@intCast(val)
|
|
3042
|
+
const n: u3 = (if (options.calc.overridden(player, roll)) |val|
|
|
3043
|
+
if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
|
|
3044
|
+
else
|
|
3045
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "distribution"))
|
|
3046
|
+
battle.rng.distribution(player)
|
|
2929
3047
|
else if (showdown)
|
|
2930
3048
|
DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
|
|
2931
3049
|
else n: {
|
|
@@ -2935,7 +3053,7 @@ pub const Rolls = struct {
|
|
|
2935
3053
|
|
|
2936
3054
|
assert(n >= 2 and n <= 5);
|
|
2937
3055
|
if (effect == .Binding) {
|
|
2938
|
-
options.chance.duration(
|
|
3056
|
+
options.chance.duration(player, n);
|
|
2939
3057
|
} else {
|
|
2940
3058
|
try options.chance.multiHit(player, n);
|
|
2941
3059
|
}
|
|
@@ -2950,15 +3068,15 @@ pub const Rolls = struct {
|
|
|
2950
3068
|
options: anytype,
|
|
2951
3069
|
) !u4 {
|
|
2952
3070
|
// TODO: consider throwing error instead of rerolling?
|
|
2953
|
-
const
|
|
3071
|
+
const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
|
|
2954
3072
|
if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
|
|
2955
3073
|
val
|
|
2956
3074
|
else
|
|
2957
3075
|
null
|
|
2958
3076
|
else
|
|
2959
|
-
null
|
|
2960
|
-
|
|
2961
|
-
|
|
3077
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
|
|
3078
|
+
battle.rng.moveSlot(player, moves, check_pp)
|
|
3079
|
+
else slot: {
|
|
2962
3080
|
if (showdown) {
|
|
2963
3081
|
if (check_pp == 0) {
|
|
2964
3082
|
var i: usize = moves.len;
|
|
@@ -3022,11 +3140,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3022
3140
|
},
|
|
3023
3141
|
.Switch => {
|
|
3024
3142
|
const side = battle.side(player);
|
|
3025
|
-
var slot:
|
|
3143
|
+
var slot: usize = 2;
|
|
3026
3144
|
while (slot <= 6) : (slot += 1) {
|
|
3027
3145
|
const id = side.order[slot - 1];
|
|
3028
3146
|
if (id == 0 or side.pokemon[id - 1].hp == 0) continue;
|
|
3029
|
-
out[n] = .{ .type = .Switch, .data = slot };
|
|
3147
|
+
out[n] = .{ .type = .Switch, .data = @intCast(slot) };
|
|
3030
3148
|
n += 1;
|
|
3031
3149
|
}
|
|
3032
3150
|
if (n == 0) {
|
|
@@ -3038,11 +3156,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3038
3156
|
const side = battle.side(player);
|
|
3039
3157
|
const foe = battle.foe(player);
|
|
3040
3158
|
|
|
3041
|
-
|
|
3159
|
+
const active = &side.active;
|
|
3042
3160
|
const stored = side.stored();
|
|
3043
3161
|
|
|
3044
3162
|
// 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
|
|
3163
|
+
// Pokémon Showdown instead produces a list with a single move that must be chosen
|
|
3046
3164
|
//
|
|
3047
3165
|
// Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
|
|
3048
3166
|
// here when not in compatibility mode, but the engine needs to be able to differentiate
|
|
@@ -3050,18 +3168,18 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3050
3168
|
// passing due to being forced into using a move. Instead of introducing another option
|
|
3051
3169
|
// we simply repurpose Move with no move slot, even though pedantically this is not
|
|
3052
3170
|
// strictly correct as the player would not have been presented the option to move or
|
|
3053
|
-
// switch at all
|
|
3171
|
+
// switch at all
|
|
3054
3172
|
if (isForced(active)) {
|
|
3055
3173
|
out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
|
|
3056
3174
|
n += 1;
|
|
3057
3175
|
return n;
|
|
3058
3176
|
}
|
|
3059
3177
|
|
|
3060
|
-
var slot:
|
|
3178
|
+
var slot: usize = 2;
|
|
3061
3179
|
while (slot <= 6) : (slot += 1) {
|
|
3062
3180
|
const id = side.order[slot - 1];
|
|
3063
3181
|
if (id == 0 or side.pokemon[id - 1].hp == 0) continue;
|
|
3064
|
-
out[n] = .{ .type = .Switch, .data = slot };
|
|
3182
|
+
out[n] = .{ .type = .Switch, .data = @intCast(slot) };
|
|
3065
3183
|
n += 1;
|
|
3066
3184
|
}
|
|
3067
3185
|
|
|
@@ -3069,7 +3187,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3069
3187
|
// On the cartridge, all of these happen after "FIGHT" (indicating you are not
|
|
3070
3188
|
// switching) but before you are allowed to select a move. Pokémon Showdown instead
|
|
3071
3189
|
// either disables all other moves in the case of limited or requires you to select a
|
|
3072
|
-
// move normally if sleeping/frozen/bound
|
|
3190
|
+
// move normally if sleeping/frozen/bound
|
|
3073
3191
|
if (!showdown and (limited or foe.active.volatiles.Binding or
|
|
3074
3192
|
Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
|
|
3075
3193
|
{
|
|
@@ -3082,7 +3200,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3082
3200
|
// Pokémon Showdown handles Bide and Binding moves by checking if the move in question
|
|
3083
3201
|
// is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
|
|
3084
3202
|
// 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
|
|
3203
|
+
// proc-ed move in their moveset) and disabling all other moves
|
|
3086
3204
|
if (limited) {
|
|
3087
3205
|
assert(showdown);
|
|
3088
3206
|
assert(side.last_selected_move != .None);
|
|
@@ -3094,7 +3212,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3094
3212
|
const struggle =
|
|
3095
3213
|
m.id == .Bide and (m.pp == 0 or active.volatiles.disable_move == slot);
|
|
3096
3214
|
const s = if (struggle) 0 else slot;
|
|
3097
|
-
out[n] = .{ .type = .Move, .data = s };
|
|
3215
|
+
out[n] = .{ .type = .Move, .data = @intCast(s) };
|
|
3098
3216
|
n += 1;
|
|
3099
3217
|
return n;
|
|
3100
3218
|
}
|
|
@@ -3108,13 +3226,13 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3108
3226
|
if (m.id == .None) break;
|
|
3109
3227
|
if (m.pp == 0) continue;
|
|
3110
3228
|
if (active.volatiles.disable_move == slot) continue;
|
|
3111
|
-
out[n] = .{ .type = .Move, .data = slot };
|
|
3229
|
+
out[n] = .{ .type = .Move, .data = @intCast(slot) };
|
|
3112
3230
|
n += 1;
|
|
3113
3231
|
}
|
|
3114
3232
|
// Struggle (Pokémon Showdown would use 'move 1' here)
|
|
3115
3233
|
if (n == before) {
|
|
3116
3234
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
3117
|
-
if (!
|
|
3235
|
+
if (!pkmn.options.mod) {
|
|
3118
3236
|
while (slot <= 4) : (slot += 1) {
|
|
3119
3237
|
if (active.moves[slot - 1].pp != 0) return n;
|
|
3120
3238
|
}
|