@pkmn/engine 0.1.0-dev.7fe22fc0 → 0.1.0-dev.84cc1a02
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 +22 -50
- package/build/pkg/data.d.ts +2 -1
- package/build/pkg/data.js +5 -3
- package/build/pkg/data.js.map +1 -1
- package/build/pkg/gen1.d.ts +3 -3
- package/build/pkg/gen1.js +13 -11
- package/build/pkg/gen1.js.map +1 -1
- package/build/pkg/index.d.ts +10 -11
- package/build/pkg/index.js.map +1 -1
- package/build.zig +31 -23
- package/package.json +29 -40
- package/src/bin/install-pkmn-engine +28 -47
- package/src/lib/binding/node.zig +5 -5
- package/src/lib/binding/wasm.zig +3 -3
- package/src/lib/common/debug.zig +0 -1
- package/src/lib/common/options.zig +3 -3
- package/src/lib/common/protocol.zig +38 -38
- package/src/lib/gen1/README.md +16 -39
- package/src/lib/gen1/data.zig +6 -6
- package/src/lib/gen1/helpers.zig +7 -10
- package/src/lib/gen1/mechanics.zig +271 -349
- package/src/lib/gen2/data.zig +4 -4
- package/src/lib/pkmn.zig +7 -7
- package/build/pkg/addon.d.ts +0 -7
- package/build/pkg/addon.js +0 -111
- package/build/pkg/addon.js.map +0 -1
- package/build/test/display.js +0 -383
- package/build/test/display.js.map +0 -1
- package/build/tools/debug.js +0 -133
- package/build/tools/debug.js.map +0 -1
- package/src/bin/pkmn-debug +0 -62
- package/src/test/display/pkmn.html.tmpl +0 -368
|
@@ -56,34 +56,30 @@ 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
|
-
|
|
61
59
|
pub fn update(battle: anytype, c1: Choice, c2: Choice, log: anytype) !Result {
|
|
62
60
|
assert(c1.type != .Pass or c2.type != .Pass or battle.turn == 0);
|
|
63
61
|
if (battle.turn == 0) return start(battle, log);
|
|
64
62
|
|
|
65
|
-
var s1 = false;
|
|
66
|
-
var s2 = false;
|
|
67
63
|
var f1: ?Move = null;
|
|
68
64
|
var f2: ?Move = null;
|
|
69
65
|
|
|
70
|
-
if (selectMove(battle, .P1, c1, c2, &
|
|
71
|
-
if (selectMove(battle, .P2, c2, c1, &
|
|
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;
|
|
66
|
+
if (selectMove(battle, .P1, c1, c2, &f1)) |r| return r;
|
|
67
|
+
if (selectMove(battle, .P2, c2, c1, &f2)) |r| return r;
|
|
78
68
|
|
|
79
69
|
if (turnOrder(battle, c1, c2) == .P1) {
|
|
80
|
-
if (try doTurn(battle, .P1, c1,
|
|
70
|
+
if (try doTurn(battle, .P1, c1, f1, .P2, c2, f2, log)) |r| return r;
|
|
81
71
|
} else {
|
|
82
|
-
if (try doTurn(battle, .P2, c2,
|
|
72
|
+
if (try doTurn(battle, .P2, c2, f2, .P1, c1, f1, log)) |r| return r;
|
|
83
73
|
}
|
|
84
74
|
|
|
85
|
-
|
|
86
|
-
if (
|
|
75
|
+
var p1 = battle.side(.P1);
|
|
76
|
+
if (p1.active.volatiles.attacks == 0) {
|
|
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
|
+
}
|
|
87
83
|
|
|
88
84
|
return endTurn(battle, log);
|
|
89
85
|
}
|
|
@@ -116,7 +112,6 @@ fn selectMove(
|
|
|
116
112
|
player: Player,
|
|
117
113
|
choice: Choice,
|
|
118
114
|
foe_choice: Choice,
|
|
119
|
-
skip_turn: *bool,
|
|
120
115
|
from: *?Move,
|
|
121
116
|
) ?Result {
|
|
122
117
|
if (choice.type == .Pass) return null;
|
|
@@ -132,20 +127,14 @@ fn selectMove(
|
|
|
132
127
|
if (volatiles.Recharging) return null;
|
|
133
128
|
if (volatiles.Rage) {
|
|
134
129
|
from.* = side.last_used_move;
|
|
135
|
-
if (showdown)
|
|
136
|
-
if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
|
|
137
|
-
saveMove(battle, player, null);
|
|
138
|
-
}
|
|
130
|
+
if (showdown) saveMove(battle, player, null);
|
|
139
131
|
return null;
|
|
140
132
|
}
|
|
141
133
|
// Pokémon Showdown removes Flinch at the end-of-turn in its residual handler
|
|
142
134
|
if (!showdown) volatiles.Flinch = false;
|
|
143
135
|
if (volatiles.Thrashing or volatiles.Charging) {
|
|
144
136
|
from.* = side.last_selected_move;
|
|
145
|
-
if (showdown)
|
|
146
|
-
if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
|
|
147
|
-
saveMove(battle, player, null);
|
|
148
|
-
}
|
|
137
|
+
if (showdown) saveMove(battle, player, null);
|
|
149
138
|
return null;
|
|
150
139
|
}
|
|
151
140
|
|
|
@@ -155,10 +144,7 @@ fn selectMove(
|
|
|
155
144
|
// pre-move select
|
|
156
145
|
if (Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP) or volatiles.Bide) {
|
|
157
146
|
assert(showdown or choice.data == 0);
|
|
158
|
-
if (showdown)
|
|
159
|
-
if (volatiles.Bide and battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
|
|
160
|
-
saveMove(battle, player, choice);
|
|
161
|
-
}
|
|
147
|
+
if (showdown) saveMove(battle, player, choice);
|
|
162
148
|
return null;
|
|
163
149
|
}
|
|
164
150
|
if (volatiles.Binding) {
|
|
@@ -189,7 +175,6 @@ fn selectMove(
|
|
|
189
175
|
}
|
|
190
176
|
|
|
191
177
|
if (battle.foe(player).active.volatiles.Binding) {
|
|
192
|
-
skip_turn.* = true;
|
|
193
178
|
if (showdown) {
|
|
194
179
|
saveMove(battle, player, choice);
|
|
195
180
|
} else {
|
|
@@ -200,7 +185,6 @@ fn selectMove(
|
|
|
200
185
|
}
|
|
201
186
|
|
|
202
187
|
// move select
|
|
203
|
-
volatiles.state = 0;
|
|
204
188
|
if (choice.data == 0) {
|
|
205
189
|
const struggle = ok: {
|
|
206
190
|
for (side.active.moves, 0..) |move, i| {
|
|
@@ -317,7 +301,11 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
|
|
|
317
301
|
// Pokémon Showdown's beforeTurnCallback shenanigans
|
|
318
302
|
if (showdown and m1 == .Counter and m2 == .Counter) battle.rng.advance(1);
|
|
319
303
|
|
|
320
|
-
const p1 =
|
|
304
|
+
const p1 = if (showdown)
|
|
305
|
+
battle.rng.range(u8, 0, 2) == 0
|
|
306
|
+
else
|
|
307
|
+
battle.rng.next() < Gen12.percent(50) + 1;
|
|
308
|
+
|
|
321
309
|
if (!showdown) return if (p1) .P1 else .P2;
|
|
322
310
|
|
|
323
311
|
// Pokémon Showdown's "lockedmove" volatile's onBeforeTurn uses BattleQueue#changeAction,
|
|
@@ -327,7 +315,7 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
|
|
|
327
315
|
const t1 = battle.side(.P1).active.volatiles.Thrashing;
|
|
328
316
|
const t2 = battle.side(.P2).active.volatiles.Thrashing;
|
|
329
317
|
// If *both* sides are thrashing it really should be another speed tie, but we've patched
|
|
330
|
-
// that out and enforce host ordering of events, so P1 just goes first
|
|
318
|
+
// that out and enforce host ordering of events, so P1 just goes first reqgardless of who
|
|
331
319
|
// won the original coin flip
|
|
332
320
|
if (t1 and t2) return .P1;
|
|
333
321
|
return if (p1) if (t1 and !t2) .P2 else .P1 else if (t2 and !t1) .P1 else .P2;
|
|
@@ -340,13 +328,9 @@ fn doTurn(
|
|
|
340
328
|
battle: anytype,
|
|
341
329
|
player: Player,
|
|
342
330
|
player_choice: Choice,
|
|
343
|
-
player_rewrap: bool,
|
|
344
|
-
player_skip: bool,
|
|
345
331
|
player_from: ?Move,
|
|
346
332
|
foe_player: Player,
|
|
347
333
|
foe_choice: Choice,
|
|
348
|
-
foe_rewrap: bool,
|
|
349
|
-
foe_skip: bool,
|
|
350
334
|
foe_from: ?Move,
|
|
351
335
|
log: anytype,
|
|
352
336
|
) !?Result {
|
|
@@ -354,16 +338,7 @@ fn doTurn(
|
|
|
354
338
|
|
|
355
339
|
var residual = true;
|
|
356
340
|
var replace = battle.side(player).stored().hp == 0;
|
|
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;
|
|
341
|
+
if (try executeMove(battle, player, player_choice, player_from, &residual, log)) |r| return r;
|
|
367
342
|
if (!replace) {
|
|
368
343
|
if (player_choice.type != .Switch) {
|
|
369
344
|
if (try checkFaint(battle, foe_player, log)) |r| return r;
|
|
@@ -374,16 +349,7 @@ fn doTurn(
|
|
|
374
349
|
|
|
375
350
|
residual = true;
|
|
376
351
|
replace = battle.side(foe_player).stored().hp == 0;
|
|
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;
|
|
352
|
+
if (try executeMove(battle, foe_player, foe_choice, foe_from, &residual, log)) |r| return r;
|
|
387
353
|
if (!replace) {
|
|
388
354
|
if (foe_choice.type != .Switch) {
|
|
389
355
|
if (try checkFaint(battle, player, log)) |r| return r;
|
|
@@ -406,8 +372,6 @@ fn executeMove(
|
|
|
406
372
|
battle: anytype,
|
|
407
373
|
player: Player,
|
|
408
374
|
choice: Choice,
|
|
409
|
-
rewrap: bool,
|
|
410
|
-
skip: bool,
|
|
411
375
|
from: ?Move,
|
|
412
376
|
residual: *bool,
|
|
413
377
|
log: anytype,
|
|
@@ -419,10 +383,9 @@ fn executeMove(
|
|
|
419
383
|
return null;
|
|
420
384
|
}
|
|
421
385
|
|
|
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
|
|
425
386
|
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
|
|
426
389
|
assert(!showdown);
|
|
427
390
|
if (battle.foe(player).active.volatiles.Binding) {
|
|
428
391
|
try log.cant(battle.active(player), .Bound);
|
|
@@ -472,7 +435,7 @@ fn executeMove(
|
|
|
472
435
|
|
|
473
436
|
var skip_can = false;
|
|
474
437
|
var skip_pp = false;
|
|
475
|
-
switch (try beforeMove(battle, player,
|
|
438
|
+
switch (try beforeMove(battle, player, from, residual, log)) {
|
|
476
439
|
.done => return null,
|
|
477
440
|
.skip_can => skip_can = true,
|
|
478
441
|
.skip_pp => skip_pp = true,
|
|
@@ -484,15 +447,14 @@ fn executeMove(
|
|
|
484
447
|
return null;
|
|
485
448
|
}
|
|
486
449
|
|
|
487
|
-
return doMove(battle, player, mslot,
|
|
450
|
+
return doMove(battle, player, mslot, auto, residual, log);
|
|
488
451
|
}
|
|
489
452
|
|
|
490
|
-
const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
|
|
453
|
+
const BeforeMove = union(enum) { done, skip_can, skip_pp, ok, err };
|
|
491
454
|
|
|
492
455
|
fn beforeMove(
|
|
493
456
|
battle: anytype,
|
|
494
457
|
player: Player,
|
|
495
|
-
skip: bool,
|
|
496
458
|
from: ?Move,
|
|
497
459
|
residual: *bool,
|
|
498
460
|
log: anytype,
|
|
@@ -524,13 +486,7 @@ fn beforeMove(
|
|
|
524
486
|
return .done;
|
|
525
487
|
}
|
|
526
488
|
|
|
527
|
-
if (
|
|
528
|
-
volatiles.Recharging = false;
|
|
529
|
-
try log.cant(ident, .Recharge);
|
|
530
|
-
return .done;
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
if (!showdown and foe.active.volatiles.Binding) {
|
|
489
|
+
if (foe.active.volatiles.Binding) {
|
|
534
490
|
try log.cant(ident, .Bound);
|
|
535
491
|
return .done;
|
|
536
492
|
}
|
|
@@ -541,7 +497,7 @@ fn beforeMove(
|
|
|
541
497
|
return .done;
|
|
542
498
|
}
|
|
543
499
|
|
|
544
|
-
if (
|
|
500
|
+
if (volatiles.Recharging) {
|
|
545
501
|
volatiles.Recharging = false;
|
|
546
502
|
try log.cant(ident, .Recharge);
|
|
547
503
|
return .done;
|
|
@@ -557,15 +513,9 @@ fn beforeMove(
|
|
|
557
513
|
// Pokémon Showdown's disable condition has a single onBeforeMove handler
|
|
558
514
|
if (showdown and try disabled(side, ident, log)) return .done;
|
|
559
515
|
|
|
560
|
-
|
|
561
|
-
try log.cant(ident, .Bound);
|
|
562
|
-
return .done;
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
// Pokémon Showdown checks for Flinch *after* instead of before... and doesn't clear Flinch
|
|
566
|
-
// until its imaginary "residual" phase, meaning Pokémon can sometimes flinch multiple times
|
|
567
|
-
// from the same original hit
|
|
516
|
+
// Pokémon Showdown checks for Flinch *after* instead of before
|
|
568
517
|
if (showdown and volatiles.Flinch) {
|
|
518
|
+
volatiles.Flinch = false;
|
|
569
519
|
try log.cant(ident, .Flinch);
|
|
570
520
|
return .done;
|
|
571
521
|
}
|
|
@@ -587,7 +537,12 @@ fn beforeMove(
|
|
|
587
537
|
} else {
|
|
588
538
|
try log.activate(ident, .Confusion);
|
|
589
539
|
|
|
590
|
-
if (
|
|
540
|
+
const confused = if (showdown)
|
|
541
|
+
!battle.rng.chance(u8, 128, 256)
|
|
542
|
+
else
|
|
543
|
+
battle.rng.next() >= Gen12.percent(50) + 1;
|
|
544
|
+
|
|
545
|
+
if (confused) {
|
|
591
546
|
assert(!volatiles.MultiHit);
|
|
592
547
|
if (!volatiles.Rage) volatiles.state = 0;
|
|
593
548
|
volatiles.Bide = false;
|
|
@@ -613,12 +568,8 @@ fn beforeMove(
|
|
|
613
568
|
player
|
|
614
569
|
else
|
|
615
570
|
player.foe();
|
|
616
|
-
|
|
617
|
-
const uncapped = battle.last_damage;
|
|
618
571
|
// Skipping adjustDamage / randomizeDamage / checkHit
|
|
619
572
|
_ = try applyDamage(battle, player, target, .Confusion, log);
|
|
620
|
-
// Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
|
|
621
|
-
if (showdown) battle.last_damage = uncapped;
|
|
622
573
|
|
|
623
574
|
return .done;
|
|
624
575
|
}
|
|
@@ -627,15 +578,22 @@ fn beforeMove(
|
|
|
627
578
|
|
|
628
579
|
if (!showdown and try disabled(side, ident, log)) return .done;
|
|
629
580
|
|
|
630
|
-
if (Status.is(stored.status, .PAR)
|
|
631
|
-
if (
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
581
|
+
if (Status.is(stored.status, .PAR)) {
|
|
582
|
+
const paralyzed = if (showdown)
|
|
583
|
+
battle.rng.chance(u8, 63, 256)
|
|
584
|
+
else
|
|
585
|
+
battle.rng.next() < Gen12.percent(25);
|
|
586
|
+
|
|
587
|
+
if (paralyzed) {
|
|
588
|
+
if (!volatiles.Rage) volatiles.state = 0;
|
|
589
|
+
volatiles.Bide = false;
|
|
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
|
+
}
|
|
639
597
|
}
|
|
640
598
|
|
|
641
599
|
if (volatiles.Bide) {
|
|
@@ -662,9 +620,8 @@ fn beforeMove(
|
|
|
662
620
|
return .done;
|
|
663
621
|
}
|
|
664
622
|
|
|
665
|
-
const sub = showdown and foe.active.volatiles.Substitute;
|
|
666
623
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
667
|
-
if (foe.stored().hp > 0
|
|
624
|
+
if (foe.stored().hp > 0) try buildRage(battle, player.foe(), log);
|
|
668
625
|
|
|
669
626
|
// For reasons passing understanding, Pokémon Showdown still inflicts residual damage to
|
|
670
627
|
// Bide's user even if the above damage has caused the foe to faint. It's simpler to always
|
|
@@ -685,13 +642,8 @@ fn beforeMove(
|
|
|
685
642
|
try log.start(battle.active(player), .ConfusionSilent);
|
|
686
643
|
}
|
|
687
644
|
try log.move(ident, side.last_selected_move, battle.active(player.foe()), from);
|
|
688
|
-
if (showdown) {
|
|
689
|
-
|
|
690
|
-
// used in useMove and doesn't have the notion of skipping canMove semantics
|
|
691
|
-
side.last_used_move = side.last_selected_move;
|
|
692
|
-
if (handleThrashing(battle, active)) {
|
|
693
|
-
try log.start(battle.active(player), .ConfusionSilent);
|
|
694
|
-
}
|
|
645
|
+
if (showdown and handleThrashing(battle, active)) {
|
|
646
|
+
try log.start(battle.active(player), .ConfusionSilent);
|
|
695
647
|
}
|
|
696
648
|
return .skip_can;
|
|
697
649
|
}
|
|
@@ -706,11 +658,7 @@ fn beforeMove(
|
|
|
706
658
|
side.last_selected_move,
|
|
707
659
|
);
|
|
708
660
|
if (showdown or battle.last_damage != 0) {
|
|
709
|
-
const sub = showdown and foe.active.volatiles.Substitute;
|
|
710
661
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
711
|
-
if (battle.foe(player).stored().hp > 0 and !sub) {
|
|
712
|
-
try buildRage(battle, player.foe(), log);
|
|
713
|
-
}
|
|
714
662
|
}
|
|
715
663
|
return .done;
|
|
716
664
|
}
|
|
@@ -768,7 +716,7 @@ fn canMove(
|
|
|
768
716
|
Effects.thrashing(battle, player);
|
|
769
717
|
} else if (!showdown and move.effect == .Binding) {
|
|
770
718
|
// Pokémon Showdown handles this after hit/miss checks and damage calculation
|
|
771
|
-
Effects.binding(battle, player
|
|
719
|
+
Effects.binding(battle, player);
|
|
772
720
|
}
|
|
773
721
|
|
|
774
722
|
return true;
|
|
@@ -809,7 +757,6 @@ fn doMove(
|
|
|
809
757
|
battle: anytype,
|
|
810
758
|
player: Player,
|
|
811
759
|
mslot: u4,
|
|
812
|
-
rewrap: bool,
|
|
813
760
|
auto: bool,
|
|
814
761
|
residual: *bool,
|
|
815
762
|
log: anytype,
|
|
@@ -840,15 +787,12 @@ fn doMove(
|
|
|
840
787
|
(@enumToInt(move.type.effectiveness(foe.active.types.type1)) == 0 or
|
|
841
788
|
@enumToInt(move.type.effectiveness(foe.active.types.type2)) == 0);
|
|
842
789
|
if (immune and move.effect != .Binding) break :miss true;
|
|
843
|
-
if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe)
|
|
844
|
-
battle.last_damage = 0;
|
|
845
|
-
break :miss true;
|
|
846
|
-
}
|
|
790
|
+
if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) break :miss true;
|
|
847
791
|
break :miss move.target != .Self and !moveHit(battle, player, move, &immune, &mist);
|
|
848
792
|
};
|
|
849
793
|
assert(!immune or miss or (showdown and move.effect == .Binding));
|
|
850
794
|
|
|
851
|
-
var late = showdown
|
|
795
|
+
var late = showdown;
|
|
852
796
|
const skip = status or immune;
|
|
853
797
|
if ((!showdown or (!skip or counter)) and !miss) blk: {
|
|
854
798
|
if (showdown and move.effect.isMulti()) {
|
|
@@ -901,9 +845,9 @@ fn doMove(
|
|
|
901
845
|
|
|
902
846
|
if (!showdown or !miss) {
|
|
903
847
|
if (move.effect == .MirrorMove) {
|
|
904
|
-
return mirrorMove(battle, player, mslot,
|
|
848
|
+
return mirrorMove(battle, player, mslot, auto, residual, log);
|
|
905
849
|
} else if (move.effect == .Metronome) {
|
|
906
|
-
return metronome(battle, player, mslot,
|
|
850
|
+
return metronome(battle, player, mslot, auto, residual, log);
|
|
907
851
|
} else if (move.effect.onEnd()) {
|
|
908
852
|
try onEnd(battle, player, move, log);
|
|
909
853
|
return null;
|
|
@@ -918,7 +862,7 @@ fn doMove(
|
|
|
918
862
|
move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
|
|
919
863
|
if (ohko) {
|
|
920
864
|
try log.immune(foe_ident, .OHKO);
|
|
921
|
-
} else if (immune and !invulnerable
|
|
865
|
+
} else if (immune and !invulnerable) {
|
|
922
866
|
try log.immune(foe_ident, .None);
|
|
923
867
|
} else if (mist) {
|
|
924
868
|
if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
|
|
@@ -950,9 +894,6 @@ fn doMove(
|
|
|
950
894
|
hits = side.active.volatiles.attacks;
|
|
951
895
|
}
|
|
952
896
|
|
|
953
|
-
// Pokémon Showdown only builds Rage for Disable/Explosion (hit/miss) when attacking into a sub
|
|
954
|
-
const sub = showdown and foe.active.volatiles.Substitute;
|
|
955
|
-
|
|
956
897
|
var nullified = false;
|
|
957
898
|
var hit: u4 = 0;
|
|
958
899
|
while (hit < hits) {
|
|
@@ -968,7 +909,7 @@ fn doMove(
|
|
|
968
909
|
if (hit == 0 and ohko) try log.ohko();
|
|
969
910
|
hit += 1;
|
|
970
911
|
if (foe.stored().hp == 0) break;
|
|
971
|
-
if (!late
|
|
912
|
+
if (!late) try buildRage(battle, player.foe(), log);
|
|
972
913
|
// If the substitute breaks during a multi-hit attack, the attack ends
|
|
973
914
|
if (nullified) break;
|
|
974
915
|
}
|
|
@@ -984,7 +925,7 @@ fn doMove(
|
|
|
984
925
|
} else if (showdown) {
|
|
985
926
|
// This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
|
|
986
927
|
if (move.effect == .Binding) {
|
|
987
|
-
Effects.binding(battle, player
|
|
928
|
+
Effects.binding(battle, player);
|
|
988
929
|
if (immune) {
|
|
989
930
|
battle.last_damage = 0;
|
|
990
931
|
// Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
|
|
@@ -1005,8 +946,8 @@ fn doMove(
|
|
|
1005
946
|
|
|
1006
947
|
if (foe.stored().hp == 0) return null;
|
|
1007
948
|
|
|
1008
|
-
// Pokémon Showdown builds Rage at the wrong time for non-MultiHit
|
|
1009
|
-
if (late and
|
|
949
|
+
// Pokémon Showdown builds Rage at the wrong time for non-MultiHit moves
|
|
950
|
+
if (late and move.effect != .Disable) try buildRage(battle, player.foe(), log);
|
|
1010
951
|
|
|
1011
952
|
if (!move.effect.isSpecial()) {
|
|
1012
953
|
// On the cartridge Rage is not considered to be "special" and thus gets executed for a
|
|
@@ -1039,7 +980,8 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
|
|
|
1039
980
|
chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
|
|
1040
981
|
chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
|
|
1041
982
|
|
|
1042
|
-
return
|
|
983
|
+
if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
|
|
984
|
+
return std.math.rotl(u8, battle.rng.next(), 3) < chance;
|
|
1043
985
|
}
|
|
1044
986
|
|
|
1045
987
|
fn calcDamage(
|
|
@@ -1143,36 +1085,50 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
|
1143
1085
|
|
|
1144
1086
|
fn randomizeDamage(battle: anytype) void {
|
|
1145
1087
|
if (battle.last_damage <= 1) return;
|
|
1146
|
-
|
|
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
|
+
|
|
1147
1098
|
battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
|
|
1148
1099
|
}
|
|
1149
1100
|
|
|
1150
1101
|
fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
|
|
1151
1102
|
const side = battle.side(player);
|
|
1152
|
-
const foe = battle.foe(player);
|
|
1153
1103
|
|
|
1154
1104
|
if (!try checkHit(battle, player, move, log)) return null;
|
|
1155
1105
|
|
|
1156
1106
|
battle.last_damage = switch (side.last_selected_move) {
|
|
1157
|
-
.SuperFang => @max(foe.stored().hp / 2, 1),
|
|
1107
|
+
.SuperFang => @max(battle.foe(player).stored().hp / 2, 1),
|
|
1158
1108
|
.SeismicToss, .NightShade => side.stored().level,
|
|
1159
1109
|
.SonicBoom => 20,
|
|
1160
1110
|
.DragonRage => 40,
|
|
1161
1111
|
// GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
|
|
1162
1112
|
.Psywave => power: {
|
|
1163
1113
|
const max = @intCast(u8, @as(u16, side.stored().level) * 3 / 2);
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1114
|
+
if (showdown) {
|
|
1115
|
+
break :power battle.rng.range(u8, 0, max);
|
|
1116
|
+
} else {
|
|
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
|
+
}
|
|
1167
1124
|
},
|
|
1168
1125
|
else => unreachable,
|
|
1169
1126
|
};
|
|
1170
1127
|
|
|
1171
1128
|
if (battle.last_damage == 0) return if (showdown) null else Result.Error;
|
|
1172
1129
|
|
|
1173
|
-
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1174
1130
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
1175
|
-
if (battle.foe(player).stored().hp > 0
|
|
1131
|
+
if (battle.foe(player).stored().hp > 0) try buildRage(battle, player.foe(), log);
|
|
1176
1132
|
|
|
1177
1133
|
return null;
|
|
1178
1134
|
}
|
|
@@ -1221,9 +1177,7 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
|
|
|
1221
1177
|
// Pokémon Showdown calls checkHit before Counter
|
|
1222
1178
|
if (!showdown and !try checkHit(battle, player, move, log)) return null;
|
|
1223
1179
|
|
|
1224
|
-
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1225
1180
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
1226
|
-
if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
|
|
1227
1181
|
return null;
|
|
1228
1182
|
}
|
|
1229
1183
|
|
|
@@ -1267,7 +1221,6 @@ fn mirrorMove(
|
|
|
1267
1221
|
battle: anytype,
|
|
1268
1222
|
player: Player,
|
|
1269
1223
|
mslot: u4,
|
|
1270
|
-
rewrap: bool,
|
|
1271
1224
|
auto: bool,
|
|
1272
1225
|
residual: *bool,
|
|
1273
1226
|
log: anytype,
|
|
@@ -1285,25 +1238,36 @@ fn mirrorMove(
|
|
|
1285
1238
|
incrementPP(side, mslot);
|
|
1286
1239
|
|
|
1287
1240
|
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
|
|
1288
|
-
return doMove(battle, player, mslot,
|
|
1241
|
+
return doMove(battle, player, mslot, auto, residual, log);
|
|
1289
1242
|
}
|
|
1290
1243
|
|
|
1291
1244
|
fn metronome(
|
|
1292
1245
|
battle: anytype,
|
|
1293
1246
|
player: Player,
|
|
1294
1247
|
mslot: u4,
|
|
1295
|
-
rewrap: bool,
|
|
1296
1248
|
auto: bool,
|
|
1297
1249
|
residual: *bool,
|
|
1298
1250
|
log: anytype,
|
|
1299
1251
|
) !?Result {
|
|
1300
1252
|
var side = battle.side(player);
|
|
1301
1253
|
|
|
1302
|
-
side.last_selected_move =
|
|
1254
|
+
side.last_selected_move = if (showdown) blk: {
|
|
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
|
+
|
|
1303
1267
|
incrementPP(side, mslot);
|
|
1304
1268
|
|
|
1305
1269
|
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
|
|
1306
|
-
return doMove(battle, player, mslot,
|
|
1270
|
+
return doMove(battle, player, mslot, auto, residual, log);
|
|
1307
1271
|
}
|
|
1308
1272
|
|
|
1309
1273
|
fn checkHit(battle: anytype, player: Player, move: Move.Data, log: anytype) !bool {
|
|
@@ -1357,24 +1321,23 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
|
|
|
1357
1321
|
}
|
|
1358
1322
|
|
|
1359
1323
|
// GLITCH: Thrash / Petal Dance / Rage get their accuracy overwritten on subsequent hits
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
var accuracy = if (overwritten) state else @as(u16, Gen12.percent(move.accuracy));
|
|
1324
|
+
var state = &side.active.volatiles.state;
|
|
1325
|
+
const overwritten = (move.effect == .Thrashing or move.effect == .Rage) and state.* > 0;
|
|
1326
|
+
assert(!overwritten or (0 < state.* and state.* <= 255 and !side.active.volatiles.Bide));
|
|
1327
|
+
var accuracy = if (overwritten) state.* else @as(u16, Gen12.percent(move.accuracy));
|
|
1366
1328
|
var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
|
|
1367
1329
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1368
1330
|
boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
|
|
1369
1331
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1370
1332
|
accuracy = @min(255, @max(1, accuracy));
|
|
1371
1333
|
|
|
1372
|
-
|
|
1373
|
-
if (showdown) overwrite = side.active.volatiles.Rage or side.active.volatiles.Thrashing;
|
|
1374
|
-
if (overwrite) side.active.volatiles.state = accuracy;
|
|
1334
|
+
if (move.effect == .Thrashing or move.effect == .Rage) state.* = accuracy;
|
|
1375
1335
|
|
|
1376
1336
|
// GLITCH: max accuracy is 255 so 1/256 chance of miss
|
|
1377
|
-
break :miss
|
|
1337
|
+
break :miss if (showdown)
|
|
1338
|
+
!battle.rng.chance(u8, @intCast(u8, accuracy), 256)
|
|
1339
|
+
else
|
|
1340
|
+
battle.rng.next() >= accuracy;
|
|
1378
1341
|
};
|
|
1379
1342
|
|
|
1380
1343
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1506,15 +1469,14 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
|
|
|
1506
1469
|
damage *= volatiles.toxic;
|
|
1507
1470
|
}
|
|
1508
1471
|
|
|
1472
|
+
// Pokémon Showdown erroneously updates last damage with uncapped Leech Seed damage
|
|
1473
|
+
if (showdown) battle.last_damage = damage;
|
|
1474
|
+
|
|
1509
1475
|
const amount = @min(damage, stored.hp);
|
|
1510
1476
|
stored.hp -= amount;
|
|
1511
1477
|
|
|
1512
1478
|
// As above, Pokémon Showdown uses damageOf but its not relevant
|
|
1513
|
-
if (amount > 0)
|
|
1514
|
-
try log.damage(ident, stored, .LeechSeed);
|
|
1515
|
-
// Pokémon Showdown erroneously updates last damage with uncapped Leech Seed damage
|
|
1516
|
-
if (showdown) battle.last_damage = damage;
|
|
1517
|
-
}
|
|
1479
|
+
if (amount > 0) try log.damage(ident, stored, .LeechSeed);
|
|
1518
1480
|
|
|
1519
1481
|
const before = foe_stored.hp;
|
|
1520
1482
|
// Uncapped damage is added back to the foe
|
|
@@ -1601,42 +1563,6 @@ fn checkEBC(battle: anytype) bool {
|
|
|
1601
1563
|
return false;
|
|
1602
1564
|
}
|
|
1603
1565
|
|
|
1604
|
-
fn disabled(side: *Side, ident: ID, log: anytype) !bool {
|
|
1605
|
-
if (side.active.volatiles.disabled_move != 0) {
|
|
1606
|
-
// A Pokémon that transforms after being disabled may end up with less move slots
|
|
1607
|
-
const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
|
|
1608
|
-
// side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
|
|
1609
|
-
const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
|
|
1610
|
-
m
|
|
1611
|
-
else
|
|
1612
|
-
side.last_selected_move;
|
|
1613
|
-
if (m != .None and m == last) {
|
|
1614
|
-
side.active.volatiles.Charging = false;
|
|
1615
|
-
try log.disabled(ident, last);
|
|
1616
|
-
return true;
|
|
1617
|
-
}
|
|
1618
|
-
}
|
|
1619
|
-
return false;
|
|
1620
|
-
}
|
|
1621
|
-
|
|
1622
|
-
inline fn buildRage(battle: anytype, who: Player, log: anytype) !void {
|
|
1623
|
-
const side = battle.side(who);
|
|
1624
|
-
if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
|
|
1625
|
-
try Effects.boost(battle, who, Move.get(.Rage), log);
|
|
1626
|
-
}
|
|
1627
|
-
}
|
|
1628
|
-
|
|
1629
|
-
fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
|
|
1630
|
-
var volatiles = &active.volatiles;
|
|
1631
|
-
assert(volatiles.Thrashing);
|
|
1632
|
-
if (volatiles.attacks > 0) return false;
|
|
1633
|
-
|
|
1634
|
-
volatiles.Thrashing = false;
|
|
1635
|
-
volatiles.Confusion = true;
|
|
1636
|
-
volatiles.confusion = Rolls.confusionDuration(battle);
|
|
1637
|
-
return true;
|
|
1638
|
-
}
|
|
1639
|
-
|
|
1640
1566
|
inline fn onBegin(
|
|
1641
1567
|
battle: anytype,
|
|
1642
1568
|
player: Player,
|
|
@@ -1727,7 +1653,10 @@ pub const Effects = struct {
|
|
|
1727
1653
|
side.active.volatiles.Bide = true;
|
|
1728
1654
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1729
1655
|
side.active.volatiles.state = 0;
|
|
1730
|
-
side.active.volatiles.attacks =
|
|
1656
|
+
side.active.volatiles.attacks = @intCast(u3, if (showdown)
|
|
1657
|
+
battle.rng.range(u4, 2, 4)
|
|
1658
|
+
else
|
|
1659
|
+
(battle.rng.next() & 1) + 2);
|
|
1731
1660
|
|
|
1732
1661
|
try log.start(battle.active(player), .Bide);
|
|
1733
1662
|
}
|
|
@@ -1749,8 +1678,14 @@ pub const Effects = struct {
|
|
|
1749
1678
|
return;
|
|
1750
1679
|
}
|
|
1751
1680
|
|
|
1752
|
-
|
|
1753
|
-
|
|
1681
|
+
const chance = !foe.active.types.includes(move.type) and if (showdown)
|
|
1682
|
+
battle.rng.chance(u8, @as(u8, if (move.effect == .BurnChance1) 26 else 77), 256)
|
|
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;
|
|
1754
1689
|
|
|
1755
1690
|
foe_stored.status = Status.init(.BRN);
|
|
1756
1691
|
foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
|
|
@@ -1774,7 +1709,11 @@ pub const Effects = struct {
|
|
|
1774
1709
|
const sub = foe.active.volatiles.Substitute;
|
|
1775
1710
|
|
|
1776
1711
|
if (move.effect == .ConfusionChance) {
|
|
1777
|
-
if (
|
|
1712
|
+
const chance = if (showdown)
|
|
1713
|
+
battle.rng.chance(u8, 25, 256)
|
|
1714
|
+
else
|
|
1715
|
+
battle.rng.next() < Gen12.percent(10);
|
|
1716
|
+
if (!chance) return;
|
|
1778
1717
|
} else {
|
|
1779
1718
|
if (showdown) {
|
|
1780
1719
|
if (!try checkHit(battle, player, move, log)) {
|
|
@@ -1793,7 +1732,10 @@ pub const Effects = struct {
|
|
|
1793
1732
|
|
|
1794
1733
|
if (foe.active.volatiles.Confusion) return;
|
|
1795
1734
|
foe.active.volatiles.Confusion = true;
|
|
1796
|
-
foe.active.volatiles.confusion =
|
|
1735
|
+
foe.active.volatiles.confusion = @intCast(u3, if (showdown)
|
|
1736
|
+
battle.rng.range(u8, 2, 6)
|
|
1737
|
+
else
|
|
1738
|
+
(battle.rng.next() & 3) + 2);
|
|
1797
1739
|
|
|
1798
1740
|
try log.start(battle.active(player.foe()), .Confusion);
|
|
1799
1741
|
}
|
|
@@ -1843,8 +1785,11 @@ pub const Effects = struct {
|
|
|
1843
1785
|
return Result.Error;
|
|
1844
1786
|
}
|
|
1845
1787
|
|
|
1846
|
-
volatiles.disabled_move = @intCast(u3,
|
|
1847
|
-
volatiles.disabled_duration =
|
|
1788
|
+
volatiles.disabled_move = @intCast(u3, randomMoveSlot(&battle.rng, &foe.active.moves, n));
|
|
1789
|
+
volatiles.disabled_duration = @intCast(u4, if (showdown)
|
|
1790
|
+
battle.rng.range(u8, 1, 9)
|
|
1791
|
+
else
|
|
1792
|
+
(battle.rng.next() & 7) + 1);
|
|
1848
1793
|
|
|
1849
1794
|
try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
|
|
1850
1795
|
return null;
|
|
@@ -1856,7 +1801,7 @@ pub const Effects = struct {
|
|
|
1856
1801
|
|
|
1857
1802
|
if (battle.last_damage == 0) {
|
|
1858
1803
|
assert(showdown);
|
|
1859
|
-
|
|
1804
|
+
return;
|
|
1860
1805
|
}
|
|
1861
1806
|
|
|
1862
1807
|
const drain = @max(battle.last_damage / 2, 1);
|
|
@@ -1883,7 +1828,15 @@ pub const Effects = struct {
|
|
|
1883
1828
|
var volatiles = &battle.foe(player).active.volatiles;
|
|
1884
1829
|
|
|
1885
1830
|
if (volatiles.Substitute) return;
|
|
1886
|
-
|
|
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;
|
|
1887
1840
|
|
|
1888
1841
|
volatiles.Flinch = true;
|
|
1889
1842
|
volatiles.Recharging = false;
|
|
@@ -1909,8 +1862,12 @@ pub const Effects = struct {
|
|
|
1909
1862
|
return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
|
|
1910
1863
|
}
|
|
1911
1864
|
|
|
1912
|
-
|
|
1913
|
-
|
|
1865
|
+
const chance = !foe.active.types.includes(move.type) and if (showdown)
|
|
1866
|
+
battle.rng.chance(u8, 26, 256)
|
|
1867
|
+
else
|
|
1868
|
+
battle.rng.next() < 1 + Gen12.percent(10);
|
|
1869
|
+
if (!chance) return;
|
|
1870
|
+
|
|
1914
1871
|
// Freeze Clause Mod
|
|
1915
1872
|
if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
1916
1873
|
|
|
@@ -2055,7 +2012,7 @@ pub const Effects = struct {
|
|
|
2055
2012
|
}
|
|
2056
2013
|
if (!try checkHit(battle, player, move, log)) return;
|
|
2057
2014
|
|
|
2058
|
-
const rslot =
|
|
2015
|
+
const rslot = randomMoveSlot(&battle.rng, &foe.active.moves, 0);
|
|
2059
2016
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
2060
2017
|
|
|
2061
2018
|
try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
|
|
@@ -2076,8 +2033,7 @@ pub const Effects = struct {
|
|
|
2076
2033
|
assert(!side.active.volatiles.MultiHit);
|
|
2077
2034
|
side.active.volatiles.MultiHit = true;
|
|
2078
2035
|
|
|
2079
|
-
side.active.volatiles.attacks =
|
|
2080
|
-
if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
|
|
2036
|
+
side.active.volatiles.attacks = if (move.effect == .MultiHit) distribution(battle) else 2;
|
|
2081
2037
|
}
|
|
2082
2038
|
|
|
2083
2039
|
fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2121,8 +2077,14 @@ pub const Effects = struct {
|
|
|
2121
2077
|
}
|
|
2122
2078
|
|
|
2123
2079
|
// Body Slam can't paralyze a Normal type Pokémon
|
|
2124
|
-
|
|
2125
|
-
|
|
2080
|
+
const chance = !foe.active.types.includes(move.type) and if (showdown)
|
|
2081
|
+
battle.rng.chance(u8, @as(u8, if (move.effect == .ParalyzeChance1) 26 else 77), 256)
|
|
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;
|
|
2126
2088
|
|
|
2127
2089
|
foe_stored.status = Status.init(.PAR);
|
|
2128
2090
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
@@ -2165,7 +2127,16 @@ pub const Effects = struct {
|
|
|
2165
2127
|
|
|
2166
2128
|
if (move.effect == .Poison) {
|
|
2167
2129
|
if (!showdown and !try checkHit(battle, player, move, log)) return;
|
|
2168
|
-
} else
|
|
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
|
+
}
|
|
2169
2140
|
|
|
2170
2141
|
foe_stored.status = Status.init(.PSN);
|
|
2171
2142
|
if (toxic) {
|
|
@@ -2179,8 +2150,10 @@ pub const Effects = struct {
|
|
|
2179
2150
|
|
|
2180
2151
|
fn rage(battle: anytype, player: Player) !void {
|
|
2181
2152
|
var volatiles = &battle.side(player).active.volatiles;
|
|
2182
|
-
|
|
2153
|
+
if (volatiles.Rage) return;
|
|
2183
2154
|
volatiles.Rage = true;
|
|
2155
|
+
assert(!volatiles.Bide);
|
|
2156
|
+
volatiles.state = 0;
|
|
2184
2157
|
}
|
|
2185
2158
|
|
|
2186
2159
|
fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
|
|
@@ -2232,7 +2205,16 @@ pub const Effects = struct {
|
|
|
2232
2205
|
}
|
|
2233
2206
|
foe.active.volatiles.Recharging = false;
|
|
2234
2207
|
|
|
2235
|
-
|
|
2208
|
+
const duration = @intCast(u3, if (showdown)
|
|
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);
|
|
2236
2218
|
try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
|
|
2237
2219
|
}
|
|
2238
2220
|
|
|
@@ -2285,7 +2267,11 @@ pub const Effects = struct {
|
|
|
2285
2267
|
assert(!volatiles.Bide);
|
|
2286
2268
|
|
|
2287
2269
|
volatiles.Thrashing = true;
|
|
2288
|
-
volatiles.
|
|
2270
|
+
volatiles.state = 0;
|
|
2271
|
+
volatiles.attacks = @intCast(u3, if (showdown)
|
|
2272
|
+
battle.rng.range(u8, 2, 4)
|
|
2273
|
+
else
|
|
2274
|
+
(battle.rng.next() & 1) + 2);
|
|
2289
2275
|
}
|
|
2290
2276
|
|
|
2291
2277
|
fn transform(battle: anytype, player: Player, log: anytype) !void {
|
|
@@ -2317,7 +2303,7 @@ pub const Effects = struct {
|
|
|
2317
2303
|
try log.transform(battle.active(player), foe_ident);
|
|
2318
2304
|
}
|
|
2319
2305
|
|
|
2320
|
-
fn binding(battle: anytype, player: Player
|
|
2306
|
+
fn binding(battle: anytype, player: Player) void {
|
|
2321
2307
|
var side = battle.side(player);
|
|
2322
2308
|
var foe = battle.foe(player);
|
|
2323
2309
|
|
|
@@ -2325,11 +2311,9 @@ pub const Effects = struct {
|
|
|
2325
2311
|
side.active.volatiles.Binding = true;
|
|
2326
2312
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2327
2313
|
// (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
|
|
2328
|
-
if (!showdown)
|
|
2329
|
-
foe.active.volatiles.Recharging = false;
|
|
2330
|
-
} else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
|
|
2314
|
+
if (!showdown) foe.active.volatiles.Recharging = false;
|
|
2331
2315
|
|
|
2332
|
-
side.active.volatiles.attacks =
|
|
2316
|
+
side.active.volatiles.attacks = distribution(battle) - 1;
|
|
2333
2317
|
}
|
|
2334
2318
|
|
|
2335
2319
|
fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2356,11 +2340,9 @@ pub const Effects = struct {
|
|
|
2356
2340
|
return try log.fail(ident, .None);
|
|
2357
2341
|
}
|
|
2358
2342
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2359
|
-
const stat = unmodifiedStats(battle,
|
|
2343
|
+
const stat = unmodifiedStats(battle, player).atk;
|
|
2360
2344
|
stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2361
2345
|
try log.boost(ident, reason, n);
|
|
2362
|
-
// Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
|
|
2363
|
-
if (showdown and move.effect == .Rage) return;
|
|
2364
2346
|
},
|
|
2365
2347
|
.DefenseUp1, .DefenseUp2 => {
|
|
2366
2348
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
@@ -2376,7 +2358,7 @@ pub const Effects = struct {
|
|
|
2376
2358
|
return try log.fail(ident, .None);
|
|
2377
2359
|
}
|
|
2378
2360
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2379
|
-
const stat = unmodifiedStats(battle,
|
|
2361
|
+
const stat = unmodifiedStats(battle, player).def;
|
|
2380
2362
|
stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2381
2363
|
try log.boost(ident, .Defense, n);
|
|
2382
2364
|
},
|
|
@@ -2393,7 +2375,7 @@ pub const Effects = struct {
|
|
|
2393
2375
|
return try log.fail(ident, .None);
|
|
2394
2376
|
}
|
|
2395
2377
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2396
|
-
const stat = unmodifiedStats(battle,
|
|
2378
|
+
const stat = unmodifiedStats(battle, player).spe;
|
|
2397
2379
|
stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2398
2380
|
try log.boost(ident, .Speed, 2);
|
|
2399
2381
|
},
|
|
@@ -2413,7 +2395,7 @@ pub const Effects = struct {
|
|
|
2413
2395
|
return try log.fail(ident, .None);
|
|
2414
2396
|
}
|
|
2415
2397
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2416
|
-
const stat = unmodifiedStats(battle,
|
|
2398
|
+
const stat = unmodifiedStats(battle, player).spc;
|
|
2417
2399
|
stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2418
2400
|
try log.boost(ident, .SpecialAttack, n);
|
|
2419
2401
|
try log.boost(ident, .SpecialDefense, n);
|
|
@@ -2440,7 +2422,11 @@ pub const Effects = struct {
|
|
|
2440
2422
|
}
|
|
2441
2423
|
|
|
2442
2424
|
if (move.effect.isStatDownChance()) {
|
|
2443
|
-
if (
|
|
2425
|
+
const chance = if (showdown)
|
|
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;
|
|
2444
2430
|
} else if (!showdown and !try checkHit(battle, player, move, log)) {
|
|
2445
2431
|
return; // checkHit already checks for Invulnerable
|
|
2446
2432
|
}
|
|
@@ -2462,7 +2448,7 @@ pub const Effects = struct {
|
|
|
2462
2448
|
return try log.fail(foe_ident, .None);
|
|
2463
2449
|
}
|
|
2464
2450
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2465
|
-
const stat = unmodifiedStats(battle, foe).atk;
|
|
2451
|
+
const stat = unmodifiedStats(battle, player.foe()).atk;
|
|
2466
2452
|
stats.atk = @max(1, stat * mod[0] / mod[1]);
|
|
2467
2453
|
try log.boost(foe_ident, .Attack, -1);
|
|
2468
2454
|
},
|
|
@@ -2480,7 +2466,7 @@ pub const Effects = struct {
|
|
|
2480
2466
|
return try log.fail(foe_ident, .None);
|
|
2481
2467
|
}
|
|
2482
2468
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2483
|
-
const stat = unmodifiedStats(battle, foe).def;
|
|
2469
|
+
const stat = unmodifiedStats(battle, player.foe()).def;
|
|
2484
2470
|
stats.def = @max(1, stat * mod[0] / mod[1]);
|
|
2485
2471
|
try log.boost(foe_ident, .Defense, -@as(i8, n));
|
|
2486
2472
|
},
|
|
@@ -2497,7 +2483,7 @@ pub const Effects = struct {
|
|
|
2497
2483
|
return try log.fail(foe_ident, .None);
|
|
2498
2484
|
}
|
|
2499
2485
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2500
|
-
const stat = unmodifiedStats(battle, foe).spe;
|
|
2486
|
+
const stat = unmodifiedStats(battle, player.foe()).spe;
|
|
2501
2487
|
stats.spe = @max(1, stat * mod[0] / mod[1]);
|
|
2502
2488
|
try log.boost(foe_ident, .Speed, -1);
|
|
2503
2489
|
assert(boosts.spe >= -6);
|
|
@@ -2517,7 +2503,7 @@ pub const Effects = struct {
|
|
|
2517
2503
|
return try log.fail(foe_ident, .None);
|
|
2518
2504
|
}
|
|
2519
2505
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2520
|
-
const stat = unmodifiedStats(battle, foe).spc;
|
|
2506
|
+
const stat = unmodifiedStats(battle, player.foe()).spc;
|
|
2521
2507
|
stats.spc = @max(1, stat * mod[0] / mod[1]);
|
|
2522
2508
|
try log.boost(foe_ident, .SpecialAttack, -1);
|
|
2523
2509
|
try log.boost(foe_ident, .SpecialDefense, -1);
|
|
@@ -2536,7 +2522,8 @@ pub const Effects = struct {
|
|
|
2536
2522
|
}
|
|
2537
2523
|
};
|
|
2538
2524
|
|
|
2539
|
-
fn unmodifiedStats(battle: anytype,
|
|
2525
|
+
fn unmodifiedStats(battle: anytype, who: Player) *Stats(u16) {
|
|
2526
|
+
const side = battle.side(who);
|
|
2540
2527
|
if (!side.active.volatiles.Transform) return &side.stored().stats;
|
|
2541
2528
|
const id = ID.from(side.active.volatiles.transform);
|
|
2542
2529
|
return &battle.side(id.player).pokemon[id.id - 1].stats;
|
|
@@ -2602,140 +2589,75 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
|
|
|
2602
2589
|
}
|
|
2603
2590
|
}
|
|
2604
2591
|
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
return std.math.rotl(u8, battle.rng.next(), 3) < chance;
|
|
2614
|
-
}
|
|
2615
|
-
|
|
2616
|
-
inline fn damage(battle: anytype) u8 {
|
|
2617
|
-
if (showdown) return battle.rng.range(u8, 217, 256);
|
|
2618
|
-
while (true) {
|
|
2619
|
-
const r = std.math.rotr(u8, battle.rng.next(), 1);
|
|
2620
|
-
if (r >= 217) return r;
|
|
2592
|
+
fn disabled(side: *Side, ident: ID, log: anytype) !bool {
|
|
2593
|
+
if (side.active.volatiles.disabled_move != 0) {
|
|
2594
|
+
// A Pokémon that transforms after being disabled may end up with less move slots
|
|
2595
|
+
const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
|
|
2596
|
+
if (m != .None and m == side.last_selected_move) {
|
|
2597
|
+
side.active.volatiles.Charging = false;
|
|
2598
|
+
try log.disabled(ident, side.last_selected_move);
|
|
2599
|
+
return true;
|
|
2621
2600
|
}
|
|
2622
2601
|
}
|
|
2602
|
+
return false;
|
|
2603
|
+
}
|
|
2623
2604
|
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
inline fn confusionDuration(battle: anytype) u3 {
|
|
2630
|
-
if (showdown) return @intCast(u3, battle.rng.range(u8, 2, 6));
|
|
2631
|
-
return @intCast(u3, (battle.rng.next() & 3) + 2);
|
|
2632
|
-
}
|
|
2633
|
-
|
|
2634
|
-
inline fn confused(battle: anytype) bool {
|
|
2635
|
-
if (showdown) return !battle.rng.chance(u8, 128, 256);
|
|
2636
|
-
return battle.rng.next() >= Gen12.percent(50) + 1;
|
|
2637
|
-
}
|
|
2638
|
-
|
|
2639
|
-
inline fn paralyzed(battle: anytype) bool {
|
|
2640
|
-
if (showdown) return battle.rng.chance(u8, 63, 256);
|
|
2641
|
-
return battle.rng.next() < Gen12.percent(25);
|
|
2605
|
+
inline fn buildRage(battle: anytype, who: Player, log: anytype) !void {
|
|
2606
|
+
const side = battle.side(who);
|
|
2607
|
+
if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
|
|
2608
|
+
try Effects.boost(battle, who, Move.get(.Rage), log);
|
|
2642
2609
|
}
|
|
2610
|
+
}
|
|
2643
2611
|
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2612
|
+
fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
|
|
2613
|
+
var volatiles = &active.volatiles;
|
|
2614
|
+
assert(volatiles.Thrashing);
|
|
2615
|
+
if (volatiles.attacks > 0) return false;
|
|
2648
2616
|
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2617
|
+
volatiles.Thrashing = false;
|
|
2618
|
+
volatiles.Confusion = true;
|
|
2619
|
+
volatiles.confusion = @intCast(u3, if (showdown)
|
|
2620
|
+
battle.rng.range(u8, 2, 6)
|
|
2621
|
+
else
|
|
2622
|
+
(battle.rng.next() & 3) + 2);
|
|
2653
2623
|
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
return battle.rng.next() < 1 + (if (low) Gen12.percent(20) else Gen12.percent(40));
|
|
2657
|
-
}
|
|
2624
|
+
return true;
|
|
2625
|
+
}
|
|
2658
2626
|
|
|
2659
|
-
|
|
2660
|
-
if (showdown) return @intCast(u3, battle.rng.range(u8, 1, 8));
|
|
2661
|
-
while (true) {
|
|
2662
|
-
const r = battle.rng.next() & 7;
|
|
2663
|
-
if (r != 0) return @intCast(u3, r);
|
|
2664
|
-
}
|
|
2665
|
-
}
|
|
2627
|
+
const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
|
|
2666
2628
|
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2629
|
+
fn distribution(battle: anytype) u3 {
|
|
2630
|
+
if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
|
|
2631
|
+
const r = (battle.rng.next() & 3);
|
|
2632
|
+
return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
|
|
2633
|
+
}
|
|
2671
2634
|
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
if (
|
|
2687
|
-
r -= 1;
|
|
2688
|
-
if (r == 0) break;
|
|
2689
|
-
}
|
|
2635
|
+
fn randomMoveSlot(rand: anytype, moves: []MoveSlot, check_pp: u4) u4 {
|
|
2636
|
+
if (showdown) {
|
|
2637
|
+
if (check_pp == 0) {
|
|
2638
|
+
var i: usize = moves.len;
|
|
2639
|
+
while (i > 0) {
|
|
2640
|
+
i -= 1;
|
|
2641
|
+
if (moves[i].id != .None) return rand.range(u4, 0, @intCast(u4, i + 1)) + 1;
|
|
2642
|
+
}
|
|
2643
|
+
} else {
|
|
2644
|
+
var r = rand.range(u4, 0, @intCast(u4, check_pp)) + 1;
|
|
2645
|
+
var i: usize = 0;
|
|
2646
|
+
while (i < moves.len and r > 0) : (i += 1) {
|
|
2647
|
+
if (moves[i].pp > 0) {
|
|
2648
|
+
r -= 1;
|
|
2649
|
+
if (r == 0) break;
|
|
2690
2650
|
}
|
|
2691
|
-
return @intCast(u4, i + 1);
|
|
2692
2651
|
}
|
|
2652
|
+
return @intCast(u4, i + 1);
|
|
2693
2653
|
}
|
|
2694
|
-
|
|
2695
|
-
while (true) {
|
|
2696
|
-
const r = @intCast(u4, battle.rng.next() & 3);
|
|
2697
|
-
if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
|
|
2698
|
-
}
|
|
2699
|
-
}
|
|
2700
|
-
|
|
2701
|
-
inline fn disableDuration(battle: anytype) u4 {
|
|
2702
|
-
if (showdown) return @intCast(u4, battle.rng.range(u8, 1, 9));
|
|
2703
|
-
return @intCast(u4, (battle.rng.next() & 7) + 1);
|
|
2704
|
-
}
|
|
2705
|
-
|
|
2706
|
-
inline fn metronome(battle: anytype) Move {
|
|
2707
|
-
if (showdown) {
|
|
2708
|
-
const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
|
|
2709
|
-
const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
|
|
2710
|
-
return @intToEnum(Move, r + mod);
|
|
2711
|
-
}
|
|
2712
|
-
while (true) {
|
|
2713
|
-
const r = battle.rng.next();
|
|
2714
|
-
if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
|
|
2715
|
-
if (r >= @enumToInt(Move.Struggle)) continue;
|
|
2716
|
-
return @intToEnum(Move, r);
|
|
2717
|
-
}
|
|
2718
|
-
}
|
|
2719
|
-
|
|
2720
|
-
inline fn psywave(battle: anytype, max: u8) u8 {
|
|
2721
|
-
if (showdown) return battle.rng.range(u8, 0, max);
|
|
2722
|
-
while (true) {
|
|
2723
|
-
const r = battle.rng.next();
|
|
2724
|
-
if (r < max) return r;
|
|
2725
|
-
}
|
|
2726
|
-
}
|
|
2727
|
-
|
|
2728
|
-
inline fn distribution(battle: anytype) u3 {
|
|
2729
|
-
if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
|
|
2730
|
-
const r = (battle.rng.next() & 3);
|
|
2731
|
-
return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
|
|
2732
2654
|
}
|
|
2733
2655
|
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2656
|
+
while (true) {
|
|
2657
|
+
const r = @intCast(u4, rand.next() & 3);
|
|
2658
|
+
if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
|
|
2737
2659
|
}
|
|
2738
|
-
}
|
|
2660
|
+
}
|
|
2739
2661
|
|
|
2740
2662
|
test "RNG agreement" {
|
|
2741
2663
|
if (!showdown) return;
|
|
@@ -2835,7 +2757,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
2835
2757
|
while (slot <= 4) : (slot += 1) {
|
|
2836
2758
|
const m = active.moves[slot - 1];
|
|
2837
2759
|
if (m.id == .None) break;
|
|
2838
|
-
if (m.id ==
|
|
2760
|
+
if (m.id == side.last_selected_move) {
|
|
2839
2761
|
// Pokémon Showdown displays Struggle if limited to Bide but unable to pick
|
|
2840
2762
|
const struggle =
|
|
2841
2763
|
m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);
|