@pkmn/engine 0.1.0-dev.550c3889 → 0.1.0-dev.5ec64ea5
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 -22
- package/build/pkg/addon.d.ts +2 -2
- package/build/pkg/addon.js +5 -5
- package/build/pkg/addon.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/test/display.js +1 -2
- package/build/test/display.js.map +1 -1
- package/build.zig +22 -17
- package/package.json +9 -10
- package/src/bin/install-pkmn-engine +3 -3
- package/src/bin/pkmn-debug +3 -2
- 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 +14 -23
- package/src/lib/gen1/data.zig +6 -6
- package/src/lib/gen1/helpers.zig +7 -10
- package/src/lib/gen1/mechanics.zig +274 -315
- package/src/lib/gen2/data.zig +4 -4
- package/src/lib/pkmn.zig +7 -7
|
@@ -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) {
|
|
@@ -668,13 +643,8 @@ fn beforeMove(
|
|
|
668
643
|
try log.start(battle.active(player), .ConfusionSilent);
|
|
669
644
|
}
|
|
670
645
|
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
|
-
}
|
|
646
|
+
if (showdown and handleThrashing(battle, active)) {
|
|
647
|
+
try log.start(battle.active(player), .ConfusionSilent);
|
|
678
648
|
}
|
|
679
649
|
return .skip_can;
|
|
680
650
|
}
|
|
@@ -689,11 +659,7 @@ fn beforeMove(
|
|
|
689
659
|
side.last_selected_move,
|
|
690
660
|
);
|
|
691
661
|
if (showdown or battle.last_damage != 0) {
|
|
692
|
-
const sub = showdown and foe.active.volatiles.Substitute;
|
|
693
662
|
_ = 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
663
|
}
|
|
698
664
|
return .done;
|
|
699
665
|
}
|
|
@@ -751,7 +717,7 @@ fn canMove(
|
|
|
751
717
|
Effects.thrashing(battle, player);
|
|
752
718
|
} else if (!showdown and move.effect == .Binding) {
|
|
753
719
|
// Pokémon Showdown handles this after hit/miss checks and damage calculation
|
|
754
|
-
Effects.binding(battle, player
|
|
720
|
+
Effects.binding(battle, player);
|
|
755
721
|
}
|
|
756
722
|
|
|
757
723
|
return true;
|
|
@@ -792,7 +758,6 @@ fn doMove(
|
|
|
792
758
|
battle: anytype,
|
|
793
759
|
player: Player,
|
|
794
760
|
mslot: u4,
|
|
795
|
-
rewrap: bool,
|
|
796
761
|
auto: bool,
|
|
797
762
|
residual: *bool,
|
|
798
763
|
log: anytype,
|
|
@@ -831,7 +796,7 @@ fn doMove(
|
|
|
831
796
|
};
|
|
832
797
|
assert(!immune or miss or (showdown and move.effect == .Binding));
|
|
833
798
|
|
|
834
|
-
var late = showdown
|
|
799
|
+
var late = showdown;
|
|
835
800
|
const skip = status or immune;
|
|
836
801
|
if ((!showdown or (!skip or counter)) and !miss) blk: {
|
|
837
802
|
if (showdown and move.effect.isMulti()) {
|
|
@@ -884,9 +849,9 @@ fn doMove(
|
|
|
884
849
|
|
|
885
850
|
if (!showdown or !miss) {
|
|
886
851
|
if (move.effect == .MirrorMove) {
|
|
887
|
-
return mirrorMove(battle, player, mslot,
|
|
852
|
+
return mirrorMove(battle, player, mslot, auto, residual, log);
|
|
888
853
|
} else if (move.effect == .Metronome) {
|
|
889
|
-
return metronome(battle, player, mslot,
|
|
854
|
+
return metronome(battle, player, mslot, auto, residual, log);
|
|
890
855
|
} else if (move.effect.onEnd()) {
|
|
891
856
|
try onEnd(battle, player, move, log);
|
|
892
857
|
return null;
|
|
@@ -901,7 +866,7 @@ fn doMove(
|
|
|
901
866
|
move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
|
|
902
867
|
if (ohko) {
|
|
903
868
|
try log.immune(foe_ident, .OHKO);
|
|
904
|
-
} else if (immune and !invulnerable
|
|
869
|
+
} else if (immune and !invulnerable) {
|
|
905
870
|
try log.immune(foe_ident, .None);
|
|
906
871
|
} else if (mist) {
|
|
907
872
|
if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
|
|
@@ -951,7 +916,7 @@ fn doMove(
|
|
|
951
916
|
if (hit == 0 and ohko) try log.ohko();
|
|
952
917
|
hit += 1;
|
|
953
918
|
if (foe.stored().hp == 0) break;
|
|
954
|
-
if (!late and
|
|
919
|
+
if (!late and !sub) try buildRage(battle, player.foe(), log);
|
|
955
920
|
// If the substitute breaks during a multi-hit attack, the attack ends
|
|
956
921
|
if (nullified) break;
|
|
957
922
|
}
|
|
@@ -967,7 +932,7 @@ fn doMove(
|
|
|
967
932
|
} else if (showdown) {
|
|
968
933
|
// This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
|
|
969
934
|
if (move.effect == .Binding) {
|
|
970
|
-
Effects.binding(battle, player
|
|
935
|
+
Effects.binding(battle, player);
|
|
971
936
|
if (immune) {
|
|
972
937
|
battle.last_damage = 0;
|
|
973
938
|
// Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
|
|
@@ -1022,7 +987,8 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
|
|
|
1022
987
|
chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
|
|
1023
988
|
chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
|
|
1024
989
|
|
|
1025
|
-
return
|
|
990
|
+
if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
|
|
991
|
+
return std.math.rotl(u8, battle.rng.next(), 3) < chance;
|
|
1026
992
|
}
|
|
1027
993
|
|
|
1028
994
|
fn calcDamage(
|
|
@@ -1126,7 +1092,16 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
|
1126
1092
|
|
|
1127
1093
|
fn randomizeDamage(battle: anytype) void {
|
|
1128
1094
|
if (battle.last_damage <= 1) return;
|
|
1129
|
-
|
|
1095
|
+
|
|
1096
|
+
const random = if (showdown)
|
|
1097
|
+
battle.rng.range(u8, 217, 256)
|
|
1098
|
+
else loop: {
|
|
1099
|
+
while (true) {
|
|
1100
|
+
const r = std.math.rotr(u8, battle.rng.next(), 1);
|
|
1101
|
+
if (r >= 217) break :loop r;
|
|
1102
|
+
}
|
|
1103
|
+
};
|
|
1104
|
+
|
|
1130
1105
|
battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
|
|
1131
1106
|
}
|
|
1132
1107
|
|
|
@@ -1144,9 +1119,16 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
|
|
|
1144
1119
|
// GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
|
|
1145
1120
|
.Psywave => power: {
|
|
1146
1121
|
const max = @intCast(u8, @as(u16, side.stored().level) * 3 / 2);
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1122
|
+
if (showdown) {
|
|
1123
|
+
break :power battle.rng.range(u8, 0, max);
|
|
1124
|
+
} else {
|
|
1125
|
+
// GLITCH: Psywave infinite glitch loop
|
|
1126
|
+
if (max <= 1) return Result.Error;
|
|
1127
|
+
while (true) {
|
|
1128
|
+
const r = battle.rng.next();
|
|
1129
|
+
if (r < max) break :power r;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1150
1132
|
},
|
|
1151
1133
|
else => unreachable,
|
|
1152
1134
|
};
|
|
@@ -1204,9 +1186,7 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
|
|
|
1204
1186
|
// Pokémon Showdown calls checkHit before Counter
|
|
1205
1187
|
if (!showdown and !try checkHit(battle, player, move, log)) return null;
|
|
1206
1188
|
|
|
1207
|
-
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1208
1189
|
_ = 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
1190
|
return null;
|
|
1211
1191
|
}
|
|
1212
1192
|
|
|
@@ -1250,7 +1230,6 @@ fn mirrorMove(
|
|
|
1250
1230
|
battle: anytype,
|
|
1251
1231
|
player: Player,
|
|
1252
1232
|
mslot: u4,
|
|
1253
|
-
rewrap: bool,
|
|
1254
1233
|
auto: bool,
|
|
1255
1234
|
residual: *bool,
|
|
1256
1235
|
log: anytype,
|
|
@@ -1268,25 +1247,36 @@ fn mirrorMove(
|
|
|
1268
1247
|
incrementPP(side, mslot);
|
|
1269
1248
|
|
|
1270
1249
|
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
|
|
1271
|
-
return doMove(battle, player, mslot,
|
|
1250
|
+
return doMove(battle, player, mslot, auto, residual, log);
|
|
1272
1251
|
}
|
|
1273
1252
|
|
|
1274
1253
|
fn metronome(
|
|
1275
1254
|
battle: anytype,
|
|
1276
1255
|
player: Player,
|
|
1277
1256
|
mslot: u4,
|
|
1278
|
-
rewrap: bool,
|
|
1279
1257
|
auto: bool,
|
|
1280
1258
|
residual: *bool,
|
|
1281
1259
|
log: anytype,
|
|
1282
1260
|
) !?Result {
|
|
1283
1261
|
var side = battle.side(player);
|
|
1284
1262
|
|
|
1285
|
-
side.last_selected_move =
|
|
1263
|
+
side.last_selected_move = if (showdown) blk: {
|
|
1264
|
+
const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
|
|
1265
|
+
const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
|
|
1266
|
+
break :blk @intToEnum(Move, r + mod);
|
|
1267
|
+
} else loop: {
|
|
1268
|
+
while (true) {
|
|
1269
|
+
const r = battle.rng.next();
|
|
1270
|
+
if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
|
|
1271
|
+
if (r >= @enumToInt(Move.Struggle)) continue;
|
|
1272
|
+
break :loop @intToEnum(Move, r);
|
|
1273
|
+
}
|
|
1274
|
+
};
|
|
1275
|
+
|
|
1286
1276
|
incrementPP(side, mslot);
|
|
1287
1277
|
|
|
1288
1278
|
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
|
|
1289
|
-
return doMove(battle, player, mslot,
|
|
1279
|
+
return doMove(battle, player, mslot, auto, residual, log);
|
|
1290
1280
|
}
|
|
1291
1281
|
|
|
1292
1282
|
fn checkHit(battle: anytype, player: Player, move: Move.Data, log: anytype) !bool {
|
|
@@ -1340,24 +1330,23 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
|
|
|
1340
1330
|
}
|
|
1341
1331
|
|
|
1342
1332
|
// 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));
|
|
1333
|
+
var state = &side.active.volatiles.state;
|
|
1334
|
+
const overwritten = (move.effect == .Thrashing or move.effect == .Rage) and state.* > 0;
|
|
1335
|
+
assert(!overwritten or (0 < state.* and state.* <= 255 and !side.active.volatiles.Bide));
|
|
1336
|
+
var accuracy = if (overwritten) state.* else @as(u16, Gen12.percent(move.accuracy));
|
|
1349
1337
|
var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
|
|
1350
1338
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1351
1339
|
boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
|
|
1352
1340
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1353
1341
|
accuracy = @min(255, @max(1, accuracy));
|
|
1354
1342
|
|
|
1355
|
-
|
|
1356
|
-
if (showdown) overwrite = side.active.volatiles.Rage or side.active.volatiles.Thrashing;
|
|
1357
|
-
if (overwrite) side.active.volatiles.state = accuracy;
|
|
1343
|
+
if (move.effect == .Thrashing or move.effect == .Rage) state.* = accuracy;
|
|
1358
1344
|
|
|
1359
1345
|
// GLITCH: max accuracy is 255 so 1/256 chance of miss
|
|
1360
|
-
break :miss
|
|
1346
|
+
break :miss if (showdown)
|
|
1347
|
+
!battle.rng.chance(u8, @intCast(u8, accuracy), 256)
|
|
1348
|
+
else
|
|
1349
|
+
battle.rng.next() >= accuracy;
|
|
1361
1350
|
};
|
|
1362
1351
|
|
|
1363
1352
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1489,6 +1478,9 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
|
|
|
1489
1478
|
damage *= volatiles.toxic;
|
|
1490
1479
|
}
|
|
1491
1480
|
|
|
1481
|
+
// Pokémon Showdown erroneously updates last damage with uncapped Leech Seed damage
|
|
1482
|
+
if (showdown) battle.last_damage = damage;
|
|
1483
|
+
|
|
1492
1484
|
const amount = @min(damage, stored.hp);
|
|
1493
1485
|
stored.hp -= amount;
|
|
1494
1486
|
|
|
@@ -1580,42 +1572,6 @@ fn checkEBC(battle: anytype) bool {
|
|
|
1580
1572
|
return false;
|
|
1581
1573
|
}
|
|
1582
1574
|
|
|
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
1575
|
inline fn onBegin(
|
|
1620
1576
|
battle: anytype,
|
|
1621
1577
|
player: Player,
|
|
@@ -1706,7 +1662,10 @@ pub const Effects = struct {
|
|
|
1706
1662
|
side.active.volatiles.Bide = true;
|
|
1707
1663
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1708
1664
|
side.active.volatiles.state = 0;
|
|
1709
|
-
side.active.volatiles.attacks =
|
|
1665
|
+
side.active.volatiles.attacks = @intCast(u3, if (showdown)
|
|
1666
|
+
battle.rng.range(u4, 2, 4)
|
|
1667
|
+
else
|
|
1668
|
+
(battle.rng.next() & 1) + 2);
|
|
1710
1669
|
|
|
1711
1670
|
try log.start(battle.active(player), .Bide);
|
|
1712
1671
|
}
|
|
@@ -1728,8 +1687,14 @@ pub const Effects = struct {
|
|
|
1728
1687
|
return;
|
|
1729
1688
|
}
|
|
1730
1689
|
|
|
1731
|
-
|
|
1732
|
-
|
|
1690
|
+
const chance = !foe.active.types.includes(move.type) and if (showdown)
|
|
1691
|
+
battle.rng.chance(u8, @as(u8, if (move.effect == .BurnChance1) 26 else 77), 256)
|
|
1692
|
+
else
|
|
1693
|
+
battle.rng.next() < 1 + (if (move.effect == .BurnChance1)
|
|
1694
|
+
Gen12.percent(10)
|
|
1695
|
+
else
|
|
1696
|
+
Gen12.percent(30));
|
|
1697
|
+
if (!chance) return;
|
|
1733
1698
|
|
|
1734
1699
|
foe_stored.status = Status.init(.BRN);
|
|
1735
1700
|
foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
|
|
@@ -1753,7 +1718,11 @@ pub const Effects = struct {
|
|
|
1753
1718
|
const sub = foe.active.volatiles.Substitute;
|
|
1754
1719
|
|
|
1755
1720
|
if (move.effect == .ConfusionChance) {
|
|
1756
|
-
if (
|
|
1721
|
+
const chance = if (showdown)
|
|
1722
|
+
battle.rng.chance(u8, 25, 256)
|
|
1723
|
+
else
|
|
1724
|
+
battle.rng.next() < Gen12.percent(10);
|
|
1725
|
+
if (!chance) return;
|
|
1757
1726
|
} else {
|
|
1758
1727
|
if (showdown) {
|
|
1759
1728
|
if (!try checkHit(battle, player, move, log)) {
|
|
@@ -1772,7 +1741,10 @@ pub const Effects = struct {
|
|
|
1772
1741
|
|
|
1773
1742
|
if (foe.active.volatiles.Confusion) return;
|
|
1774
1743
|
foe.active.volatiles.Confusion = true;
|
|
1775
|
-
foe.active.volatiles.confusion =
|
|
1744
|
+
foe.active.volatiles.confusion = @intCast(u3, if (showdown)
|
|
1745
|
+
battle.rng.range(u8, 2, 6)
|
|
1746
|
+
else
|
|
1747
|
+
(battle.rng.next() & 3) + 2);
|
|
1776
1748
|
|
|
1777
1749
|
try log.start(battle.active(player.foe()), .Confusion);
|
|
1778
1750
|
}
|
|
@@ -1822,8 +1794,11 @@ pub const Effects = struct {
|
|
|
1822
1794
|
return Result.Error;
|
|
1823
1795
|
}
|
|
1824
1796
|
|
|
1825
|
-
volatiles.disabled_move = @intCast(u3,
|
|
1826
|
-
volatiles.disabled_duration =
|
|
1797
|
+
volatiles.disabled_move = @intCast(u3, randomMoveSlot(&battle.rng, &foe.active.moves, n));
|
|
1798
|
+
volatiles.disabled_duration = @intCast(u4, if (showdown)
|
|
1799
|
+
battle.rng.range(u8, 1, 9)
|
|
1800
|
+
else
|
|
1801
|
+
(battle.rng.next() & 7) + 1);
|
|
1827
1802
|
|
|
1828
1803
|
try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
|
|
1829
1804
|
return null;
|
|
@@ -1835,7 +1810,7 @@ pub const Effects = struct {
|
|
|
1835
1810
|
|
|
1836
1811
|
if (battle.last_damage == 0) {
|
|
1837
1812
|
assert(showdown);
|
|
1838
|
-
|
|
1813
|
+
return;
|
|
1839
1814
|
}
|
|
1840
1815
|
|
|
1841
1816
|
const drain = @max(battle.last_damage / 2, 1);
|
|
@@ -1862,7 +1837,15 @@ pub const Effects = struct {
|
|
|
1862
1837
|
var volatiles = &battle.foe(player).active.volatiles;
|
|
1863
1838
|
|
|
1864
1839
|
if (volatiles.Substitute) return;
|
|
1865
|
-
|
|
1840
|
+
|
|
1841
|
+
const chance = if (showdown)
|
|
1842
|
+
battle.rng.chance(u8, @as(u8, if (move.effect == .FlinchChance1) 26 else 77), 256)
|
|
1843
|
+
else
|
|
1844
|
+
battle.rng.next() < 1 + (if (move.effect == .FlinchChance1)
|
|
1845
|
+
Gen12.percent(10)
|
|
1846
|
+
else
|
|
1847
|
+
Gen12.percent(30));
|
|
1848
|
+
if (!chance) return;
|
|
1866
1849
|
|
|
1867
1850
|
volatiles.Flinch = true;
|
|
1868
1851
|
volatiles.Recharging = false;
|
|
@@ -1888,8 +1871,12 @@ pub const Effects = struct {
|
|
|
1888
1871
|
return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
|
|
1889
1872
|
}
|
|
1890
1873
|
|
|
1891
|
-
|
|
1892
|
-
|
|
1874
|
+
const chance = !foe.active.types.includes(move.type) and if (showdown)
|
|
1875
|
+
battle.rng.chance(u8, 26, 256)
|
|
1876
|
+
else
|
|
1877
|
+
battle.rng.next() < 1 + Gen12.percent(10);
|
|
1878
|
+
if (!chance) return;
|
|
1879
|
+
|
|
1893
1880
|
// Freeze Clause Mod
|
|
1894
1881
|
if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
1895
1882
|
|
|
@@ -2034,7 +2021,7 @@ pub const Effects = struct {
|
|
|
2034
2021
|
}
|
|
2035
2022
|
if (!try checkHit(battle, player, move, log)) return;
|
|
2036
2023
|
|
|
2037
|
-
const rslot =
|
|
2024
|
+
const rslot = randomMoveSlot(&battle.rng, &foe.active.moves, 0);
|
|
2038
2025
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
2039
2026
|
|
|
2040
2027
|
try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
|
|
@@ -2055,8 +2042,7 @@ pub const Effects = struct {
|
|
|
2055
2042
|
assert(!side.active.volatiles.MultiHit);
|
|
2056
2043
|
side.active.volatiles.MultiHit = true;
|
|
2057
2044
|
|
|
2058
|
-
side.active.volatiles.attacks =
|
|
2059
|
-
if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
|
|
2045
|
+
side.active.volatiles.attacks = if (move.effect == .MultiHit) distribution(battle) else 2;
|
|
2060
2046
|
}
|
|
2061
2047
|
|
|
2062
2048
|
fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2100,8 +2086,14 @@ pub const Effects = struct {
|
|
|
2100
2086
|
}
|
|
2101
2087
|
|
|
2102
2088
|
// Body Slam can't paralyze a Normal type Pokémon
|
|
2103
|
-
|
|
2104
|
-
|
|
2089
|
+
const chance = !foe.active.types.includes(move.type) and if (showdown)
|
|
2090
|
+
battle.rng.chance(u8, @as(u8, if (move.effect == .ParalyzeChance1) 26 else 77), 256)
|
|
2091
|
+
else
|
|
2092
|
+
battle.rng.next() < 1 + (if (move.effect == .ParalyzeChance1)
|
|
2093
|
+
Gen12.percent(10)
|
|
2094
|
+
else
|
|
2095
|
+
Gen12.percent(30));
|
|
2096
|
+
if (!chance) return;
|
|
2105
2097
|
|
|
2106
2098
|
foe_stored.status = Status.init(.PAR);
|
|
2107
2099
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
@@ -2144,7 +2136,16 @@ pub const Effects = struct {
|
|
|
2144
2136
|
|
|
2145
2137
|
if (move.effect == .Poison) {
|
|
2146
2138
|
if (!showdown and !try checkHit(battle, player, move, log)) return;
|
|
2147
|
-
} else
|
|
2139
|
+
} else {
|
|
2140
|
+
const chance = if (showdown)
|
|
2141
|
+
battle.rng.chance(u8, @as(u8, if (move.effect == .PoisonChance1) 52 else 103), 256)
|
|
2142
|
+
else
|
|
2143
|
+
battle.rng.next() < 1 + (if (move.effect == .PoisonChance1)
|
|
2144
|
+
Gen12.percent(20)
|
|
2145
|
+
else
|
|
2146
|
+
Gen12.percent(40));
|
|
2147
|
+
if (!chance) return;
|
|
2148
|
+
}
|
|
2148
2149
|
|
|
2149
2150
|
foe_stored.status = Status.init(.PSN);
|
|
2150
2151
|
if (toxic) {
|
|
@@ -2158,8 +2159,10 @@ pub const Effects = struct {
|
|
|
2158
2159
|
|
|
2159
2160
|
fn rage(battle: anytype, player: Player) !void {
|
|
2160
2161
|
var volatiles = &battle.side(player).active.volatiles;
|
|
2161
|
-
|
|
2162
|
+
if (volatiles.Rage) return;
|
|
2162
2163
|
volatiles.Rage = true;
|
|
2164
|
+
assert(!volatiles.Bide);
|
|
2165
|
+
volatiles.state = 0;
|
|
2163
2166
|
}
|
|
2164
2167
|
|
|
2165
2168
|
fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
|
|
@@ -2211,7 +2214,16 @@ pub const Effects = struct {
|
|
|
2211
2214
|
}
|
|
2212
2215
|
foe.active.volatiles.Recharging = false;
|
|
2213
2216
|
|
|
2214
|
-
|
|
2217
|
+
const duration = @intCast(u3, if (showdown)
|
|
2218
|
+
battle.rng.range(u8, 1, 8)
|
|
2219
|
+
else loop: {
|
|
2220
|
+
while (true) {
|
|
2221
|
+
const r = battle.rng.next() & 7;
|
|
2222
|
+
if (r != 0) break :loop r;
|
|
2223
|
+
}
|
|
2224
|
+
});
|
|
2225
|
+
|
|
2226
|
+
foe_stored.status = Status.slp(duration);
|
|
2215
2227
|
try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
|
|
2216
2228
|
}
|
|
2217
2229
|
|
|
@@ -2264,7 +2276,11 @@ pub const Effects = struct {
|
|
|
2264
2276
|
assert(!volatiles.Bide);
|
|
2265
2277
|
|
|
2266
2278
|
volatiles.Thrashing = true;
|
|
2267
|
-
volatiles.
|
|
2279
|
+
volatiles.state = 0;
|
|
2280
|
+
volatiles.attacks = @intCast(u3, if (showdown)
|
|
2281
|
+
battle.rng.range(u8, 2, 4)
|
|
2282
|
+
else
|
|
2283
|
+
(battle.rng.next() & 1) + 2);
|
|
2268
2284
|
}
|
|
2269
2285
|
|
|
2270
2286
|
fn transform(battle: anytype, player: Player, log: anytype) !void {
|
|
@@ -2296,7 +2312,7 @@ pub const Effects = struct {
|
|
|
2296
2312
|
try log.transform(battle.active(player), foe_ident);
|
|
2297
2313
|
}
|
|
2298
2314
|
|
|
2299
|
-
fn binding(battle: anytype, player: Player
|
|
2315
|
+
fn binding(battle: anytype, player: Player) void {
|
|
2300
2316
|
var side = battle.side(player);
|
|
2301
2317
|
var foe = battle.foe(player);
|
|
2302
2318
|
|
|
@@ -2304,11 +2320,9 @@ pub const Effects = struct {
|
|
|
2304
2320
|
side.active.volatiles.Binding = true;
|
|
2305
2321
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2306
2322
|
// (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);
|
|
2323
|
+
if (!showdown) foe.active.volatiles.Recharging = false;
|
|
2310
2324
|
|
|
2311
|
-
side.active.volatiles.attacks =
|
|
2325
|
+
side.active.volatiles.attacks = distribution(battle) - 1;
|
|
2312
2326
|
}
|
|
2313
2327
|
|
|
2314
2328
|
fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2335,7 +2349,7 @@ pub const Effects = struct {
|
|
|
2335
2349
|
return try log.fail(ident, .None);
|
|
2336
2350
|
}
|
|
2337
2351
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2338
|
-
const stat = unmodifiedStats(battle,
|
|
2352
|
+
const stat = unmodifiedStats(battle, player).atk;
|
|
2339
2353
|
stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2340
2354
|
try log.boost(ident, reason, n);
|
|
2341
2355
|
// Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
|
|
@@ -2355,7 +2369,7 @@ pub const Effects = struct {
|
|
|
2355
2369
|
return try log.fail(ident, .None);
|
|
2356
2370
|
}
|
|
2357
2371
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2358
|
-
const stat = unmodifiedStats(battle,
|
|
2372
|
+
const stat = unmodifiedStats(battle, player).def;
|
|
2359
2373
|
stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2360
2374
|
try log.boost(ident, .Defense, n);
|
|
2361
2375
|
},
|
|
@@ -2372,7 +2386,7 @@ pub const Effects = struct {
|
|
|
2372
2386
|
return try log.fail(ident, .None);
|
|
2373
2387
|
}
|
|
2374
2388
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2375
|
-
const stat = unmodifiedStats(battle,
|
|
2389
|
+
const stat = unmodifiedStats(battle, player).spe;
|
|
2376
2390
|
stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2377
2391
|
try log.boost(ident, .Speed, 2);
|
|
2378
2392
|
},
|
|
@@ -2392,7 +2406,7 @@ pub const Effects = struct {
|
|
|
2392
2406
|
return try log.fail(ident, .None);
|
|
2393
2407
|
}
|
|
2394
2408
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2395
|
-
const stat = unmodifiedStats(battle,
|
|
2409
|
+
const stat = unmodifiedStats(battle, player).spc;
|
|
2396
2410
|
stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2397
2411
|
try log.boost(ident, .SpecialAttack, n);
|
|
2398
2412
|
try log.boost(ident, .SpecialDefense, n);
|
|
@@ -2419,7 +2433,11 @@ pub const Effects = struct {
|
|
|
2419
2433
|
}
|
|
2420
2434
|
|
|
2421
2435
|
if (move.effect.isStatDownChance()) {
|
|
2422
|
-
if (
|
|
2436
|
+
const chance = if (showdown)
|
|
2437
|
+
battle.rng.chance(u8, 85, 256)
|
|
2438
|
+
else
|
|
2439
|
+
battle.rng.next() < Gen12.percent(33) + 1;
|
|
2440
|
+
if (!chance or foe.active.volatiles.Invulnerable) return;
|
|
2423
2441
|
} else if (!showdown and !try checkHit(battle, player, move, log)) {
|
|
2424
2442
|
return; // checkHit already checks for Invulnerable
|
|
2425
2443
|
}
|
|
@@ -2441,7 +2459,7 @@ pub const Effects = struct {
|
|
|
2441
2459
|
return try log.fail(foe_ident, .None);
|
|
2442
2460
|
}
|
|
2443
2461
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2444
|
-
const stat = unmodifiedStats(battle, foe).atk;
|
|
2462
|
+
const stat = unmodifiedStats(battle, player.foe()).atk;
|
|
2445
2463
|
stats.atk = @max(1, stat * mod[0] / mod[1]);
|
|
2446
2464
|
try log.boost(foe_ident, .Attack, -1);
|
|
2447
2465
|
},
|
|
@@ -2459,7 +2477,7 @@ pub const Effects = struct {
|
|
|
2459
2477
|
return try log.fail(foe_ident, .None);
|
|
2460
2478
|
}
|
|
2461
2479
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2462
|
-
const stat = unmodifiedStats(battle, foe).def;
|
|
2480
|
+
const stat = unmodifiedStats(battle, player.foe()).def;
|
|
2463
2481
|
stats.def = @max(1, stat * mod[0] / mod[1]);
|
|
2464
2482
|
try log.boost(foe_ident, .Defense, -@as(i8, n));
|
|
2465
2483
|
},
|
|
@@ -2476,7 +2494,7 @@ pub const Effects = struct {
|
|
|
2476
2494
|
return try log.fail(foe_ident, .None);
|
|
2477
2495
|
}
|
|
2478
2496
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2479
|
-
const stat = unmodifiedStats(battle, foe).spe;
|
|
2497
|
+
const stat = unmodifiedStats(battle, player.foe()).spe;
|
|
2480
2498
|
stats.spe = @max(1, stat * mod[0] / mod[1]);
|
|
2481
2499
|
try log.boost(foe_ident, .Speed, -1);
|
|
2482
2500
|
assert(boosts.spe >= -6);
|
|
@@ -2496,7 +2514,7 @@ pub const Effects = struct {
|
|
|
2496
2514
|
return try log.fail(foe_ident, .None);
|
|
2497
2515
|
}
|
|
2498
2516
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2499
|
-
const stat = unmodifiedStats(battle, foe).spc;
|
|
2517
|
+
const stat = unmodifiedStats(battle, player.foe()).spc;
|
|
2500
2518
|
stats.spc = @max(1, stat * mod[0] / mod[1]);
|
|
2501
2519
|
try log.boost(foe_ident, .SpecialAttack, -1);
|
|
2502
2520
|
try log.boost(foe_ident, .SpecialDefense, -1);
|
|
@@ -2515,7 +2533,8 @@ pub const Effects = struct {
|
|
|
2515
2533
|
}
|
|
2516
2534
|
};
|
|
2517
2535
|
|
|
2518
|
-
fn unmodifiedStats(battle: anytype,
|
|
2536
|
+
fn unmodifiedStats(battle: anytype, who: Player) *Stats(u16) {
|
|
2537
|
+
const side = battle.side(who);
|
|
2519
2538
|
if (!side.active.volatiles.Transform) return &side.stored().stats;
|
|
2520
2539
|
const id = ID.from(side.active.volatiles.transform);
|
|
2521
2540
|
return &battle.side(id.player).pokemon[id.id - 1].stats;
|
|
@@ -2581,140 +2600,80 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
|
|
|
2581
2600
|
}
|
|
2582
2601
|
}
|
|
2583
2602
|
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
while (true) {
|
|
2598
|
-
const r = std.math.rotr(u8, battle.rng.next(), 1);
|
|
2599
|
-
if (r >= 217) return r;
|
|
2603
|
+
fn disabled(side: *Side, ident: ID, log: anytype) !bool {
|
|
2604
|
+
if (side.active.volatiles.disabled_move != 0) {
|
|
2605
|
+
// A Pokémon that transforms after being disabled may end up with less move slots
|
|
2606
|
+
const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
|
|
2607
|
+
// side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
|
|
2608
|
+
const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
|
|
2609
|
+
m
|
|
2610
|
+
else
|
|
2611
|
+
side.last_selected_move;
|
|
2612
|
+
if (m != .None and m == last) {
|
|
2613
|
+
side.active.volatiles.Charging = false;
|
|
2614
|
+
try log.disabled(ident, last);
|
|
2615
|
+
return true;
|
|
2600
2616
|
}
|
|
2601
2617
|
}
|
|
2618
|
+
return false;
|
|
2619
|
+
}
|
|
2602
2620
|
|
|
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);
|
|
2621
|
+
inline fn buildRage(battle: anytype, who: Player, log: anytype) !void {
|
|
2622
|
+
const side = battle.side(who);
|
|
2623
|
+
if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
|
|
2624
|
+
try Effects.boost(battle, who, Move.get(.Rage), log);
|
|
2621
2625
|
}
|
|
2626
|
+
}
|
|
2622
2627
|
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2628
|
+
fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
|
|
2629
|
+
var volatiles = &active.volatiles;
|
|
2630
|
+
assert(volatiles.Thrashing);
|
|
2631
|
+
if (volatiles.attacks > 0) return false;
|
|
2627
2632
|
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2633
|
+
volatiles.Thrashing = false;
|
|
2634
|
+
volatiles.Confusion = true;
|
|
2635
|
+
volatiles.confusion = @intCast(u3, if (showdown)
|
|
2636
|
+
battle.rng.range(u8, 2, 6)
|
|
2637
|
+
else
|
|
2638
|
+
(battle.rng.next() & 3) + 2);
|
|
2632
2639
|
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
return battle.rng.next() < 1 + (if (low) Gen12.percent(20) else Gen12.percent(40));
|
|
2636
|
-
}
|
|
2640
|
+
return true;
|
|
2641
|
+
}
|
|
2637
2642
|
|
|
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
|
-
}
|
|
2643
|
+
const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
|
|
2645
2644
|
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2645
|
+
fn distribution(battle: anytype) u3 {
|
|
2646
|
+
if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
|
|
2647
|
+
const r = (battle.rng.next() & 3);
|
|
2648
|
+
return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
|
|
2649
|
+
}
|
|
2650
2650
|
|
|
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
|
-
}
|
|
2651
|
+
fn randomMoveSlot(rand: anytype, moves: []MoveSlot, check_pp: u4) u4 {
|
|
2652
|
+
if (showdown) {
|
|
2653
|
+
if (check_pp == 0) {
|
|
2654
|
+
var i: usize = moves.len;
|
|
2655
|
+
while (i > 0) {
|
|
2656
|
+
i -= 1;
|
|
2657
|
+
if (moves[i].id != .None) return rand.range(u4, 0, @intCast(u4, i + 1)) + 1;
|
|
2658
|
+
}
|
|
2659
|
+
} else {
|
|
2660
|
+
var r = rand.range(u4, 0, @intCast(u4, check_pp)) + 1;
|
|
2661
|
+
var i: usize = 0;
|
|
2662
|
+
while (i < moves.len and r > 0) : (i += 1) {
|
|
2663
|
+
if (moves[i].pp > 0) {
|
|
2664
|
+
r -= 1;
|
|
2665
|
+
if (r == 0) break;
|
|
2669
2666
|
}
|
|
2670
|
-
return @intCast(u4, i + 1);
|
|
2671
2667
|
}
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
while (true) {
|
|
2675
|
-
const r = @intCast(u4, battle.rng.next() & 3);
|
|
2676
|
-
if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
|
|
2668
|
+
return @intCast(u4, i + 1);
|
|
2677
2669
|
}
|
|
2678
2670
|
}
|
|
2679
2671
|
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2672
|
+
while (true) {
|
|
2673
|
+
const r = @intCast(u4, rand.next() & 3);
|
|
2674
|
+
if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
|
|
2683
2675
|
}
|
|
2684
|
-
|
|
2685
|
-
inline fn metronome(battle: anytype) Move {
|
|
2686
|
-
if (showdown) {
|
|
2687
|
-
const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
|
|
2688
|
-
const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
|
|
2689
|
-
return @intToEnum(Move, r + mod);
|
|
2690
|
-
}
|
|
2691
|
-
while (true) {
|
|
2692
|
-
const r = battle.rng.next();
|
|
2693
|
-
if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
|
|
2694
|
-
if (r >= @enumToInt(Move.Struggle)) continue;
|
|
2695
|
-
return @intToEnum(Move, r);
|
|
2696
|
-
}
|
|
2697
|
-
}
|
|
2698
|
-
|
|
2699
|
-
inline fn psywave(battle: anytype, max: u8) u8 {
|
|
2700
|
-
if (showdown) return battle.rng.range(u8, 0, max);
|
|
2701
|
-
while (true) {
|
|
2702
|
-
const r = battle.rng.next();
|
|
2703
|
-
if (r < max) return r;
|
|
2704
|
-
}
|
|
2705
|
-
}
|
|
2706
|
-
|
|
2707
|
-
inline fn distribution(battle: anytype) u3 {
|
|
2708
|
-
if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
|
|
2709
|
-
const r = (battle.rng.next() & 3);
|
|
2710
|
-
return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
|
|
2711
|
-
}
|
|
2712
|
-
|
|
2713
|
-
inline fn unboost(battle: anytype) bool {
|
|
2714
|
-
if (showdown) return battle.rng.chance(u8, 85, 256);
|
|
2715
|
-
return battle.rng.next() < Gen12.percent(33) + 1;
|
|
2716
|
-
}
|
|
2717
|
-
};
|
|
2676
|
+
}
|
|
2718
2677
|
|
|
2719
2678
|
test "RNG agreement" {
|
|
2720
2679
|
if (!showdown) return;
|