@pkmn/engine 0.1.0-dev.47feedd9 → 0.1.0-dev.53335a32
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 +34 -24
- package/package.json +29 -40
- package/src/bin/install-pkmn-engine +33 -60
- 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 +33 -48
- package/src/lib/gen1/data.zig +6 -6
- package/src/lib/gen1/helpers.zig +7 -10
- package/src/lib/gen1/mechanics.zig +283 -342
- 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 -396
- 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,15 +486,13 @@ fn beforeMove(
|
|
|
524
486
|
return .done;
|
|
525
487
|
}
|
|
526
488
|
|
|
527
|
-
if (
|
|
489
|
+
if (foe.active.volatiles.Binding) {
|
|
528
490
|
try log.cant(ident, .Bound);
|
|
529
491
|
return .done;
|
|
530
492
|
}
|
|
531
493
|
|
|
532
|
-
if (volatiles.Flinch) {
|
|
533
|
-
|
|
534
|
-
// Pokémon can sometimes flinch multiple times from the same original hit
|
|
535
|
-
if (!showdown) volatiles.Flinch = false;
|
|
494
|
+
if (!showdown and volatiles.Flinch) {
|
|
495
|
+
volatiles.Flinch = false;
|
|
536
496
|
try log.cant(ident, .Flinch);
|
|
537
497
|
return .done;
|
|
538
498
|
}
|
|
@@ -553,6 +513,13 @@ fn beforeMove(
|
|
|
553
513
|
// Pokémon Showdown's disable condition has a single onBeforeMove handler
|
|
554
514
|
if (showdown and try disabled(side, ident, log)) return .done;
|
|
555
515
|
|
|
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
|
+
|
|
556
523
|
// This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
|
|
557
524
|
// before the side had a selected a move - we simply need to assume this leads to a desync
|
|
558
525
|
if (side.last_selected_move == .None) {
|
|
@@ -570,7 +537,12 @@ fn beforeMove(
|
|
|
570
537
|
} else {
|
|
571
538
|
try log.activate(ident, .Confusion);
|
|
572
539
|
|
|
573
|
-
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) {
|
|
574
546
|
assert(!volatiles.MultiHit);
|
|
575
547
|
if (!volatiles.Rage) volatiles.state = 0;
|
|
576
548
|
volatiles.Bide = false;
|
|
@@ -596,12 +568,8 @@ fn beforeMove(
|
|
|
596
568
|
player
|
|
597
569
|
else
|
|
598
570
|
player.foe();
|
|
599
|
-
|
|
600
|
-
const uncapped = battle.last_damage;
|
|
601
571
|
// Skipping adjustDamage / randomizeDamage / checkHit
|
|
602
572
|
_ = try applyDamage(battle, player, target, .Confusion, log);
|
|
603
|
-
// Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
|
|
604
|
-
if (showdown) battle.last_damage = uncapped;
|
|
605
573
|
|
|
606
574
|
return .done;
|
|
607
575
|
}
|
|
@@ -610,15 +578,22 @@ fn beforeMove(
|
|
|
610
578
|
|
|
611
579
|
if (!showdown and try disabled(side, ident, log)) return .done;
|
|
612
580
|
|
|
613
|
-
if (Status.is(stored.status, .PAR)
|
|
614
|
-
if (
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
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
|
+
}
|
|
622
597
|
}
|
|
623
598
|
|
|
624
599
|
if (volatiles.Bide) {
|
|
@@ -645,9 +620,8 @@ fn beforeMove(
|
|
|
645
620
|
return .done;
|
|
646
621
|
}
|
|
647
622
|
|
|
648
|
-
const sub = showdown and foe.active.volatiles.Substitute;
|
|
649
623
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
650
|
-
if (foe.stored().hp > 0
|
|
624
|
+
if (foe.stored().hp > 0) try buildRage(battle, player.foe(), log);
|
|
651
625
|
|
|
652
626
|
// For reasons passing understanding, Pokémon Showdown still inflicts residual damage to
|
|
653
627
|
// Bide's user even if the above damage has caused the foe to faint. It's simpler to always
|
|
@@ -668,13 +642,8 @@ fn beforeMove(
|
|
|
668
642
|
try log.start(battle.active(player), .ConfusionSilent);
|
|
669
643
|
}
|
|
670
644
|
try log.move(ident, side.last_selected_move, battle.active(player.foe()), from);
|
|
671
|
-
if (showdown) {
|
|
672
|
-
|
|
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
|
-
}
|
|
645
|
+
if (showdown and handleThrashing(battle, active)) {
|
|
646
|
+
try log.start(battle.active(player), .ConfusionSilent);
|
|
678
647
|
}
|
|
679
648
|
return .skip_can;
|
|
680
649
|
}
|
|
@@ -689,11 +658,7 @@ fn beforeMove(
|
|
|
689
658
|
side.last_selected_move,
|
|
690
659
|
);
|
|
691
660
|
if (showdown or battle.last_damage != 0) {
|
|
692
|
-
const sub = showdown and foe.active.volatiles.Substitute;
|
|
693
661
|
_ = 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
|
-
}
|
|
697
662
|
}
|
|
698
663
|
return .done;
|
|
699
664
|
}
|
|
@@ -751,7 +716,7 @@ fn canMove(
|
|
|
751
716
|
Effects.thrashing(battle, player);
|
|
752
717
|
} else if (!showdown and move.effect == .Binding) {
|
|
753
718
|
// Pokémon Showdown handles this after hit/miss checks and damage calculation
|
|
754
|
-
Effects.binding(battle, player
|
|
719
|
+
Effects.binding(battle, player);
|
|
755
720
|
}
|
|
756
721
|
|
|
757
722
|
return true;
|
|
@@ -792,7 +757,6 @@ fn doMove(
|
|
|
792
757
|
battle: anytype,
|
|
793
758
|
player: Player,
|
|
794
759
|
mslot: u4,
|
|
795
|
-
rewrap: bool,
|
|
796
760
|
auto: bool,
|
|
797
761
|
residual: *bool,
|
|
798
762
|
log: anytype,
|
|
@@ -823,15 +787,12 @@ fn doMove(
|
|
|
823
787
|
(@enumToInt(move.type.effectiveness(foe.active.types.type1)) == 0 or
|
|
824
788
|
@enumToInt(move.type.effectiveness(foe.active.types.type2)) == 0);
|
|
825
789
|
if (immune and move.effect != .Binding) break :miss true;
|
|
826
|
-
if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe)
|
|
827
|
-
battle.last_damage = 0;
|
|
828
|
-
break :miss true;
|
|
829
|
-
}
|
|
790
|
+
if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) break :miss true;
|
|
830
791
|
break :miss move.target != .Self and !moveHit(battle, player, move, &immune, &mist);
|
|
831
792
|
};
|
|
832
793
|
assert(!immune or miss or (showdown and move.effect == .Binding));
|
|
833
794
|
|
|
834
|
-
var late = showdown
|
|
795
|
+
var late = showdown;
|
|
835
796
|
const skip = status or immune;
|
|
836
797
|
if ((!showdown or (!skip or counter)) and !miss) blk: {
|
|
837
798
|
if (showdown and move.effect.isMulti()) {
|
|
@@ -884,9 +845,9 @@ fn doMove(
|
|
|
884
845
|
|
|
885
846
|
if (!showdown or !miss) {
|
|
886
847
|
if (move.effect == .MirrorMove) {
|
|
887
|
-
return mirrorMove(battle, player, mslot,
|
|
848
|
+
return mirrorMove(battle, player, mslot, auto, residual, log);
|
|
888
849
|
} else if (move.effect == .Metronome) {
|
|
889
|
-
return metronome(battle, player, mslot,
|
|
850
|
+
return metronome(battle, player, mslot, auto, residual, log);
|
|
890
851
|
} else if (move.effect.onEnd()) {
|
|
891
852
|
try onEnd(battle, player, move, log);
|
|
892
853
|
return null;
|
|
@@ -901,7 +862,7 @@ fn doMove(
|
|
|
901
862
|
move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
|
|
902
863
|
if (ohko) {
|
|
903
864
|
try log.immune(foe_ident, .OHKO);
|
|
904
|
-
} else if (immune and !invulnerable
|
|
865
|
+
} else if (immune and !invulnerable) {
|
|
905
866
|
try log.immune(foe_ident, .None);
|
|
906
867
|
} else if (mist) {
|
|
907
868
|
if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
|
|
@@ -933,9 +894,6 @@ fn doMove(
|
|
|
933
894
|
hits = side.active.volatiles.attacks;
|
|
934
895
|
}
|
|
935
896
|
|
|
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
|
-
|
|
939
897
|
var nullified = false;
|
|
940
898
|
var hit: u4 = 0;
|
|
941
899
|
while (hit < hits) {
|
|
@@ -951,7 +909,7 @@ fn doMove(
|
|
|
951
909
|
if (hit == 0 and ohko) try log.ohko();
|
|
952
910
|
hit += 1;
|
|
953
911
|
if (foe.stored().hp == 0) break;
|
|
954
|
-
if (!late
|
|
912
|
+
if (!late) try buildRage(battle, player.foe(), log);
|
|
955
913
|
// If the substitute breaks during a multi-hit attack, the attack ends
|
|
956
914
|
if (nullified) break;
|
|
957
915
|
}
|
|
@@ -967,7 +925,7 @@ fn doMove(
|
|
|
967
925
|
} else if (showdown) {
|
|
968
926
|
// This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
|
|
969
927
|
if (move.effect == .Binding) {
|
|
970
|
-
Effects.binding(battle, player
|
|
928
|
+
Effects.binding(battle, player);
|
|
971
929
|
if (immune) {
|
|
972
930
|
battle.last_damage = 0;
|
|
973
931
|
// Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
|
|
@@ -988,8 +946,8 @@ fn doMove(
|
|
|
988
946
|
|
|
989
947
|
if (foe.stored().hp == 0) return null;
|
|
990
948
|
|
|
991
|
-
// Pokémon Showdown builds Rage at the wrong time for non-MultiHit
|
|
992
|
-
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);
|
|
993
951
|
|
|
994
952
|
if (!move.effect.isSpecial()) {
|
|
995
953
|
// On the cartridge Rage is not considered to be "special" and thus gets executed for a
|
|
@@ -1022,7 +980,8 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
|
|
|
1022
980
|
chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
|
|
1023
981
|
chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
|
|
1024
982
|
|
|
1025
|
-
return
|
|
983
|
+
if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
|
|
984
|
+
return std.math.rotl(u8, battle.rng.next(), 3) < chance;
|
|
1026
985
|
}
|
|
1027
986
|
|
|
1028
987
|
fn calcDamage(
|
|
@@ -1126,36 +1085,50 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
|
1126
1085
|
|
|
1127
1086
|
fn randomizeDamage(battle: anytype) void {
|
|
1128
1087
|
if (battle.last_damage <= 1) return;
|
|
1129
|
-
|
|
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
|
+
|
|
1130
1098
|
battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
|
|
1131
1099
|
}
|
|
1132
1100
|
|
|
1133
1101
|
fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
|
|
1134
1102
|
const side = battle.side(player);
|
|
1135
|
-
const foe = battle.foe(player);
|
|
1136
1103
|
|
|
1137
1104
|
if (!try checkHit(battle, player, move, log)) return null;
|
|
1138
1105
|
|
|
1139
1106
|
battle.last_damage = switch (side.last_selected_move) {
|
|
1140
|
-
.SuperFang => @max(foe.stored().hp / 2, 1),
|
|
1107
|
+
.SuperFang => @max(battle.foe(player).stored().hp / 2, 1),
|
|
1141
1108
|
.SeismicToss, .NightShade => side.stored().level,
|
|
1142
1109
|
.SonicBoom => 20,
|
|
1143
1110
|
.DragonRage => 40,
|
|
1144
1111
|
// GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
|
|
1145
1112
|
.Psywave => power: {
|
|
1146
1113
|
const max = @intCast(u8, @as(u16, side.stored().level) * 3 / 2);
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
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
|
+
}
|
|
1150
1124
|
},
|
|
1151
1125
|
else => unreachable,
|
|
1152
1126
|
};
|
|
1153
1127
|
|
|
1154
1128
|
if (battle.last_damage == 0) return if (showdown) null else Result.Error;
|
|
1155
1129
|
|
|
1156
|
-
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1157
1130
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
1158
|
-
if (battle.foe(player).stored().hp > 0
|
|
1131
|
+
if (battle.foe(player).stored().hp > 0) try buildRage(battle, player.foe(), log);
|
|
1159
1132
|
|
|
1160
1133
|
return null;
|
|
1161
1134
|
}
|
|
@@ -1204,9 +1177,7 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
|
|
|
1204
1177
|
// Pokémon Showdown calls checkHit before Counter
|
|
1205
1178
|
if (!showdown and !try checkHit(battle, player, move, log)) return null;
|
|
1206
1179
|
|
|
1207
|
-
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1208
1180
|
_ = 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);
|
|
1210
1181
|
return null;
|
|
1211
1182
|
}
|
|
1212
1183
|
|
|
@@ -1250,7 +1221,6 @@ fn mirrorMove(
|
|
|
1250
1221
|
battle: anytype,
|
|
1251
1222
|
player: Player,
|
|
1252
1223
|
mslot: u4,
|
|
1253
|
-
rewrap: bool,
|
|
1254
1224
|
auto: bool,
|
|
1255
1225
|
residual: *bool,
|
|
1256
1226
|
log: anytype,
|
|
@@ -1263,30 +1233,41 @@ fn mirrorMove(
|
|
|
1263
1233
|
if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
|
|
1264
1234
|
try log.fail(battle.active(player), .None);
|
|
1265
1235
|
return null;
|
|
1266
|
-
} else if (!showdown or foe.last_used_move != .Struggle) {
|
|
1267
|
-
incrementPP(side, mslot);
|
|
1268
1236
|
}
|
|
1269
1237
|
|
|
1238
|
+
incrementPP(side, mslot);
|
|
1239
|
+
|
|
1270
1240
|
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
|
|
1271
|
-
return doMove(battle, player, mslot,
|
|
1241
|
+
return doMove(battle, player, mslot, auto, residual, log);
|
|
1272
1242
|
}
|
|
1273
1243
|
|
|
1274
1244
|
fn metronome(
|
|
1275
1245
|
battle: anytype,
|
|
1276
1246
|
player: Player,
|
|
1277
1247
|
mslot: u4,
|
|
1278
|
-
rewrap: bool,
|
|
1279
1248
|
auto: bool,
|
|
1280
1249
|
residual: *bool,
|
|
1281
1250
|
log: anytype,
|
|
1282
1251
|
) !?Result {
|
|
1283
1252
|
var side = battle.side(player);
|
|
1284
1253
|
|
|
1285
|
-
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
|
+
|
|
1286
1267
|
incrementPP(side, mslot);
|
|
1287
1268
|
|
|
1288
1269
|
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
|
|
1289
|
-
return doMove(battle, player, mslot,
|
|
1270
|
+
return doMove(battle, player, mslot, auto, residual, log);
|
|
1290
1271
|
}
|
|
1291
1272
|
|
|
1292
1273
|
fn checkHit(battle: anytype, player: Player, move: Move.Data, log: anytype) !bool {
|
|
@@ -1340,24 +1321,23 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
|
|
|
1340
1321
|
}
|
|
1341
1322
|
|
|
1342
1323
|
// GLITCH: Thrash / Petal Dance / Rage get their accuracy overwritten on subsequent hits
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
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));
|
|
1349
1328
|
var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
|
|
1350
1329
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1351
1330
|
boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
|
|
1352
1331
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1353
1332
|
accuracy = @min(255, @max(1, accuracy));
|
|
1354
1333
|
|
|
1355
|
-
|
|
1356
|
-
if (showdown) overwrite = side.active.volatiles.Rage or side.active.volatiles.Thrashing;
|
|
1357
|
-
if (overwrite) side.active.volatiles.state = accuracy;
|
|
1334
|
+
if (move.effect == .Thrashing or move.effect == .Rage) state.* = accuracy;
|
|
1358
1335
|
|
|
1359
1336
|
// GLITCH: max accuracy is 255 so 1/256 chance of miss
|
|
1360
|
-
break :miss
|
|
1337
|
+
break :miss if (showdown)
|
|
1338
|
+
!battle.rng.chance(u8, @intCast(u8, accuracy), 256)
|
|
1339
|
+
else
|
|
1340
|
+
battle.rng.next() >= accuracy;
|
|
1361
1341
|
};
|
|
1362
1342
|
|
|
1363
1343
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1489,6 +1469,9 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
|
|
|
1489
1469
|
damage *= volatiles.toxic;
|
|
1490
1470
|
}
|
|
1491
1471
|
|
|
1472
|
+
// Pokémon Showdown erroneously updates last damage with uncapped Leech Seed damage
|
|
1473
|
+
if (showdown) battle.last_damage = damage;
|
|
1474
|
+
|
|
1492
1475
|
const amount = @min(damage, stored.hp);
|
|
1493
1476
|
stored.hp -= amount;
|
|
1494
1477
|
|
|
@@ -1580,42 +1563,6 @@ fn checkEBC(battle: anytype) bool {
|
|
|
1580
1563
|
return false;
|
|
1581
1564
|
}
|
|
1582
1565
|
|
|
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
|
-
|
|
1619
1566
|
inline fn onBegin(
|
|
1620
1567
|
battle: anytype,
|
|
1621
1568
|
player: Player,
|
|
@@ -1706,7 +1653,10 @@ pub const Effects = struct {
|
|
|
1706
1653
|
side.active.volatiles.Bide = true;
|
|
1707
1654
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1708
1655
|
side.active.volatiles.state = 0;
|
|
1709
|
-
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);
|
|
1710
1660
|
|
|
1711
1661
|
try log.start(battle.active(player), .Bide);
|
|
1712
1662
|
}
|
|
@@ -1728,8 +1678,14 @@ pub const Effects = struct {
|
|
|
1728
1678
|
return;
|
|
1729
1679
|
}
|
|
1730
1680
|
|
|
1731
|
-
|
|
1732
|
-
|
|
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;
|
|
1733
1689
|
|
|
1734
1690
|
foe_stored.status = Status.init(.BRN);
|
|
1735
1691
|
foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
|
|
@@ -1753,7 +1709,11 @@ pub const Effects = struct {
|
|
|
1753
1709
|
const sub = foe.active.volatiles.Substitute;
|
|
1754
1710
|
|
|
1755
1711
|
if (move.effect == .ConfusionChance) {
|
|
1756
|
-
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;
|
|
1757
1717
|
} else {
|
|
1758
1718
|
if (showdown) {
|
|
1759
1719
|
if (!try checkHit(battle, player, move, log)) {
|
|
@@ -1772,7 +1732,10 @@ pub const Effects = struct {
|
|
|
1772
1732
|
|
|
1773
1733
|
if (foe.active.volatiles.Confusion) return;
|
|
1774
1734
|
foe.active.volatiles.Confusion = true;
|
|
1775
|
-
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);
|
|
1776
1739
|
|
|
1777
1740
|
try log.start(battle.active(player.foe()), .Confusion);
|
|
1778
1741
|
}
|
|
@@ -1822,8 +1785,11 @@ pub const Effects = struct {
|
|
|
1822
1785
|
return Result.Error;
|
|
1823
1786
|
}
|
|
1824
1787
|
|
|
1825
|
-
volatiles.disabled_move = @intCast(u3,
|
|
1826
|
-
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);
|
|
1827
1793
|
|
|
1828
1794
|
try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
|
|
1829
1795
|
return null;
|
|
@@ -1835,7 +1801,7 @@ pub const Effects = struct {
|
|
|
1835
1801
|
|
|
1836
1802
|
if (battle.last_damage == 0) {
|
|
1837
1803
|
assert(showdown);
|
|
1838
|
-
|
|
1804
|
+
return;
|
|
1839
1805
|
}
|
|
1840
1806
|
|
|
1841
1807
|
const drain = @max(battle.last_damage / 2, 1);
|
|
@@ -1862,7 +1828,15 @@ pub const Effects = struct {
|
|
|
1862
1828
|
var volatiles = &battle.foe(player).active.volatiles;
|
|
1863
1829
|
|
|
1864
1830
|
if (volatiles.Substitute) return;
|
|
1865
|
-
|
|
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;
|
|
1866
1840
|
|
|
1867
1841
|
volatiles.Flinch = true;
|
|
1868
1842
|
volatiles.Recharging = false;
|
|
@@ -1888,8 +1862,12 @@ pub const Effects = struct {
|
|
|
1888
1862
|
return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
|
|
1889
1863
|
}
|
|
1890
1864
|
|
|
1891
|
-
|
|
1892
|
-
|
|
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
|
+
|
|
1893
1871
|
// Freeze Clause Mod
|
|
1894
1872
|
if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
1895
1873
|
|
|
@@ -2026,15 +2004,15 @@ pub const Effects = struct {
|
|
|
2026
2004
|
}
|
|
2027
2005
|
break :has_mimic false;
|
|
2028
2006
|
};
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2007
|
+
// If the foe is Invulnerable we still want to fall through to checkHit to be able to
|
|
2008
|
+
// trigger |-miss| instead of |-fail|
|
|
2009
|
+
if (!has_mimic and !foe.active.volatiles.Invulnerable) {
|
|
2032
2010
|
return try log.fail(battle.active(player.foe()), .None);
|
|
2033
2011
|
}
|
|
2034
2012
|
}
|
|
2035
2013
|
if (!try checkHit(battle, player, move, log)) return;
|
|
2036
2014
|
|
|
2037
|
-
const rslot =
|
|
2015
|
+
const rslot = randomMoveSlot(&battle.rng, &foe.active.moves, 0);
|
|
2038
2016
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
2039
2017
|
|
|
2040
2018
|
try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
|
|
@@ -2055,8 +2033,7 @@ pub const Effects = struct {
|
|
|
2055
2033
|
assert(!side.active.volatiles.MultiHit);
|
|
2056
2034
|
side.active.volatiles.MultiHit = true;
|
|
2057
2035
|
|
|
2058
|
-
side.active.volatiles.attacks =
|
|
2059
|
-
if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
|
|
2036
|
+
side.active.volatiles.attacks = if (move.effect == .MultiHit) distribution(battle) else 2;
|
|
2060
2037
|
}
|
|
2061
2038
|
|
|
2062
2039
|
fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2100,8 +2077,14 @@ pub const Effects = struct {
|
|
|
2100
2077
|
}
|
|
2101
2078
|
|
|
2102
2079
|
// Body Slam can't paralyze a Normal type Pokémon
|
|
2103
|
-
|
|
2104
|
-
|
|
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;
|
|
2105
2088
|
|
|
2106
2089
|
foe_stored.status = Status.init(.PAR);
|
|
2107
2090
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
@@ -2144,7 +2127,16 @@ pub const Effects = struct {
|
|
|
2144
2127
|
|
|
2145
2128
|
if (move.effect == .Poison) {
|
|
2146
2129
|
if (!showdown and !try checkHit(battle, player, move, log)) return;
|
|
2147
|
-
} 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
|
+
}
|
|
2148
2140
|
|
|
2149
2141
|
foe_stored.status = Status.init(.PSN);
|
|
2150
2142
|
if (toxic) {
|
|
@@ -2158,8 +2150,10 @@ pub const Effects = struct {
|
|
|
2158
2150
|
|
|
2159
2151
|
fn rage(battle: anytype, player: Player) !void {
|
|
2160
2152
|
var volatiles = &battle.side(player).active.volatiles;
|
|
2161
|
-
|
|
2153
|
+
if (volatiles.Rage) return;
|
|
2162
2154
|
volatiles.Rage = true;
|
|
2155
|
+
assert(!volatiles.Bide);
|
|
2156
|
+
volatiles.state = 0;
|
|
2163
2157
|
}
|
|
2164
2158
|
|
|
2165
2159
|
fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
|
|
@@ -2211,7 +2205,16 @@ pub const Effects = struct {
|
|
|
2211
2205
|
}
|
|
2212
2206
|
foe.active.volatiles.Recharging = false;
|
|
2213
2207
|
|
|
2214
|
-
|
|
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);
|
|
2215
2218
|
try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
|
|
2216
2219
|
}
|
|
2217
2220
|
|
|
@@ -2264,7 +2267,11 @@ pub const Effects = struct {
|
|
|
2264
2267
|
assert(!volatiles.Bide);
|
|
2265
2268
|
|
|
2266
2269
|
volatiles.Thrashing = true;
|
|
2267
|
-
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);
|
|
2268
2275
|
}
|
|
2269
2276
|
|
|
2270
2277
|
fn transform(battle: anytype, player: Player, log: anytype) !void {
|
|
@@ -2296,7 +2303,7 @@ pub const Effects = struct {
|
|
|
2296
2303
|
try log.transform(battle.active(player), foe_ident);
|
|
2297
2304
|
}
|
|
2298
2305
|
|
|
2299
|
-
fn binding(battle: anytype, player: Player
|
|
2306
|
+
fn binding(battle: anytype, player: Player) void {
|
|
2300
2307
|
var side = battle.side(player);
|
|
2301
2308
|
var foe = battle.foe(player);
|
|
2302
2309
|
|
|
@@ -2304,11 +2311,9 @@ pub const Effects = struct {
|
|
|
2304
2311
|
side.active.volatiles.Binding = true;
|
|
2305
2312
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2306
2313
|
// (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
|
|
2307
|
-
if (!showdown)
|
|
2308
|
-
foe.active.volatiles.Recharging = false;
|
|
2309
|
-
} else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
|
|
2314
|
+
if (!showdown) foe.active.volatiles.Recharging = false;
|
|
2310
2315
|
|
|
2311
|
-
side.active.volatiles.attacks =
|
|
2316
|
+
side.active.volatiles.attacks = distribution(battle) - 1;
|
|
2312
2317
|
}
|
|
2313
2318
|
|
|
2314
2319
|
fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2335,11 +2340,9 @@ pub const Effects = struct {
|
|
|
2335
2340
|
return try log.fail(ident, .None);
|
|
2336
2341
|
}
|
|
2337
2342
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2338
|
-
const stat = unmodifiedStats(battle,
|
|
2343
|
+
const stat = unmodifiedStats(battle, player).atk;
|
|
2339
2344
|
stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2340
2345
|
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;
|
|
2343
2346
|
},
|
|
2344
2347
|
.DefenseUp1, .DefenseUp2 => {
|
|
2345
2348
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
@@ -2355,7 +2358,7 @@ pub const Effects = struct {
|
|
|
2355
2358
|
return try log.fail(ident, .None);
|
|
2356
2359
|
}
|
|
2357
2360
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2358
|
-
const stat = unmodifiedStats(battle,
|
|
2361
|
+
const stat = unmodifiedStats(battle, player).def;
|
|
2359
2362
|
stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2360
2363
|
try log.boost(ident, .Defense, n);
|
|
2361
2364
|
},
|
|
@@ -2372,7 +2375,7 @@ pub const Effects = struct {
|
|
|
2372
2375
|
return try log.fail(ident, .None);
|
|
2373
2376
|
}
|
|
2374
2377
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2375
|
-
const stat = unmodifiedStats(battle,
|
|
2378
|
+
const stat = unmodifiedStats(battle, player).spe;
|
|
2376
2379
|
stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2377
2380
|
try log.boost(ident, .Speed, 2);
|
|
2378
2381
|
},
|
|
@@ -2392,7 +2395,7 @@ pub const Effects = struct {
|
|
|
2392
2395
|
return try log.fail(ident, .None);
|
|
2393
2396
|
}
|
|
2394
2397
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2395
|
-
const stat = unmodifiedStats(battle,
|
|
2398
|
+
const stat = unmodifiedStats(battle, player).spc;
|
|
2396
2399
|
stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2397
2400
|
try log.boost(ident, .SpecialAttack, n);
|
|
2398
2401
|
try log.boost(ident, .SpecialDefense, n);
|
|
@@ -2419,7 +2422,11 @@ pub const Effects = struct {
|
|
|
2419
2422
|
}
|
|
2420
2423
|
|
|
2421
2424
|
if (move.effect.isStatDownChance()) {
|
|
2422
|
-
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;
|
|
2423
2430
|
} else if (!showdown and !try checkHit(battle, player, move, log)) {
|
|
2424
2431
|
return; // checkHit already checks for Invulnerable
|
|
2425
2432
|
}
|
|
@@ -2441,7 +2448,7 @@ pub const Effects = struct {
|
|
|
2441
2448
|
return try log.fail(foe_ident, .None);
|
|
2442
2449
|
}
|
|
2443
2450
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2444
|
-
const stat = unmodifiedStats(battle, foe).atk;
|
|
2451
|
+
const stat = unmodifiedStats(battle, player.foe()).atk;
|
|
2445
2452
|
stats.atk = @max(1, stat * mod[0] / mod[1]);
|
|
2446
2453
|
try log.boost(foe_ident, .Attack, -1);
|
|
2447
2454
|
},
|
|
@@ -2459,7 +2466,7 @@ pub const Effects = struct {
|
|
|
2459
2466
|
return try log.fail(foe_ident, .None);
|
|
2460
2467
|
}
|
|
2461
2468
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2462
|
-
const stat = unmodifiedStats(battle, foe).def;
|
|
2469
|
+
const stat = unmodifiedStats(battle, player.foe()).def;
|
|
2463
2470
|
stats.def = @max(1, stat * mod[0] / mod[1]);
|
|
2464
2471
|
try log.boost(foe_ident, .Defense, -@as(i8, n));
|
|
2465
2472
|
},
|
|
@@ -2476,7 +2483,7 @@ pub const Effects = struct {
|
|
|
2476
2483
|
return try log.fail(foe_ident, .None);
|
|
2477
2484
|
}
|
|
2478
2485
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2479
|
-
const stat = unmodifiedStats(battle, foe).spe;
|
|
2486
|
+
const stat = unmodifiedStats(battle, player.foe()).spe;
|
|
2480
2487
|
stats.spe = @max(1, stat * mod[0] / mod[1]);
|
|
2481
2488
|
try log.boost(foe_ident, .Speed, -1);
|
|
2482
2489
|
assert(boosts.spe >= -6);
|
|
@@ -2496,7 +2503,7 @@ pub const Effects = struct {
|
|
|
2496
2503
|
return try log.fail(foe_ident, .None);
|
|
2497
2504
|
}
|
|
2498
2505
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2499
|
-
const stat = unmodifiedStats(battle, foe).spc;
|
|
2506
|
+
const stat = unmodifiedStats(battle, player.foe()).spc;
|
|
2500
2507
|
stats.spc = @max(1, stat * mod[0] / mod[1]);
|
|
2501
2508
|
try log.boost(foe_ident, .SpecialAttack, -1);
|
|
2502
2509
|
try log.boost(foe_ident, .SpecialDefense, -1);
|
|
@@ -2515,7 +2522,8 @@ pub const Effects = struct {
|
|
|
2515
2522
|
}
|
|
2516
2523
|
};
|
|
2517
2524
|
|
|
2518
|
-
fn unmodifiedStats(battle: anytype,
|
|
2525
|
+
fn unmodifiedStats(battle: anytype, who: Player) *Stats(u16) {
|
|
2526
|
+
const side = battle.side(who);
|
|
2519
2527
|
if (!side.active.volatiles.Transform) return &side.stored().stats;
|
|
2520
2528
|
const id = ID.from(side.active.volatiles.transform);
|
|
2521
2529
|
return &battle.side(id.player).pokemon[id.id - 1].stats;
|
|
@@ -2581,140 +2589,75 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
|
|
|
2581
2589
|
}
|
|
2582
2590
|
}
|
|
2583
2591
|
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
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;
|
|
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;
|
|
2600
2600
|
}
|
|
2601
2601
|
}
|
|
2602
|
+
return false;
|
|
2603
|
+
}
|
|
2602
2604
|
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
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
|
-
}
|
|
2612
|
-
|
|
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
|
-
}
|
|
2617
|
-
|
|
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);
|
|
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);
|
|
2621
2609
|
}
|
|
2610
|
+
}
|
|
2622
2611
|
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2612
|
+
fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
|
|
2613
|
+
var volatiles = &active.volatiles;
|
|
2614
|
+
assert(volatiles.Thrashing);
|
|
2615
|
+
if (volatiles.attacks > 0) return false;
|
|
2627
2616
|
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
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);
|
|
2632
2623
|
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
return battle.rng.next() < 1 + (if (low) Gen12.percent(20) else Gen12.percent(40));
|
|
2636
|
-
}
|
|
2624
|
+
return true;
|
|
2625
|
+
}
|
|
2637
2626
|
|
|
2638
|
-
|
|
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
|
-
}
|
|
2627
|
+
const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
|
|
2645
2628
|
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
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
|
+
}
|
|
2650
2634
|
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
if (
|
|
2666
|
-
r -= 1;
|
|
2667
|
-
if (r == 0) break;
|
|
2668
|
-
}
|
|
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;
|
|
2669
2650
|
}
|
|
2670
|
-
return @intCast(u4, i + 1);
|
|
2671
2651
|
}
|
|
2652
|
+
return @intCast(u4, i + 1);
|
|
2672
2653
|
}
|
|
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;
|
|
2677
|
-
}
|
|
2678
|
-
}
|
|
2679
|
-
|
|
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);
|
|
2683
|
-
}
|
|
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
2654
|
}
|
|
2712
2655
|
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
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;
|
|
2716
2659
|
}
|
|
2717
|
-
}
|
|
2660
|
+
}
|
|
2718
2661
|
|
|
2719
2662
|
test "RNG agreement" {
|
|
2720
2663
|
if (!showdown) return;
|
|
@@ -2814,11 +2757,9 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
2814
2757
|
while (slot <= 4) : (slot += 1) {
|
|
2815
2758
|
const m = active.moves[slot - 1];
|
|
2816
2759
|
if (m.id == .None) break;
|
|
2817
|
-
if (m.id ==
|
|
2818
|
-
// Pokémon Showdown displays Struggle if limited to Bide
|
|
2819
|
-
const
|
|
2820
|
-
m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);
|
|
2821
|
-
const s = if (struggle) 0 else slot;
|
|
2760
|
+
if (m.id == side.last_selected_move) {
|
|
2761
|
+
// Pokémon Showdown displays Struggle if limited to Bide when out of PP
|
|
2762
|
+
const s = if (m.pp == 0 and m.id == .Bide) 0 else slot;
|
|
2822
2763
|
out[n] = .{ .type = .Move, .data = s };
|
|
2823
2764
|
n += 1;
|
|
2824
2765
|
return n;
|