@pkmn/engine 0.1.0-dev.545e0ef3 → 0.1.0-dev.550c3889
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/README.md +50 -22
- package/build/pkg/addon.d.ts +7 -0
- package/build/pkg/addon.js +111 -0
- package/build/pkg/addon.js.map +1 -0
- package/build/pkg/data.d.ts +1 -2
- package/build/pkg/data.js +3 -5
- package/build/pkg/data.js.map +1 -1
- package/build/pkg/gen1.d.ts +3 -3
- package/build/pkg/gen1.js +11 -13
- package/build/pkg/gen1.js.map +1 -1
- package/build/pkg/index.d.ts +11 -10
- package/build/pkg/index.js.map +1 -1
- package/build/test/display.js +383 -0
- package/build/test/display.js.map +1 -0
- package/build/tools/debug.js +133 -0
- package/build/tools/debug.js.map +1 -0
- package/build.zig +24 -34
- package/package.json +40 -29
- package/src/bin/install-pkmn-engine +60 -33
- package/src/bin/pkmn-debug +62 -0
- package/src/lib/binding/node.zig +5 -5
- package/src/lib/binding/wasm.zig +3 -3
- package/src/lib/common/debug.zig +1 -0
- package/src/lib/common/options.zig +3 -3
- package/src/lib/common/protocol.zig +38 -38
- package/src/lib/gen1/README.md +28 -13
- package/src/lib/gen1/data.zig +6 -6
- package/src/lib/gen1/helpers.zig +10 -7
- package/src/lib/gen1/mechanics.zig +333 -276
- package/src/lib/gen2/data.zig +4 -4
- package/src/lib/pkmn.zig +7 -7
- package/src/test/display/pkmn.html.tmpl +368 -0
|
@@ -56,30 +56,34 @@ const BOOSTS = &[_][2]u8{
|
|
|
56
56
|
|
|
57
57
|
const MAX_STAT_VALUE = 999;
|
|
58
58
|
|
|
59
|
+
const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
|
|
60
|
+
|
|
59
61
|
pub fn update(battle: anytype, c1: Choice, c2: Choice, log: anytype) !Result {
|
|
60
62
|
assert(c1.type != .Pass or c2.type != .Pass or battle.turn == 0);
|
|
61
63
|
if (battle.turn == 0) return start(battle, log);
|
|
62
64
|
|
|
65
|
+
var s1 = false;
|
|
66
|
+
var s2 = false;
|
|
63
67
|
var f1: ?Move = null;
|
|
64
68
|
var f2: ?Move = null;
|
|
65
69
|
|
|
66
|
-
if (selectMove(battle, .P1, c1, c2, &f1)) |r| return r;
|
|
67
|
-
if (selectMove(battle, .P2, c2, c1, &f2)) |r| return r;
|
|
70
|
+
if (selectMove(battle, .P1, c1, c2, &s1, &f1)) |r| return r;
|
|
71
|
+
if (selectMove(battle, .P2, c2, c1, &s2, &f2)) |r| return r;
|
|
72
|
+
|
|
73
|
+
var p1 = battle.side(.P1);
|
|
74
|
+
var p2 = battle.side(.P2);
|
|
75
|
+
|
|
76
|
+
var r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
|
|
77
|
+
var r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
|
|
68
78
|
|
|
69
79
|
if (turnOrder(battle, c1, c2) == .P1) {
|
|
70
|
-
if (try doTurn(battle, .P1, c1, f1, .P2, c2, f2, log)) |r| return r;
|
|
80
|
+
if (try doTurn(battle, .P1, c1, r1, s1, f1, .P2, c2, r2, s2, f2, log)) |r| return r;
|
|
71
81
|
} else {
|
|
72
|
-
if (try doTurn(battle, .P2, c2, f2, .P1, c1, f1, log)) |r| return r;
|
|
82
|
+
if (try doTurn(battle, .P2, c2, r2, s2, f2, .P1, c1, r1, s1, f1, log)) |r| return r;
|
|
73
83
|
}
|
|
74
84
|
|
|
75
|
-
|
|
76
|
-
if (
|
|
77
|
-
p1.active.volatiles.Binding = false;
|
|
78
|
-
}
|
|
79
|
-
var p2 = battle.side(.P2);
|
|
80
|
-
if (p2.active.volatiles.attacks == 0) {
|
|
81
|
-
p2.active.volatiles.Binding = false;
|
|
82
|
-
}
|
|
85
|
+
if (p1.active.volatiles.attacks == 0) p1.active.volatiles.Binding = false;
|
|
86
|
+
if (p2.active.volatiles.attacks == 0) p2.active.volatiles.Binding = false;
|
|
83
87
|
|
|
84
88
|
return endTurn(battle, log);
|
|
85
89
|
}
|
|
@@ -112,6 +116,7 @@ fn selectMove(
|
|
|
112
116
|
player: Player,
|
|
113
117
|
choice: Choice,
|
|
114
118
|
foe_choice: Choice,
|
|
119
|
+
skip_turn: *bool,
|
|
115
120
|
from: *?Move,
|
|
116
121
|
) ?Result {
|
|
117
122
|
if (choice.type == .Pass) return null;
|
|
@@ -127,14 +132,20 @@ fn selectMove(
|
|
|
127
132
|
if (volatiles.Recharging) return null;
|
|
128
133
|
if (volatiles.Rage) {
|
|
129
134
|
from.* = side.last_used_move;
|
|
130
|
-
if (showdown)
|
|
135
|
+
if (showdown) {
|
|
136
|
+
if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
|
|
137
|
+
saveMove(battle, player, null);
|
|
138
|
+
}
|
|
131
139
|
return null;
|
|
132
140
|
}
|
|
133
141
|
// Pokémon Showdown removes Flinch at the end-of-turn in its residual handler
|
|
134
142
|
if (!showdown) volatiles.Flinch = false;
|
|
135
143
|
if (volatiles.Thrashing or volatiles.Charging) {
|
|
136
144
|
from.* = side.last_selected_move;
|
|
137
|
-
if (showdown)
|
|
145
|
+
if (showdown) {
|
|
146
|
+
if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
|
|
147
|
+
saveMove(battle, player, null);
|
|
148
|
+
}
|
|
138
149
|
return null;
|
|
139
150
|
}
|
|
140
151
|
|
|
@@ -144,7 +155,10 @@ fn selectMove(
|
|
|
144
155
|
// pre-move select
|
|
145
156
|
if (Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP) or volatiles.Bide) {
|
|
146
157
|
assert(showdown or choice.data == 0);
|
|
147
|
-
if (showdown)
|
|
158
|
+
if (showdown) {
|
|
159
|
+
if (volatiles.Bide and battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
|
|
160
|
+
saveMove(battle, player, choice);
|
|
161
|
+
}
|
|
148
162
|
return null;
|
|
149
163
|
}
|
|
150
164
|
if (volatiles.Binding) {
|
|
@@ -175,6 +189,7 @@ fn selectMove(
|
|
|
175
189
|
}
|
|
176
190
|
|
|
177
191
|
if (battle.foe(player).active.volatiles.Binding) {
|
|
192
|
+
skip_turn.* = true;
|
|
178
193
|
if (showdown) {
|
|
179
194
|
saveMove(battle, player, choice);
|
|
180
195
|
} else {
|
|
@@ -185,6 +200,7 @@ fn selectMove(
|
|
|
185
200
|
}
|
|
186
201
|
|
|
187
202
|
// move select
|
|
203
|
+
volatiles.state = 0;
|
|
188
204
|
if (choice.data == 0) {
|
|
189
205
|
const struggle = ok: {
|
|
190
206
|
for (side.active.moves, 0..) |move, i| {
|
|
@@ -301,11 +317,7 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
|
|
|
301
317
|
// Pokémon Showdown's beforeTurnCallback shenanigans
|
|
302
318
|
if (showdown and m1 == .Counter and m2 == .Counter) battle.rng.advance(1);
|
|
303
319
|
|
|
304
|
-
const p1 =
|
|
305
|
-
battle.rng.range(u8, 0, 2) == 0
|
|
306
|
-
else
|
|
307
|
-
battle.rng.next() < Gen12.percent(50) + 1;
|
|
308
|
-
|
|
320
|
+
const p1 = Rolls.speedTie(battle);
|
|
309
321
|
if (!showdown) return if (p1) .P1 else .P2;
|
|
310
322
|
|
|
311
323
|
// Pokémon Showdown's "lockedmove" volatile's onBeforeTurn uses BattleQueue#changeAction,
|
|
@@ -315,7 +327,7 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
|
|
|
315
327
|
const t1 = battle.side(.P1).active.volatiles.Thrashing;
|
|
316
328
|
const t2 = battle.side(.P2).active.volatiles.Thrashing;
|
|
317
329
|
// If *both* sides are thrashing it really should be another speed tie, but we've patched
|
|
318
|
-
// that out and enforce host ordering of events, so P1 just goes first
|
|
330
|
+
// that out and enforce host ordering of events, so P1 just goes first regardless of who
|
|
319
331
|
// won the original coin flip
|
|
320
332
|
if (t1 and t2) return .P1;
|
|
321
333
|
return if (p1) if (t1 and !t2) .P2 else .P1 else if (t2 and !t1) .P1 else .P2;
|
|
@@ -328,9 +340,13 @@ fn doTurn(
|
|
|
328
340
|
battle: anytype,
|
|
329
341
|
player: Player,
|
|
330
342
|
player_choice: Choice,
|
|
343
|
+
player_rewrap: bool,
|
|
344
|
+
player_skip: bool,
|
|
331
345
|
player_from: ?Move,
|
|
332
346
|
foe_player: Player,
|
|
333
347
|
foe_choice: Choice,
|
|
348
|
+
foe_rewrap: bool,
|
|
349
|
+
foe_skip: bool,
|
|
334
350
|
foe_from: ?Move,
|
|
335
351
|
log: anytype,
|
|
336
352
|
) !?Result {
|
|
@@ -338,7 +354,16 @@ fn doTurn(
|
|
|
338
354
|
|
|
339
355
|
var residual = true;
|
|
340
356
|
var replace = battle.side(player).stored().hp == 0;
|
|
341
|
-
if (try executeMove(
|
|
357
|
+
if (try executeMove(
|
|
358
|
+
battle,
|
|
359
|
+
player,
|
|
360
|
+
player_choice,
|
|
361
|
+
player_rewrap,
|
|
362
|
+
player_skip,
|
|
363
|
+
player_from,
|
|
364
|
+
&residual,
|
|
365
|
+
log,
|
|
366
|
+
)) |r| return r;
|
|
342
367
|
if (!replace) {
|
|
343
368
|
if (player_choice.type != .Switch) {
|
|
344
369
|
if (try checkFaint(battle, foe_player, log)) |r| return r;
|
|
@@ -349,7 +374,16 @@ fn doTurn(
|
|
|
349
374
|
|
|
350
375
|
residual = true;
|
|
351
376
|
replace = battle.side(foe_player).stored().hp == 0;
|
|
352
|
-
if (try executeMove(
|
|
377
|
+
if (try executeMove(
|
|
378
|
+
battle,
|
|
379
|
+
foe_player,
|
|
380
|
+
foe_choice,
|
|
381
|
+
foe_rewrap,
|
|
382
|
+
foe_skip,
|
|
383
|
+
foe_from,
|
|
384
|
+
&residual,
|
|
385
|
+
log,
|
|
386
|
+
)) |r| return r;
|
|
353
387
|
if (!replace) {
|
|
354
388
|
if (foe_choice.type != .Switch) {
|
|
355
389
|
if (try checkFaint(battle, player, log)) |r| return r;
|
|
@@ -372,6 +406,8 @@ fn executeMove(
|
|
|
372
406
|
battle: anytype,
|
|
373
407
|
player: Player,
|
|
374
408
|
choice: Choice,
|
|
409
|
+
rewrap: bool,
|
|
410
|
+
skip: bool,
|
|
375
411
|
from: ?Move,
|
|
376
412
|
residual: *bool,
|
|
377
413
|
log: anytype,
|
|
@@ -383,9 +419,10 @@ fn executeMove(
|
|
|
383
419
|
return null;
|
|
384
420
|
}
|
|
385
421
|
|
|
422
|
+
// This is the correct place to check for SKIP_TURN and abort early, however since Pokémon
|
|
423
|
+
// Showdown overwrites the SKIP_TURN sentinel with its botched move select we need to add an
|
|
424
|
+
// additional skip boolean to accomplish the same thing in the Binding check of BeforeMove
|
|
386
425
|
if (side.last_selected_move == .SKIP_TURN) {
|
|
387
|
-
// Pokémon Showdown overwrites the SKIP_TURN sentinel with its botched move select,
|
|
388
|
-
// Binding instead gets handled in beforeMove after sleep and freeze
|
|
389
426
|
assert(!showdown);
|
|
390
427
|
if (battle.foe(player).active.volatiles.Binding) {
|
|
391
428
|
try log.cant(battle.active(player), .Bound);
|
|
@@ -435,7 +472,7 @@ fn executeMove(
|
|
|
435
472
|
|
|
436
473
|
var skip_can = false;
|
|
437
474
|
var skip_pp = false;
|
|
438
|
-
switch (try beforeMove(battle, player, from, residual, log)) {
|
|
475
|
+
switch (try beforeMove(battle, player, skip, from, residual, log)) {
|
|
439
476
|
.done => return null,
|
|
440
477
|
.skip_can => skip_can = true,
|
|
441
478
|
.skip_pp => skip_pp = true,
|
|
@@ -447,14 +484,15 @@ fn executeMove(
|
|
|
447
484
|
return null;
|
|
448
485
|
}
|
|
449
486
|
|
|
450
|
-
return doMove(battle, player, mslot, auto, residual, log);
|
|
487
|
+
return doMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
451
488
|
}
|
|
452
489
|
|
|
453
|
-
const BeforeMove =
|
|
490
|
+
const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
|
|
454
491
|
|
|
455
492
|
fn beforeMove(
|
|
456
493
|
battle: anytype,
|
|
457
494
|
player: Player,
|
|
495
|
+
skip: bool,
|
|
458
496
|
from: ?Move,
|
|
459
497
|
residual: *bool,
|
|
460
498
|
log: anytype,
|
|
@@ -486,13 +524,15 @@ fn beforeMove(
|
|
|
486
524
|
return .done;
|
|
487
525
|
}
|
|
488
526
|
|
|
489
|
-
if (foe.active.volatiles.Binding) {
|
|
527
|
+
if (skip or foe.active.volatiles.Binding) {
|
|
490
528
|
try log.cant(ident, .Bound);
|
|
491
529
|
return .done;
|
|
492
530
|
}
|
|
493
531
|
|
|
494
|
-
if (
|
|
495
|
-
|
|
532
|
+
if (volatiles.Flinch) {
|
|
533
|
+
// Pokémon Showdown doesn't clear Flinch until its imaginary "residual" phase, meaning
|
|
534
|
+
// Pokémon can sometimes flinch multiple times from the same original hit
|
|
535
|
+
if (!showdown) volatiles.Flinch = false;
|
|
496
536
|
try log.cant(ident, .Flinch);
|
|
497
537
|
return .done;
|
|
498
538
|
}
|
|
@@ -513,13 +553,6 @@ fn beforeMove(
|
|
|
513
553
|
// Pokémon Showdown's disable condition has a single onBeforeMove handler
|
|
514
554
|
if (showdown and try disabled(side, ident, log)) return .done;
|
|
515
555
|
|
|
516
|
-
// Pokémon Showdown checks for Flinch *after* instead of before
|
|
517
|
-
if (showdown and volatiles.Flinch) {
|
|
518
|
-
volatiles.Flinch = false;
|
|
519
|
-
try log.cant(ident, .Flinch);
|
|
520
|
-
return .done;
|
|
521
|
-
}
|
|
522
|
-
|
|
523
556
|
// This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
|
|
524
557
|
// before the side had a selected a move - we simply need to assume this leads to a desync
|
|
525
558
|
if (side.last_selected_move == .None) {
|
|
@@ -537,12 +570,7 @@ fn beforeMove(
|
|
|
537
570
|
} else {
|
|
538
571
|
try log.activate(ident, .Confusion);
|
|
539
572
|
|
|
540
|
-
|
|
541
|
-
!battle.rng.chance(u8, 128, 256)
|
|
542
|
-
else
|
|
543
|
-
battle.rng.next() >= Gen12.percent(50) + 1;
|
|
544
|
-
|
|
545
|
-
if (confused) {
|
|
573
|
+
if (Rolls.confused(battle)) {
|
|
546
574
|
assert(!volatiles.MultiHit);
|
|
547
575
|
if (!volatiles.Rage) volatiles.state = 0;
|
|
548
576
|
volatiles.Bide = false;
|
|
@@ -568,8 +596,12 @@ fn beforeMove(
|
|
|
568
596
|
player
|
|
569
597
|
else
|
|
570
598
|
player.foe();
|
|
599
|
+
|
|
600
|
+
const uncapped = battle.last_damage;
|
|
571
601
|
// Skipping adjustDamage / randomizeDamage / checkHit
|
|
572
602
|
_ = try applyDamage(battle, player, target, .Confusion, log);
|
|
603
|
+
// Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
|
|
604
|
+
if (showdown) battle.last_damage = uncapped;
|
|
573
605
|
|
|
574
606
|
return .done;
|
|
575
607
|
}
|
|
@@ -578,22 +610,15 @@ fn beforeMove(
|
|
|
578
610
|
|
|
579
611
|
if (!showdown and try disabled(side, ident, log)) return .done;
|
|
580
612
|
|
|
581
|
-
if (Status.is(stored.status, .PAR)) {
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
volatiles.Thrashing = false;
|
|
591
|
-
volatiles.Charging = false;
|
|
592
|
-
volatiles.Binding = false;
|
|
593
|
-
// GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
|
|
594
|
-
try log.cant(ident, .Paralysis);
|
|
595
|
-
return .done;
|
|
596
|
-
}
|
|
613
|
+
if (Status.is(stored.status, .PAR) and Rolls.paralyzed(battle)) {
|
|
614
|
+
if (!volatiles.Rage) volatiles.state = 0;
|
|
615
|
+
volatiles.Bide = false;
|
|
616
|
+
volatiles.Thrashing = false;
|
|
617
|
+
volatiles.Charging = false;
|
|
618
|
+
volatiles.Binding = false;
|
|
619
|
+
// GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
|
|
620
|
+
try log.cant(ident, .Paralysis);
|
|
621
|
+
return .done;
|
|
597
622
|
}
|
|
598
623
|
|
|
599
624
|
if (volatiles.Bide) {
|
|
@@ -620,8 +645,9 @@ fn beforeMove(
|
|
|
620
645
|
return .done;
|
|
621
646
|
}
|
|
622
647
|
|
|
648
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
623
649
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
624
|
-
if (foe.stored().hp > 0) try buildRage(battle, player.foe(), log);
|
|
650
|
+
if (foe.stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
|
|
625
651
|
|
|
626
652
|
// For reasons passing understanding, Pokémon Showdown still inflicts residual damage to
|
|
627
653
|
// Bide's user even if the above damage has caused the foe to faint. It's simpler to always
|
|
@@ -642,8 +668,13 @@ fn beforeMove(
|
|
|
642
668
|
try log.start(battle.active(player), .ConfusionSilent);
|
|
643
669
|
}
|
|
644
670
|
try log.move(ident, side.last_selected_move, battle.active(player.foe()), from);
|
|
645
|
-
if (showdown
|
|
646
|
-
|
|
671
|
+
if (showdown) {
|
|
672
|
+
// This shouldn't actually set last_used_move, but Pokémon Showdown sets last
|
|
673
|
+
// used in useMove and doesn't have the notion of skipping canMove semantics
|
|
674
|
+
side.last_used_move = side.last_selected_move;
|
|
675
|
+
if (handleThrashing(battle, active)) {
|
|
676
|
+
try log.start(battle.active(player), .ConfusionSilent);
|
|
677
|
+
}
|
|
647
678
|
}
|
|
648
679
|
return .skip_can;
|
|
649
680
|
}
|
|
@@ -658,7 +689,11 @@ fn beforeMove(
|
|
|
658
689
|
side.last_selected_move,
|
|
659
690
|
);
|
|
660
691
|
if (showdown or battle.last_damage != 0) {
|
|
692
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
661
693
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
694
|
+
if (battle.foe(player).stored().hp > 0 and !sub) {
|
|
695
|
+
try buildRage(battle, player.foe(), log);
|
|
696
|
+
}
|
|
662
697
|
}
|
|
663
698
|
return .done;
|
|
664
699
|
}
|
|
@@ -716,7 +751,7 @@ fn canMove(
|
|
|
716
751
|
Effects.thrashing(battle, player);
|
|
717
752
|
} else if (!showdown and move.effect == .Binding) {
|
|
718
753
|
// Pokémon Showdown handles this after hit/miss checks and damage calculation
|
|
719
|
-
Effects.binding(battle, player);
|
|
754
|
+
Effects.binding(battle, player, false);
|
|
720
755
|
}
|
|
721
756
|
|
|
722
757
|
return true;
|
|
@@ -757,6 +792,7 @@ fn doMove(
|
|
|
757
792
|
battle: anytype,
|
|
758
793
|
player: Player,
|
|
759
794
|
mslot: u4,
|
|
795
|
+
rewrap: bool,
|
|
760
796
|
auto: bool,
|
|
761
797
|
residual: *bool,
|
|
762
798
|
log: anytype,
|
|
@@ -787,12 +823,15 @@ fn doMove(
|
|
|
787
823
|
(@enumToInt(move.type.effectiveness(foe.active.types.type1)) == 0 or
|
|
788
824
|
@enumToInt(move.type.effectiveness(foe.active.types.type2)) == 0);
|
|
789
825
|
if (immune and move.effect != .Binding) break :miss true;
|
|
790
|
-
if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe)
|
|
826
|
+
if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) {
|
|
827
|
+
battle.last_damage = 0;
|
|
828
|
+
break :miss true;
|
|
829
|
+
}
|
|
791
830
|
break :miss move.target != .Self and !moveHit(battle, player, move, &immune, &mist);
|
|
792
831
|
};
|
|
793
832
|
assert(!immune or miss or (showdown and move.effect == .Binding));
|
|
794
833
|
|
|
795
|
-
var late = showdown;
|
|
834
|
+
var late = showdown and move.effect != .Explode;
|
|
796
835
|
const skip = status or immune;
|
|
797
836
|
if ((!showdown or (!skip or counter)) and !miss) blk: {
|
|
798
837
|
if (showdown and move.effect.isMulti()) {
|
|
@@ -845,9 +884,9 @@ fn doMove(
|
|
|
845
884
|
|
|
846
885
|
if (!showdown or !miss) {
|
|
847
886
|
if (move.effect == .MirrorMove) {
|
|
848
|
-
return mirrorMove(battle, player, mslot, auto, residual, log);
|
|
887
|
+
return mirrorMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
849
888
|
} else if (move.effect == .Metronome) {
|
|
850
|
-
return metronome(battle, player, mslot, auto, residual, log);
|
|
889
|
+
return metronome(battle, player, mslot, rewrap, auto, residual, log);
|
|
851
890
|
} else if (move.effect.onEnd()) {
|
|
852
891
|
try onEnd(battle, player, move, log);
|
|
853
892
|
return null;
|
|
@@ -862,7 +901,7 @@ fn doMove(
|
|
|
862
901
|
move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
|
|
863
902
|
if (ohko) {
|
|
864
903
|
try log.immune(foe_ident, .OHKO);
|
|
865
|
-
} else if (immune and !invulnerable) {
|
|
904
|
+
} else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
|
|
866
905
|
try log.immune(foe_ident, .None);
|
|
867
906
|
} else if (mist) {
|
|
868
907
|
if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
|
|
@@ -894,6 +933,9 @@ fn doMove(
|
|
|
894
933
|
hits = side.active.volatiles.attacks;
|
|
895
934
|
}
|
|
896
935
|
|
|
936
|
+
// Pokémon Showdown only builds Rage for Disable/Explosion (hit/miss) when attacking into a sub
|
|
937
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
938
|
+
|
|
897
939
|
var nullified = false;
|
|
898
940
|
var hit: u4 = 0;
|
|
899
941
|
while (hit < hits) {
|
|
@@ -909,7 +951,7 @@ fn doMove(
|
|
|
909
951
|
if (hit == 0 and ohko) try log.ohko();
|
|
910
952
|
hit += 1;
|
|
911
953
|
if (foe.stored().hp == 0) break;
|
|
912
|
-
if (!late) try buildRage(battle, player.foe(), log);
|
|
954
|
+
if (!late and (!sub or move.effect == .Explode)) try buildRage(battle, player.foe(), log);
|
|
913
955
|
// If the substitute breaks during a multi-hit attack, the attack ends
|
|
914
956
|
if (nullified) break;
|
|
915
957
|
}
|
|
@@ -925,7 +967,7 @@ fn doMove(
|
|
|
925
967
|
} else if (showdown) {
|
|
926
968
|
// This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
|
|
927
969
|
if (move.effect == .Binding) {
|
|
928
|
-
Effects.binding(battle, player);
|
|
970
|
+
Effects.binding(battle, player, rewrap);
|
|
929
971
|
if (immune) {
|
|
930
972
|
battle.last_damage = 0;
|
|
931
973
|
// Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
|
|
@@ -946,8 +988,8 @@ fn doMove(
|
|
|
946
988
|
|
|
947
989
|
if (foe.stored().hp == 0) return null;
|
|
948
990
|
|
|
949
|
-
// Pokémon Showdown builds Rage at the wrong time for non-MultiHit
|
|
950
|
-
if (late and move.effect != .Disable) try buildRage(battle, player.foe(), log);
|
|
991
|
+
// Pokémon Showdown builds Rage at the wrong time for non-MultiHit move
|
|
992
|
+
if (late and !sub and move.effect != .Disable) try buildRage(battle, player.foe(), log);
|
|
951
993
|
|
|
952
994
|
if (!move.effect.isSpecial()) {
|
|
953
995
|
// On the cartridge Rage is not considered to be "special" and thus gets executed for a
|
|
@@ -980,8 +1022,7 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
|
|
|
980
1022
|
chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
|
|
981
1023
|
chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
|
|
982
1024
|
|
|
983
|
-
|
|
984
|
-
return std.math.rotl(u8, battle.rng.next(), 3) < chance;
|
|
1025
|
+
return Rolls.criticalHit(battle, chance);
|
|
985
1026
|
}
|
|
986
1027
|
|
|
987
1028
|
fn calcDamage(
|
|
@@ -1085,50 +1126,36 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
|
1085
1126
|
|
|
1086
1127
|
fn randomizeDamage(battle: anytype) void {
|
|
1087
1128
|
if (battle.last_damage <= 1) return;
|
|
1088
|
-
|
|
1089
|
-
const random = if (showdown)
|
|
1090
|
-
battle.rng.range(u8, 217, 256)
|
|
1091
|
-
else loop: {
|
|
1092
|
-
while (true) {
|
|
1093
|
-
const r = std.math.rotr(u8, battle.rng.next(), 1);
|
|
1094
|
-
if (r >= 217) break :loop r;
|
|
1095
|
-
}
|
|
1096
|
-
};
|
|
1097
|
-
|
|
1129
|
+
const random = Rolls.damage(battle);
|
|
1098
1130
|
battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
|
|
1099
1131
|
}
|
|
1100
1132
|
|
|
1101
1133
|
fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
|
|
1102
1134
|
const side = battle.side(player);
|
|
1135
|
+
const foe = battle.foe(player);
|
|
1103
1136
|
|
|
1104
1137
|
if (!try checkHit(battle, player, move, log)) return null;
|
|
1105
1138
|
|
|
1106
1139
|
battle.last_damage = switch (side.last_selected_move) {
|
|
1107
|
-
.SuperFang => @max(
|
|
1140
|
+
.SuperFang => @max(foe.stored().hp / 2, 1),
|
|
1108
1141
|
.SeismicToss, .NightShade => side.stored().level,
|
|
1109
1142
|
.SonicBoom => 20,
|
|
1110
1143
|
.DragonRage => 40,
|
|
1111
1144
|
// GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
|
|
1112
1145
|
.Psywave => power: {
|
|
1113
1146
|
const max = @intCast(u8, @as(u16, side.stored().level) * 3 / 2);
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
// GLITCH: Psywave infinite glitch loop
|
|
1118
|
-
if (max <= 1) return Result.Error;
|
|
1119
|
-
while (true) {
|
|
1120
|
-
const r = battle.rng.next();
|
|
1121
|
-
if (r < max) break :power r;
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1147
|
+
// GLITCH: Psywave infinite glitch loop
|
|
1148
|
+
if (!showdown and max <= 1) return Result.Error;
|
|
1149
|
+
break :power Rolls.psywave(battle, max);
|
|
1124
1150
|
},
|
|
1125
1151
|
else => unreachable,
|
|
1126
1152
|
};
|
|
1127
1153
|
|
|
1128
1154
|
if (battle.last_damage == 0) return if (showdown) null else Result.Error;
|
|
1129
1155
|
|
|
1156
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1130
1157
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
1131
|
-
if (battle.foe(player).stored().hp > 0) try buildRage(battle, player.foe(), log);
|
|
1158
|
+
if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
|
|
1132
1159
|
|
|
1133
1160
|
return null;
|
|
1134
1161
|
}
|
|
@@ -1177,7 +1204,9 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
|
|
|
1177
1204
|
// Pokémon Showdown calls checkHit before Counter
|
|
1178
1205
|
if (!showdown and !try checkHit(battle, player, move, log)) return null;
|
|
1179
1206
|
|
|
1207
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1180
1208
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
1209
|
+
if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
|
|
1181
1210
|
return null;
|
|
1182
1211
|
}
|
|
1183
1212
|
|
|
@@ -1221,6 +1250,7 @@ fn mirrorMove(
|
|
|
1221
1250
|
battle: anytype,
|
|
1222
1251
|
player: Player,
|
|
1223
1252
|
mslot: u4,
|
|
1253
|
+
rewrap: bool,
|
|
1224
1254
|
auto: bool,
|
|
1225
1255
|
residual: *bool,
|
|
1226
1256
|
log: anytype,
|
|
@@ -1238,36 +1268,25 @@ fn mirrorMove(
|
|
|
1238
1268
|
incrementPP(side, mslot);
|
|
1239
1269
|
|
|
1240
1270
|
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
|
|
1241
|
-
return doMove(battle, player, mslot, auto, residual, log);
|
|
1271
|
+
return doMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
1242
1272
|
}
|
|
1243
1273
|
|
|
1244
1274
|
fn metronome(
|
|
1245
1275
|
battle: anytype,
|
|
1246
1276
|
player: Player,
|
|
1247
1277
|
mslot: u4,
|
|
1278
|
+
rewrap: bool,
|
|
1248
1279
|
auto: bool,
|
|
1249
1280
|
residual: *bool,
|
|
1250
1281
|
log: anytype,
|
|
1251
1282
|
) !?Result {
|
|
1252
1283
|
var side = battle.side(player);
|
|
1253
1284
|
|
|
1254
|
-
side.last_selected_move =
|
|
1255
|
-
const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
|
|
1256
|
-
const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
|
|
1257
|
-
break :blk @intToEnum(Move, r + mod);
|
|
1258
|
-
} else loop: {
|
|
1259
|
-
while (true) {
|
|
1260
|
-
const r = battle.rng.next();
|
|
1261
|
-
if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
|
|
1262
|
-
if (r >= @enumToInt(Move.Struggle)) continue;
|
|
1263
|
-
break :loop @intToEnum(Move, r);
|
|
1264
|
-
}
|
|
1265
|
-
};
|
|
1266
|
-
|
|
1285
|
+
side.last_selected_move = Rolls.metronome(battle);
|
|
1267
1286
|
incrementPP(side, mslot);
|
|
1268
1287
|
|
|
1269
1288
|
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
|
|
1270
|
-
return doMove(battle, player, mslot, auto, residual, log);
|
|
1289
|
+
return doMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
1271
1290
|
}
|
|
1272
1291
|
|
|
1273
1292
|
fn checkHit(battle: anytype, player: Player, move: Move.Data, log: anytype) !bool {
|
|
@@ -1321,23 +1340,24 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
|
|
|
1321
1340
|
}
|
|
1322
1341
|
|
|
1323
1342
|
// GLITCH: Thrash / Petal Dance / Rage get their accuracy overwritten on subsequent hits
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1343
|
+
const state = side.active.volatiles.state;
|
|
1344
|
+
var overwrite = move.effect == .Rage or move.effect == .Thrashing;
|
|
1345
|
+
const overwritten = overwrite and state > 0;
|
|
1346
|
+
assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
|
|
1347
|
+
|
|
1348
|
+
var accuracy = if (overwritten) state else @as(u16, Gen12.percent(move.accuracy));
|
|
1328
1349
|
var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
|
|
1329
1350
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1330
1351
|
boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
|
|
1331
1352
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1332
1353
|
accuracy = @min(255, @max(1, accuracy));
|
|
1333
1354
|
|
|
1334
|
-
|
|
1355
|
+
// Pokémon Showdown only overwrites if the volatile is present
|
|
1356
|
+
if (showdown) overwrite = side.active.volatiles.Rage or side.active.volatiles.Thrashing;
|
|
1357
|
+
if (overwrite) side.active.volatiles.state = accuracy;
|
|
1335
1358
|
|
|
1336
1359
|
// GLITCH: max accuracy is 255 so 1/256 chance of miss
|
|
1337
|
-
break :miss
|
|
1338
|
-
!battle.rng.chance(u8, @intCast(u8, accuracy), 256)
|
|
1339
|
-
else
|
|
1340
|
-
battle.rng.next() >= accuracy;
|
|
1360
|
+
break :miss !Rolls.hit(battle, accuracy);
|
|
1341
1361
|
};
|
|
1342
1362
|
|
|
1343
1363
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1469,9 +1489,6 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
|
|
|
1469
1489
|
damage *= volatiles.toxic;
|
|
1470
1490
|
}
|
|
1471
1491
|
|
|
1472
|
-
// Pokémon Showdown erroneously updates last damage with uncapped Leech Seed damage
|
|
1473
|
-
if (showdown) battle.last_damage = damage;
|
|
1474
|
-
|
|
1475
1492
|
const amount = @min(damage, stored.hp);
|
|
1476
1493
|
stored.hp -= amount;
|
|
1477
1494
|
|
|
@@ -1563,6 +1580,42 @@ fn checkEBC(battle: anytype) bool {
|
|
|
1563
1580
|
return false;
|
|
1564
1581
|
}
|
|
1565
1582
|
|
|
1583
|
+
fn disabled(side: *Side, ident: ID, log: anytype) !bool {
|
|
1584
|
+
if (side.active.volatiles.disabled_move != 0) {
|
|
1585
|
+
// A Pokémon that transforms after being disabled may end up with less move slots
|
|
1586
|
+
const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
|
|
1587
|
+
// side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
|
|
1588
|
+
const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
|
|
1589
|
+
m
|
|
1590
|
+
else
|
|
1591
|
+
side.last_selected_move;
|
|
1592
|
+
if (m != .None and m == last) {
|
|
1593
|
+
side.active.volatiles.Charging = false;
|
|
1594
|
+
try log.disabled(ident, last);
|
|
1595
|
+
return true;
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
return false;
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
inline fn buildRage(battle: anytype, who: Player, log: anytype) !void {
|
|
1602
|
+
const side = battle.side(who);
|
|
1603
|
+
if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
|
|
1604
|
+
try Effects.boost(battle, who, Move.get(.Rage), log);
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
|
|
1609
|
+
var volatiles = &active.volatiles;
|
|
1610
|
+
assert(volatiles.Thrashing);
|
|
1611
|
+
if (volatiles.attacks > 0) return false;
|
|
1612
|
+
|
|
1613
|
+
volatiles.Thrashing = false;
|
|
1614
|
+
volatiles.Confusion = true;
|
|
1615
|
+
volatiles.confusion = Rolls.confusionDuration(battle);
|
|
1616
|
+
return true;
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1566
1619
|
inline fn onBegin(
|
|
1567
1620
|
battle: anytype,
|
|
1568
1621
|
player: Player,
|
|
@@ -1653,10 +1706,7 @@ pub const Effects = struct {
|
|
|
1653
1706
|
side.active.volatiles.Bide = true;
|
|
1654
1707
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1655
1708
|
side.active.volatiles.state = 0;
|
|
1656
|
-
side.active.volatiles.attacks =
|
|
1657
|
-
battle.rng.range(u4, 2, 4)
|
|
1658
|
-
else
|
|
1659
|
-
(battle.rng.next() & 1) + 2);
|
|
1709
|
+
side.active.volatiles.attacks = Rolls.bideThrashDuration(battle);
|
|
1660
1710
|
|
|
1661
1711
|
try log.start(battle.active(player), .Bide);
|
|
1662
1712
|
}
|
|
@@ -1678,14 +1728,8 @@ pub const Effects = struct {
|
|
|
1678
1728
|
return;
|
|
1679
1729
|
}
|
|
1680
1730
|
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
else
|
|
1684
|
-
battle.rng.next() < 1 + (if (move.effect == .BurnChance1)
|
|
1685
|
-
Gen12.percent(10)
|
|
1686
|
-
else
|
|
1687
|
-
Gen12.percent(30));
|
|
1688
|
-
if (!chance) return;
|
|
1731
|
+
if (foe.active.types.includes(move.type)) return;
|
|
1732
|
+
if (!Rolls.secondaryChance(battle, move.effect == .BurnChance1)) return;
|
|
1689
1733
|
|
|
1690
1734
|
foe_stored.status = Status.init(.BRN);
|
|
1691
1735
|
foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
|
|
@@ -1709,11 +1753,7 @@ pub const Effects = struct {
|
|
|
1709
1753
|
const sub = foe.active.volatiles.Substitute;
|
|
1710
1754
|
|
|
1711
1755
|
if (move.effect == .ConfusionChance) {
|
|
1712
|
-
|
|
1713
|
-
battle.rng.chance(u8, 25, 256)
|
|
1714
|
-
else
|
|
1715
|
-
battle.rng.next() < Gen12.percent(10);
|
|
1716
|
-
if (!chance) return;
|
|
1756
|
+
if (!Rolls.confusionChance(battle)) return;
|
|
1717
1757
|
} else {
|
|
1718
1758
|
if (showdown) {
|
|
1719
1759
|
if (!try checkHit(battle, player, move, log)) {
|
|
@@ -1732,10 +1772,7 @@ pub const Effects = struct {
|
|
|
1732
1772
|
|
|
1733
1773
|
if (foe.active.volatiles.Confusion) return;
|
|
1734
1774
|
foe.active.volatiles.Confusion = true;
|
|
1735
|
-
foe.active.volatiles.confusion =
|
|
1736
|
-
battle.rng.range(u8, 2, 6)
|
|
1737
|
-
else
|
|
1738
|
-
(battle.rng.next() & 3) + 2);
|
|
1775
|
+
foe.active.volatiles.confusion = Rolls.confusionDuration(battle);
|
|
1739
1776
|
|
|
1740
1777
|
try log.start(battle.active(player.foe()), .Confusion);
|
|
1741
1778
|
}
|
|
@@ -1785,11 +1822,8 @@ pub const Effects = struct {
|
|
|
1785
1822
|
return Result.Error;
|
|
1786
1823
|
}
|
|
1787
1824
|
|
|
1788
|
-
volatiles.disabled_move = @intCast(u3,
|
|
1789
|
-
volatiles.disabled_duration =
|
|
1790
|
-
battle.rng.range(u8, 1, 9)
|
|
1791
|
-
else
|
|
1792
|
-
(battle.rng.next() & 7) + 1);
|
|
1825
|
+
volatiles.disabled_move = @intCast(u3, Rolls.moveSlot(battle, &foe.active.moves, n));
|
|
1826
|
+
volatiles.disabled_duration = Rolls.disableDuration(battle);
|
|
1793
1827
|
|
|
1794
1828
|
try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
|
|
1795
1829
|
return null;
|
|
@@ -1801,7 +1835,7 @@ pub const Effects = struct {
|
|
|
1801
1835
|
|
|
1802
1836
|
if (battle.last_damage == 0) {
|
|
1803
1837
|
assert(showdown);
|
|
1804
|
-
return;
|
|
1838
|
+
if (!battle.foe(player).active.volatiles.Substitute) return;
|
|
1805
1839
|
}
|
|
1806
1840
|
|
|
1807
1841
|
const drain = @max(battle.last_damage / 2, 1);
|
|
@@ -1828,15 +1862,7 @@ pub const Effects = struct {
|
|
|
1828
1862
|
var volatiles = &battle.foe(player).active.volatiles;
|
|
1829
1863
|
|
|
1830
1864
|
if (volatiles.Substitute) return;
|
|
1831
|
-
|
|
1832
|
-
const chance = if (showdown)
|
|
1833
|
-
battle.rng.chance(u8, @as(u8, if (move.effect == .FlinchChance1) 26 else 77), 256)
|
|
1834
|
-
else
|
|
1835
|
-
battle.rng.next() < 1 + (if (move.effect == .FlinchChance1)
|
|
1836
|
-
Gen12.percent(10)
|
|
1837
|
-
else
|
|
1838
|
-
Gen12.percent(30));
|
|
1839
|
-
if (!chance) return;
|
|
1865
|
+
if (!Rolls.secondaryChance(battle, move.effect == .FlinchChance1)) return;
|
|
1840
1866
|
|
|
1841
1867
|
volatiles.Flinch = true;
|
|
1842
1868
|
volatiles.Recharging = false;
|
|
@@ -1862,12 +1888,8 @@ pub const Effects = struct {
|
|
|
1862
1888
|
return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
|
|
1863
1889
|
}
|
|
1864
1890
|
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
else
|
|
1868
|
-
battle.rng.next() < 1 + Gen12.percent(10);
|
|
1869
|
-
if (!chance) return;
|
|
1870
|
-
|
|
1891
|
+
if (foe.active.types.includes(move.type)) return;
|
|
1892
|
+
if (!Rolls.secondaryChance(battle, true)) return;
|
|
1871
1893
|
// Freeze Clause Mod
|
|
1872
1894
|
if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
1873
1895
|
|
|
@@ -2012,7 +2034,7 @@ pub const Effects = struct {
|
|
|
2012
2034
|
}
|
|
2013
2035
|
if (!try checkHit(battle, player, move, log)) return;
|
|
2014
2036
|
|
|
2015
|
-
const rslot =
|
|
2037
|
+
const rslot = Rolls.moveSlot(battle, &foe.active.moves, 0);
|
|
2016
2038
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
2017
2039
|
|
|
2018
2040
|
try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
|
|
@@ -2033,7 +2055,8 @@ pub const Effects = struct {
|
|
|
2033
2055
|
assert(!side.active.volatiles.MultiHit);
|
|
2034
2056
|
side.active.volatiles.MultiHit = true;
|
|
2035
2057
|
|
|
2036
|
-
side.active.volatiles.attacks =
|
|
2058
|
+
side.active.volatiles.attacks =
|
|
2059
|
+
if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
|
|
2037
2060
|
}
|
|
2038
2061
|
|
|
2039
2062
|
fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2077,14 +2100,8 @@ pub const Effects = struct {
|
|
|
2077
2100
|
}
|
|
2078
2101
|
|
|
2079
2102
|
// Body Slam can't paralyze a Normal type Pokémon
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
else
|
|
2083
|
-
battle.rng.next() < 1 + (if (move.effect == .ParalyzeChance1)
|
|
2084
|
-
Gen12.percent(10)
|
|
2085
|
-
else
|
|
2086
|
-
Gen12.percent(30));
|
|
2087
|
-
if (!chance) return;
|
|
2103
|
+
if (foe.active.types.includes(move.type)) return;
|
|
2104
|
+
if (!Rolls.secondaryChance(battle, move.effect == .ParalyzeChance1)) return;
|
|
2088
2105
|
|
|
2089
2106
|
foe_stored.status = Status.init(.PAR);
|
|
2090
2107
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
@@ -2127,16 +2144,7 @@ pub const Effects = struct {
|
|
|
2127
2144
|
|
|
2128
2145
|
if (move.effect == .Poison) {
|
|
2129
2146
|
if (!showdown and !try checkHit(battle, player, move, log)) return;
|
|
2130
|
-
} else
|
|
2131
|
-
const chance = if (showdown)
|
|
2132
|
-
battle.rng.chance(u8, @as(u8, if (move.effect == .PoisonChance1) 52 else 103), 256)
|
|
2133
|
-
else
|
|
2134
|
-
battle.rng.next() < 1 + (if (move.effect == .PoisonChance1)
|
|
2135
|
-
Gen12.percent(20)
|
|
2136
|
-
else
|
|
2137
|
-
Gen12.percent(40));
|
|
2138
|
-
if (!chance) return;
|
|
2139
|
-
}
|
|
2147
|
+
} else if (!Rolls.poisonChance(battle, move.effect == .PoisonChance1)) return;
|
|
2140
2148
|
|
|
2141
2149
|
foe_stored.status = Status.init(.PSN);
|
|
2142
2150
|
if (toxic) {
|
|
@@ -2150,10 +2158,8 @@ pub const Effects = struct {
|
|
|
2150
2158
|
|
|
2151
2159
|
fn rage(battle: anytype, player: Player) !void {
|
|
2152
2160
|
var volatiles = &battle.side(player).active.volatiles;
|
|
2153
|
-
if (volatiles.Rage) return;
|
|
2154
|
-
volatiles.Rage = true;
|
|
2155
2161
|
assert(!volatiles.Bide);
|
|
2156
|
-
volatiles.
|
|
2162
|
+
volatiles.Rage = true;
|
|
2157
2163
|
}
|
|
2158
2164
|
|
|
2159
2165
|
fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
|
|
@@ -2205,16 +2211,7 @@ pub const Effects = struct {
|
|
|
2205
2211
|
}
|
|
2206
2212
|
foe.active.volatiles.Recharging = false;
|
|
2207
2213
|
|
|
2208
|
-
|
|
2209
|
-
battle.rng.range(u8, 1, 8)
|
|
2210
|
-
else loop: {
|
|
2211
|
-
while (true) {
|
|
2212
|
-
const r = battle.rng.next() & 7;
|
|
2213
|
-
if (r != 0) break :loop r;
|
|
2214
|
-
}
|
|
2215
|
-
});
|
|
2216
|
-
|
|
2217
|
-
foe_stored.status = Status.slp(duration);
|
|
2214
|
+
foe_stored.status = Status.slp(Rolls.sleepDuration(battle));
|
|
2218
2215
|
try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
|
|
2219
2216
|
}
|
|
2220
2217
|
|
|
@@ -2267,11 +2264,7 @@ pub const Effects = struct {
|
|
|
2267
2264
|
assert(!volatiles.Bide);
|
|
2268
2265
|
|
|
2269
2266
|
volatiles.Thrashing = true;
|
|
2270
|
-
volatiles.
|
|
2271
|
-
volatiles.attacks = @intCast(u3, if (showdown)
|
|
2272
|
-
battle.rng.range(u8, 2, 4)
|
|
2273
|
-
else
|
|
2274
|
-
(battle.rng.next() & 1) + 2);
|
|
2267
|
+
volatiles.attacks = Rolls.bideThrashDuration(battle);
|
|
2275
2268
|
}
|
|
2276
2269
|
|
|
2277
2270
|
fn transform(battle: anytype, player: Player, log: anytype) !void {
|
|
@@ -2303,7 +2296,7 @@ pub const Effects = struct {
|
|
|
2303
2296
|
try log.transform(battle.active(player), foe_ident);
|
|
2304
2297
|
}
|
|
2305
2298
|
|
|
2306
|
-
fn binding(battle: anytype, player: Player) void {
|
|
2299
|
+
fn binding(battle: anytype, player: Player, rewrap: bool) void {
|
|
2307
2300
|
var side = battle.side(player);
|
|
2308
2301
|
var foe = battle.foe(player);
|
|
2309
2302
|
|
|
@@ -2311,9 +2304,11 @@ pub const Effects = struct {
|
|
|
2311
2304
|
side.active.volatiles.Binding = true;
|
|
2312
2305
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2313
2306
|
// (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
|
|
2314
|
-
if (!showdown)
|
|
2307
|
+
if (!showdown) {
|
|
2308
|
+
foe.active.volatiles.Recharging = false;
|
|
2309
|
+
} else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
|
|
2315
2310
|
|
|
2316
|
-
side.active.volatiles.attacks = distribution(battle) - 1;
|
|
2311
|
+
side.active.volatiles.attacks = Rolls.distribution(battle) - 1;
|
|
2317
2312
|
}
|
|
2318
2313
|
|
|
2319
2314
|
fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2340,9 +2335,11 @@ pub const Effects = struct {
|
|
|
2340
2335
|
return try log.fail(ident, .None);
|
|
2341
2336
|
}
|
|
2342
2337
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2343
|
-
const stat = unmodifiedStats(battle,
|
|
2338
|
+
const stat = unmodifiedStats(battle, side).atk;
|
|
2344
2339
|
stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2345
2340
|
try log.boost(ident, reason, n);
|
|
2341
|
+
// Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
|
|
2342
|
+
if (showdown and move.effect == .Rage) return;
|
|
2346
2343
|
},
|
|
2347
2344
|
.DefenseUp1, .DefenseUp2 => {
|
|
2348
2345
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
@@ -2358,7 +2355,7 @@ pub const Effects = struct {
|
|
|
2358
2355
|
return try log.fail(ident, .None);
|
|
2359
2356
|
}
|
|
2360
2357
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2361
|
-
const stat = unmodifiedStats(battle,
|
|
2358
|
+
const stat = unmodifiedStats(battle, side).def;
|
|
2362
2359
|
stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2363
2360
|
try log.boost(ident, .Defense, n);
|
|
2364
2361
|
},
|
|
@@ -2375,7 +2372,7 @@ pub const Effects = struct {
|
|
|
2375
2372
|
return try log.fail(ident, .None);
|
|
2376
2373
|
}
|
|
2377
2374
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2378
|
-
const stat = unmodifiedStats(battle,
|
|
2375
|
+
const stat = unmodifiedStats(battle, side).spe;
|
|
2379
2376
|
stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2380
2377
|
try log.boost(ident, .Speed, 2);
|
|
2381
2378
|
},
|
|
@@ -2395,7 +2392,7 @@ pub const Effects = struct {
|
|
|
2395
2392
|
return try log.fail(ident, .None);
|
|
2396
2393
|
}
|
|
2397
2394
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2398
|
-
const stat = unmodifiedStats(battle,
|
|
2395
|
+
const stat = unmodifiedStats(battle, side).spc;
|
|
2399
2396
|
stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2400
2397
|
try log.boost(ident, .SpecialAttack, n);
|
|
2401
2398
|
try log.boost(ident, .SpecialDefense, n);
|
|
@@ -2422,11 +2419,7 @@ pub const Effects = struct {
|
|
|
2422
2419
|
}
|
|
2423
2420
|
|
|
2424
2421
|
if (move.effect.isStatDownChance()) {
|
|
2425
|
-
|
|
2426
|
-
battle.rng.chance(u8, 85, 256)
|
|
2427
|
-
else
|
|
2428
|
-
battle.rng.next() < Gen12.percent(33) + 1;
|
|
2429
|
-
if (!chance or foe.active.volatiles.Invulnerable) return;
|
|
2422
|
+
if (!Rolls.unboost(battle) or foe.active.volatiles.Invulnerable) return;
|
|
2430
2423
|
} else if (!showdown and !try checkHit(battle, player, move, log)) {
|
|
2431
2424
|
return; // checkHit already checks for Invulnerable
|
|
2432
2425
|
}
|
|
@@ -2448,7 +2441,7 @@ pub const Effects = struct {
|
|
|
2448
2441
|
return try log.fail(foe_ident, .None);
|
|
2449
2442
|
}
|
|
2450
2443
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2451
|
-
const stat = unmodifiedStats(battle,
|
|
2444
|
+
const stat = unmodifiedStats(battle, foe).atk;
|
|
2452
2445
|
stats.atk = @max(1, stat * mod[0] / mod[1]);
|
|
2453
2446
|
try log.boost(foe_ident, .Attack, -1);
|
|
2454
2447
|
},
|
|
@@ -2466,7 +2459,7 @@ pub const Effects = struct {
|
|
|
2466
2459
|
return try log.fail(foe_ident, .None);
|
|
2467
2460
|
}
|
|
2468
2461
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2469
|
-
const stat = unmodifiedStats(battle,
|
|
2462
|
+
const stat = unmodifiedStats(battle, foe).def;
|
|
2470
2463
|
stats.def = @max(1, stat * mod[0] / mod[1]);
|
|
2471
2464
|
try log.boost(foe_ident, .Defense, -@as(i8, n));
|
|
2472
2465
|
},
|
|
@@ -2483,7 +2476,7 @@ pub const Effects = struct {
|
|
|
2483
2476
|
return try log.fail(foe_ident, .None);
|
|
2484
2477
|
}
|
|
2485
2478
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2486
|
-
const stat = unmodifiedStats(battle,
|
|
2479
|
+
const stat = unmodifiedStats(battle, foe).spe;
|
|
2487
2480
|
stats.spe = @max(1, stat * mod[0] / mod[1]);
|
|
2488
2481
|
try log.boost(foe_ident, .Speed, -1);
|
|
2489
2482
|
assert(boosts.spe >= -6);
|
|
@@ -2503,7 +2496,7 @@ pub const Effects = struct {
|
|
|
2503
2496
|
return try log.fail(foe_ident, .None);
|
|
2504
2497
|
}
|
|
2505
2498
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2506
|
-
const stat = unmodifiedStats(battle,
|
|
2499
|
+
const stat = unmodifiedStats(battle, foe).spc;
|
|
2507
2500
|
stats.spc = @max(1, stat * mod[0] / mod[1]);
|
|
2508
2501
|
try log.boost(foe_ident, .SpecialAttack, -1);
|
|
2509
2502
|
try log.boost(foe_ident, .SpecialDefense, -1);
|
|
@@ -2522,8 +2515,7 @@ pub const Effects = struct {
|
|
|
2522
2515
|
}
|
|
2523
2516
|
};
|
|
2524
2517
|
|
|
2525
|
-
fn unmodifiedStats(battle: anytype,
|
|
2526
|
-
const side = battle.side(who);
|
|
2518
|
+
fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
|
|
2527
2519
|
if (!side.active.volatiles.Transform) return &side.stored().stats;
|
|
2528
2520
|
const id = ID.from(side.active.volatiles.transform);
|
|
2529
2521
|
return &battle.side(id.player).pokemon[id.id - 1].stats;
|
|
@@ -2589,75 +2581,140 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
|
|
|
2589
2581
|
}
|
|
2590
2582
|
}
|
|
2591
2583
|
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2584
|
+
pub const Rolls = struct {
|
|
2585
|
+
inline fn speedTie(battle: anytype) bool {
|
|
2586
|
+
if (showdown) return battle.rng.range(u8, 0, 2) == 0;
|
|
2587
|
+
return battle.rng.next() < Gen12.percent(50) + 1;
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2590
|
+
inline fn criticalHit(battle: anytype, chance: u16) bool {
|
|
2591
|
+
if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
|
|
2592
|
+
return std.math.rotl(u8, battle.rng.next(), 3) < chance;
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2595
|
+
inline fn damage(battle: anytype) u8 {
|
|
2596
|
+
if (showdown) return battle.rng.range(u8, 217, 256);
|
|
2597
|
+
while (true) {
|
|
2598
|
+
const r = std.math.rotr(u8, battle.rng.next(), 1);
|
|
2599
|
+
if (r >= 217) return r;
|
|
2600
2600
|
}
|
|
2601
2601
|
}
|
|
2602
|
-
return false;
|
|
2603
|
-
}
|
|
2604
2602
|
|
|
2605
|
-
inline fn
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
try Effects.boost(battle, who, Move.get(.Rage), log);
|
|
2603
|
+
inline fn hit(battle: anytype, accuracy: u16) bool {
|
|
2604
|
+
if (showdown) return battle.rng.chance(u8, @intCast(u8, accuracy), 256);
|
|
2605
|
+
return battle.rng.next() < accuracy;
|
|
2609
2606
|
}
|
|
2610
|
-
}
|
|
2611
2607
|
|
|
2612
|
-
fn
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2608
|
+
inline fn confusionDuration(battle: anytype) u3 {
|
|
2609
|
+
if (showdown) return @intCast(u3, battle.rng.range(u8, 2, 6));
|
|
2610
|
+
return @intCast(u3, (battle.rng.next() & 3) + 2);
|
|
2611
|
+
}
|
|
2616
2612
|
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
else
|
|
2622
|
-
(battle.rng.next() & 3) + 2);
|
|
2613
|
+
inline fn confused(battle: anytype) bool {
|
|
2614
|
+
if (showdown) return !battle.rng.chance(u8, 128, 256);
|
|
2615
|
+
return battle.rng.next() >= Gen12.percent(50) + 1;
|
|
2616
|
+
}
|
|
2623
2617
|
|
|
2624
|
-
|
|
2625
|
-
|
|
2618
|
+
inline fn paralyzed(battle: anytype) bool {
|
|
2619
|
+
if (showdown) return battle.rng.chance(u8, 63, 256);
|
|
2620
|
+
return battle.rng.next() < Gen12.percent(25);
|
|
2621
|
+
}
|
|
2626
2622
|
|
|
2627
|
-
|
|
2623
|
+
inline fn confusionChance(battle: anytype) bool {
|
|
2624
|
+
if (showdown) return battle.rng.chance(u8, 25, 256);
|
|
2625
|
+
return battle.rng.next() < Gen12.percent(10);
|
|
2626
|
+
}
|
|
2628
2627
|
|
|
2629
|
-
fn
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
}
|
|
2628
|
+
inline fn secondaryChance(battle: anytype, low: bool) bool {
|
|
2629
|
+
if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 26 else 77), 256);
|
|
2630
|
+
return battle.rng.next() < 1 + (if (low) Gen12.percent(10) else Gen12.percent(30));
|
|
2631
|
+
}
|
|
2634
2632
|
|
|
2635
|
-
fn
|
|
2636
|
-
|
|
2637
|
-
if (
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2633
|
+
inline fn poisonChance(battle: anytype, low: bool) bool {
|
|
2634
|
+
if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 52 else 103), 256);
|
|
2635
|
+
return battle.rng.next() < 1 + (if (low) Gen12.percent(20) else Gen12.percent(40));
|
|
2636
|
+
}
|
|
2637
|
+
|
|
2638
|
+
inline fn sleepDuration(battle: anytype) u3 {
|
|
2639
|
+
if (showdown) return @intCast(u3, battle.rng.range(u8, 1, 8));
|
|
2640
|
+
while (true) {
|
|
2641
|
+
const r = battle.rng.next() & 7;
|
|
2642
|
+
if (r != 0) return @intCast(u3, r);
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2646
|
+
inline fn bideThrashDuration(battle: anytype) u3 {
|
|
2647
|
+
if (showdown) return @intCast(u3, battle.rng.range(u4, 2, 4));
|
|
2648
|
+
return @intCast(u3, (battle.rng.next() & 1) + 2);
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
fn moveSlot(battle: anytype, moves: []MoveSlot, check_pp: u4) u4 {
|
|
2652
|
+
if (showdown) {
|
|
2653
|
+
if (check_pp == 0) {
|
|
2654
|
+
var i: usize = moves.len;
|
|
2655
|
+
while (i > 0) {
|
|
2656
|
+
i -= 1;
|
|
2657
|
+
if (moves[i].id != .None) {
|
|
2658
|
+
return battle.rng.range(u4, 0, @intCast(u4, i + 1)) + 1;
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
} else {
|
|
2662
|
+
var r = battle.rng.range(u4, 0, @intCast(u4, check_pp)) + 1;
|
|
2663
|
+
var i: usize = 0;
|
|
2664
|
+
while (i < moves.len and r > 0) : (i += 1) {
|
|
2665
|
+
if (moves[i].pp > 0) {
|
|
2666
|
+
r -= 1;
|
|
2667
|
+
if (r == 0) break;
|
|
2668
|
+
}
|
|
2650
2669
|
}
|
|
2670
|
+
return @intCast(u4, i + 1);
|
|
2651
2671
|
}
|
|
2652
|
-
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
while (true) {
|
|
2675
|
+
const r = @intCast(u4, battle.rng.next() & 3);
|
|
2676
|
+
if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
|
|
2653
2677
|
}
|
|
2654
2678
|
}
|
|
2655
2679
|
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2680
|
+
inline fn disableDuration(battle: anytype) u4 {
|
|
2681
|
+
if (showdown) return @intCast(u4, battle.rng.range(u8, 1, 9));
|
|
2682
|
+
return @intCast(u4, (battle.rng.next() & 7) + 1);
|
|
2659
2683
|
}
|
|
2660
|
-
|
|
2684
|
+
|
|
2685
|
+
inline fn metronome(battle: anytype) Move {
|
|
2686
|
+
if (showdown) {
|
|
2687
|
+
const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
|
|
2688
|
+
const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
|
|
2689
|
+
return @intToEnum(Move, r + mod);
|
|
2690
|
+
}
|
|
2691
|
+
while (true) {
|
|
2692
|
+
const r = battle.rng.next();
|
|
2693
|
+
if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
|
|
2694
|
+
if (r >= @enumToInt(Move.Struggle)) continue;
|
|
2695
|
+
return @intToEnum(Move, r);
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
|
|
2699
|
+
inline fn psywave(battle: anytype, max: u8) u8 {
|
|
2700
|
+
if (showdown) return battle.rng.range(u8, 0, max);
|
|
2701
|
+
while (true) {
|
|
2702
|
+
const r = battle.rng.next();
|
|
2703
|
+
if (r < max) return r;
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
|
|
2707
|
+
inline fn distribution(battle: anytype) u3 {
|
|
2708
|
+
if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
|
|
2709
|
+
const r = (battle.rng.next() & 3);
|
|
2710
|
+
return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
|
|
2711
|
+
}
|
|
2712
|
+
|
|
2713
|
+
inline fn unboost(battle: anytype) bool {
|
|
2714
|
+
if (showdown) return battle.rng.chance(u8, 85, 256);
|
|
2715
|
+
return battle.rng.next() < Gen12.percent(33) + 1;
|
|
2716
|
+
}
|
|
2717
|
+
};
|
|
2661
2718
|
|
|
2662
2719
|
test "RNG agreement" {
|
|
2663
2720
|
if (!showdown) return;
|
|
@@ -2757,7 +2814,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
2757
2814
|
while (slot <= 4) : (slot += 1) {
|
|
2758
2815
|
const m = active.moves[slot - 1];
|
|
2759
2816
|
if (m.id == .None) break;
|
|
2760
|
-
if (m.id == side.last_selected_move) {
|
|
2817
|
+
if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
|
|
2761
2818
|
// Pokémon Showdown displays Struggle if limited to Bide but unable to pick
|
|
2762
2819
|
const struggle =
|
|
2763
2820
|
m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);
|