@pkmn/engine 0.1.0-dev.b9fb6eab → 0.1.0-dev.cf6f5d2c
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -22
- package/build/pkg/addon.d.ts +7 -0
- package/build/pkg/addon.js +111 -0
- package/build/pkg/addon.js.map +1 -0
- package/build/pkg/data.d.ts +1 -2
- package/build/pkg/data.js +3 -5
- package/build/pkg/data.js.map +1 -1
- package/build/pkg/gen1.d.ts +3 -3
- package/build/pkg/gen1.js +11 -13
- package/build/pkg/gen1.js.map +1 -1
- package/build/pkg/index.d.ts +11 -10
- package/build/pkg/index.js.map +1 -1
- package/build/test/display.js +396 -0
- package/build/test/display.js.map +1 -0
- package/build/tools/debug.js +133 -0
- package/build/tools/debug.js.map +1 -0
- package/build.zig +17 -22
- package/package.json +40 -29
- package/src/bin/install-pkmn-engine +3 -3
- package/src/bin/pkmn-debug +62 -0
- package/src/lib/binding/node.zig +5 -5
- package/src/lib/binding/wasm.zig +3 -3
- package/src/lib/common/debug.zig +1 -0
- package/src/lib/common/options.zig +3 -3
- package/src/lib/common/protocol.zig +38 -38
- package/src/lib/gen1/README.md +47 -23
- package/src/lib/gen1/data.zig +6 -6
- package/src/lib/gen1/helpers.zig +10 -7
- package/src/lib/gen1/mechanics.zig +347 -288
- package/src/lib/gen2/data.zig +4 -4
- package/src/lib/pkmn.zig +7 -7
- package/src/test/display/pkmn.html.tmpl +368 -0
|
@@ -56,30 +56,34 @@ const BOOSTS = &[_][2]u8{
|
|
|
56
56
|
|
|
57
57
|
const MAX_STAT_VALUE = 999;
|
|
58
58
|
|
|
59
|
+
const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
|
|
60
|
+
|
|
59
61
|
pub fn update(battle: anytype, c1: Choice, c2: Choice, log: anytype) !Result {
|
|
60
62
|
assert(c1.type != .Pass or c2.type != .Pass or battle.turn == 0);
|
|
61
63
|
if (battle.turn == 0) return start(battle, log);
|
|
62
64
|
|
|
65
|
+
var s1 = false;
|
|
66
|
+
var s2 = false;
|
|
63
67
|
var f1: ?Move = null;
|
|
64
68
|
var f2: ?Move = null;
|
|
65
69
|
|
|
66
|
-
if (selectMove(battle, .P1, c1, c2, &f1)) |r| return r;
|
|
67
|
-
if (selectMove(battle, .P2, c2, c1, &f2)) |r| return r;
|
|
70
|
+
if (selectMove(battle, .P1, c1, c2, &s1, &f1)) |r| return r;
|
|
71
|
+
if (selectMove(battle, .P2, c2, c1, &s2, &f2)) |r| return r;
|
|
72
|
+
|
|
73
|
+
var p1 = battle.side(.P1);
|
|
74
|
+
var p2 = battle.side(.P2);
|
|
75
|
+
|
|
76
|
+
var r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
|
|
77
|
+
var r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
|
|
68
78
|
|
|
69
79
|
if (turnOrder(battle, c1, c2) == .P1) {
|
|
70
|
-
if (try doTurn(battle, .P1, c1, f1, .P2, c2, f2, log)) |r| return r;
|
|
80
|
+
if (try doTurn(battle, .P1, c1, r1, s1, f1, .P2, c2, r2, s2, f2, log)) |r| return r;
|
|
71
81
|
} else {
|
|
72
|
-
if (try doTurn(battle, .P2, c2, f2, .P1, c1, f1, log)) |r| return r;
|
|
82
|
+
if (try doTurn(battle, .P2, c2, r2, s2, f2, .P1, c1, r1, s1, f1, log)) |r| return r;
|
|
73
83
|
}
|
|
74
84
|
|
|
75
|
-
|
|
76
|
-
if (
|
|
77
|
-
p1.active.volatiles.Binding = false;
|
|
78
|
-
}
|
|
79
|
-
var p2 = battle.side(.P2);
|
|
80
|
-
if (p2.active.volatiles.attacks == 0) {
|
|
81
|
-
p2.active.volatiles.Binding = false;
|
|
82
|
-
}
|
|
85
|
+
if (p1.active.volatiles.attacks == 0) p1.active.volatiles.Binding = false;
|
|
86
|
+
if (p2.active.volatiles.attacks == 0) p2.active.volatiles.Binding = false;
|
|
83
87
|
|
|
84
88
|
return endTurn(battle, log);
|
|
85
89
|
}
|
|
@@ -112,6 +116,7 @@ fn selectMove(
|
|
|
112
116
|
player: Player,
|
|
113
117
|
choice: Choice,
|
|
114
118
|
foe_choice: Choice,
|
|
119
|
+
skip_turn: *bool,
|
|
115
120
|
from: *?Move,
|
|
116
121
|
) ?Result {
|
|
117
122
|
if (choice.type == .Pass) return null;
|
|
@@ -124,17 +129,26 @@ fn selectMove(
|
|
|
124
129
|
(choice.type == .Move and choice.data == @boolToInt(showdown)));
|
|
125
130
|
|
|
126
131
|
// pre-battle menu
|
|
127
|
-
if (volatiles.Recharging)
|
|
132
|
+
if (volatiles.Recharging) {
|
|
133
|
+
if (showdown and battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
128
136
|
if (volatiles.Rage) {
|
|
129
137
|
from.* = side.last_used_move;
|
|
130
|
-
if (showdown)
|
|
138
|
+
if (showdown) {
|
|
139
|
+
if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
|
|
140
|
+
saveMove(battle, player, null);
|
|
141
|
+
}
|
|
131
142
|
return null;
|
|
132
143
|
}
|
|
133
144
|
// Pokémon Showdown removes Flinch at the end-of-turn in its residual handler
|
|
134
145
|
if (!showdown) volatiles.Flinch = false;
|
|
135
146
|
if (volatiles.Thrashing or volatiles.Charging) {
|
|
136
147
|
from.* = side.last_selected_move;
|
|
137
|
-
if (showdown)
|
|
148
|
+
if (showdown) {
|
|
149
|
+
if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
|
|
150
|
+
saveMove(battle, player, null);
|
|
151
|
+
}
|
|
138
152
|
return null;
|
|
139
153
|
}
|
|
140
154
|
|
|
@@ -144,7 +158,10 @@ fn selectMove(
|
|
|
144
158
|
// pre-move select
|
|
145
159
|
if (Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP) or volatiles.Bide) {
|
|
146
160
|
assert(showdown or choice.data == 0);
|
|
147
|
-
if (showdown)
|
|
161
|
+
if (showdown) {
|
|
162
|
+
if (volatiles.Bide and battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
|
|
163
|
+
saveMove(battle, player, choice);
|
|
164
|
+
}
|
|
148
165
|
return null;
|
|
149
166
|
}
|
|
150
167
|
if (volatiles.Binding) {
|
|
@@ -175,6 +192,7 @@ fn selectMove(
|
|
|
175
192
|
}
|
|
176
193
|
|
|
177
194
|
if (battle.foe(player).active.volatiles.Binding) {
|
|
195
|
+
skip_turn.* = true;
|
|
178
196
|
if (showdown) {
|
|
179
197
|
saveMove(battle, player, choice);
|
|
180
198
|
} else {
|
|
@@ -185,6 +203,7 @@ fn selectMove(
|
|
|
185
203
|
}
|
|
186
204
|
|
|
187
205
|
// move select
|
|
206
|
+
volatiles.state = 0;
|
|
188
207
|
if (choice.data == 0) {
|
|
189
208
|
const struggle = ok: {
|
|
190
209
|
for (side.active.moves, 0..) |move, i| {
|
|
@@ -301,11 +320,7 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
|
|
|
301
320
|
// Pokémon Showdown's beforeTurnCallback shenanigans
|
|
302
321
|
if (showdown and m1 == .Counter and m2 == .Counter) battle.rng.advance(1);
|
|
303
322
|
|
|
304
|
-
const p1 =
|
|
305
|
-
battle.rng.range(u8, 0, 2) == 0
|
|
306
|
-
else
|
|
307
|
-
battle.rng.next() < Gen12.percent(50) + 1;
|
|
308
|
-
|
|
323
|
+
const p1 = Rolls.speedTie(battle);
|
|
309
324
|
if (!showdown) return if (p1) .P1 else .P2;
|
|
310
325
|
|
|
311
326
|
// Pokémon Showdown's "lockedmove" volatile's onBeforeTurn uses BattleQueue#changeAction,
|
|
@@ -315,7 +330,7 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
|
|
|
315
330
|
const t1 = battle.side(.P1).active.volatiles.Thrashing;
|
|
316
331
|
const t2 = battle.side(.P2).active.volatiles.Thrashing;
|
|
317
332
|
// If *both* sides are thrashing it really should be another speed tie, but we've patched
|
|
318
|
-
// that out and enforce host ordering of events, so P1 just goes first
|
|
333
|
+
// that out and enforce host ordering of events, so P1 just goes first regardless of who
|
|
319
334
|
// won the original coin flip
|
|
320
335
|
if (t1 and t2) return .P1;
|
|
321
336
|
return if (p1) if (t1 and !t2) .P2 else .P1 else if (t2 and !t1) .P1 else .P2;
|
|
@@ -328,9 +343,13 @@ fn doTurn(
|
|
|
328
343
|
battle: anytype,
|
|
329
344
|
player: Player,
|
|
330
345
|
player_choice: Choice,
|
|
346
|
+
player_rewrap: bool,
|
|
347
|
+
player_skip: bool,
|
|
331
348
|
player_from: ?Move,
|
|
332
349
|
foe_player: Player,
|
|
333
350
|
foe_choice: Choice,
|
|
351
|
+
foe_rewrap: bool,
|
|
352
|
+
foe_skip: bool,
|
|
334
353
|
foe_from: ?Move,
|
|
335
354
|
log: anytype,
|
|
336
355
|
) !?Result {
|
|
@@ -338,7 +357,16 @@ fn doTurn(
|
|
|
338
357
|
|
|
339
358
|
var residual = true;
|
|
340
359
|
var replace = battle.side(player).stored().hp == 0;
|
|
341
|
-
if (try executeMove(
|
|
360
|
+
if (try executeMove(
|
|
361
|
+
battle,
|
|
362
|
+
player,
|
|
363
|
+
player_choice,
|
|
364
|
+
player_rewrap,
|
|
365
|
+
player_skip,
|
|
366
|
+
player_from,
|
|
367
|
+
&residual,
|
|
368
|
+
log,
|
|
369
|
+
)) |r| return r;
|
|
342
370
|
if (!replace) {
|
|
343
371
|
if (player_choice.type != .Switch) {
|
|
344
372
|
if (try checkFaint(battle, foe_player, log)) |r| return r;
|
|
@@ -349,7 +377,16 @@ fn doTurn(
|
|
|
349
377
|
|
|
350
378
|
residual = true;
|
|
351
379
|
replace = battle.side(foe_player).stored().hp == 0;
|
|
352
|
-
if (try executeMove(
|
|
380
|
+
if (try executeMove(
|
|
381
|
+
battle,
|
|
382
|
+
foe_player,
|
|
383
|
+
foe_choice,
|
|
384
|
+
foe_rewrap,
|
|
385
|
+
foe_skip,
|
|
386
|
+
foe_from,
|
|
387
|
+
&residual,
|
|
388
|
+
log,
|
|
389
|
+
)) |r| return r;
|
|
353
390
|
if (!replace) {
|
|
354
391
|
if (foe_choice.type != .Switch) {
|
|
355
392
|
if (try checkFaint(battle, player, log)) |r| return r;
|
|
@@ -372,6 +409,8 @@ fn executeMove(
|
|
|
372
409
|
battle: anytype,
|
|
373
410
|
player: Player,
|
|
374
411
|
choice: Choice,
|
|
412
|
+
rewrap: bool,
|
|
413
|
+
skip: bool,
|
|
375
414
|
from: ?Move,
|
|
376
415
|
residual: *bool,
|
|
377
416
|
log: anytype,
|
|
@@ -383,9 +422,10 @@ fn executeMove(
|
|
|
383
422
|
return null;
|
|
384
423
|
}
|
|
385
424
|
|
|
425
|
+
// This is the correct place to check for SKIP_TURN and abort early, however since Pokémon
|
|
426
|
+
// Showdown overwrites the SKIP_TURN sentinel with its botched move select we need to add an
|
|
427
|
+
// additional skip boolean to accomplish the same thing in the Binding check of BeforeMove
|
|
386
428
|
if (side.last_selected_move == .SKIP_TURN) {
|
|
387
|
-
// Pokémon Showdown overwrites the SKIP_TURN sentinel with its botched move select,
|
|
388
|
-
// Binding instead gets handled in beforeMove after sleep and freeze
|
|
389
429
|
assert(!showdown);
|
|
390
430
|
if (battle.foe(player).active.volatiles.Binding) {
|
|
391
431
|
try log.cant(battle.active(player), .Bound);
|
|
@@ -435,7 +475,7 @@ fn executeMove(
|
|
|
435
475
|
|
|
436
476
|
var skip_can = false;
|
|
437
477
|
var skip_pp = false;
|
|
438
|
-
switch (try beforeMove(battle, player, from, residual, log)) {
|
|
478
|
+
switch (try beforeMove(battle, player, skip, from, residual, log)) {
|
|
439
479
|
.done => return null,
|
|
440
480
|
.skip_can => skip_can = true,
|
|
441
481
|
.skip_pp => skip_pp = true,
|
|
@@ -447,14 +487,15 @@ fn executeMove(
|
|
|
447
487
|
return null;
|
|
448
488
|
}
|
|
449
489
|
|
|
450
|
-
return doMove(battle, player, mslot, auto, residual, log);
|
|
490
|
+
return doMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
451
491
|
}
|
|
452
492
|
|
|
453
|
-
const BeforeMove =
|
|
493
|
+
const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
|
|
454
494
|
|
|
455
495
|
fn beforeMove(
|
|
456
496
|
battle: anytype,
|
|
457
497
|
player: Player,
|
|
498
|
+
skip: bool,
|
|
458
499
|
from: ?Move,
|
|
459
500
|
residual: *bool,
|
|
460
501
|
log: anytype,
|
|
@@ -486,13 +527,15 @@ fn beforeMove(
|
|
|
486
527
|
return .done;
|
|
487
528
|
}
|
|
488
529
|
|
|
489
|
-
if (foe.active.volatiles.Binding) {
|
|
530
|
+
if (skip or foe.active.volatiles.Binding) {
|
|
490
531
|
try log.cant(ident, .Bound);
|
|
491
532
|
return .done;
|
|
492
533
|
}
|
|
493
534
|
|
|
494
|
-
if (
|
|
495
|
-
|
|
535
|
+
if (volatiles.Flinch) {
|
|
536
|
+
// Pokémon Showdown doesn't clear Flinch until its imaginary "residual" phase, meaning
|
|
537
|
+
// Pokémon can sometimes flinch multiple times from the same original hit
|
|
538
|
+
if (!showdown) volatiles.Flinch = false;
|
|
496
539
|
try log.cant(ident, .Flinch);
|
|
497
540
|
return .done;
|
|
498
541
|
}
|
|
@@ -513,13 +556,6 @@ fn beforeMove(
|
|
|
513
556
|
// Pokémon Showdown's disable condition has a single onBeforeMove handler
|
|
514
557
|
if (showdown and try disabled(side, ident, log)) return .done;
|
|
515
558
|
|
|
516
|
-
// Pokémon Showdown checks for Flinch *after* instead of before
|
|
517
|
-
if (showdown and volatiles.Flinch) {
|
|
518
|
-
volatiles.Flinch = false;
|
|
519
|
-
try log.cant(ident, .Flinch);
|
|
520
|
-
return .done;
|
|
521
|
-
}
|
|
522
|
-
|
|
523
559
|
// This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
|
|
524
560
|
// before the side had a selected a move - we simply need to assume this leads to a desync
|
|
525
561
|
if (side.last_selected_move == .None) {
|
|
@@ -537,12 +573,7 @@ fn beforeMove(
|
|
|
537
573
|
} else {
|
|
538
574
|
try log.activate(ident, .Confusion);
|
|
539
575
|
|
|
540
|
-
|
|
541
|
-
!battle.rng.chance(u8, 128, 256)
|
|
542
|
-
else
|
|
543
|
-
battle.rng.next() >= Gen12.percent(50) + 1;
|
|
544
|
-
|
|
545
|
-
if (confused) {
|
|
576
|
+
if (Rolls.confused(battle)) {
|
|
546
577
|
assert(!volatiles.MultiHit);
|
|
547
578
|
if (!volatiles.Rage) volatiles.state = 0;
|
|
548
579
|
volatiles.Bide = false;
|
|
@@ -568,8 +599,12 @@ fn beforeMove(
|
|
|
568
599
|
player
|
|
569
600
|
else
|
|
570
601
|
player.foe();
|
|
602
|
+
|
|
603
|
+
const uncapped = battle.last_damage;
|
|
571
604
|
// Skipping adjustDamage / randomizeDamage / checkHit
|
|
572
605
|
_ = try applyDamage(battle, player, target, .Confusion, log);
|
|
606
|
+
// Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
|
|
607
|
+
if (showdown) battle.last_damage = uncapped;
|
|
573
608
|
|
|
574
609
|
return .done;
|
|
575
610
|
}
|
|
@@ -578,22 +613,15 @@ fn beforeMove(
|
|
|
578
613
|
|
|
579
614
|
if (!showdown and try disabled(side, ident, log)) return .done;
|
|
580
615
|
|
|
581
|
-
if (Status.is(stored.status, .PAR)) {
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
volatiles.Thrashing = false;
|
|
591
|
-
volatiles.Charging = false;
|
|
592
|
-
volatiles.Binding = false;
|
|
593
|
-
// GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
|
|
594
|
-
try log.cant(ident, .Paralysis);
|
|
595
|
-
return .done;
|
|
596
|
-
}
|
|
616
|
+
if (Status.is(stored.status, .PAR) and Rolls.paralyzed(battle)) {
|
|
617
|
+
if (!volatiles.Rage) volatiles.state = 0;
|
|
618
|
+
volatiles.Bide = false;
|
|
619
|
+
volatiles.Thrashing = false;
|
|
620
|
+
volatiles.Charging = false;
|
|
621
|
+
volatiles.Binding = false;
|
|
622
|
+
// GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
|
|
623
|
+
try log.cant(ident, .Paralysis);
|
|
624
|
+
return .done;
|
|
597
625
|
}
|
|
598
626
|
|
|
599
627
|
if (volatiles.Bide) {
|
|
@@ -620,8 +648,9 @@ fn beforeMove(
|
|
|
620
648
|
return .done;
|
|
621
649
|
}
|
|
622
650
|
|
|
651
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
623
652
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
624
|
-
if (foe.stored().hp > 0) try buildRage(battle, player.foe(), log);
|
|
653
|
+
if (foe.stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
|
|
625
654
|
|
|
626
655
|
// For reasons passing understanding, Pokémon Showdown still inflicts residual damage to
|
|
627
656
|
// Bide's user even if the above damage has caused the foe to faint. It's simpler to always
|
|
@@ -642,8 +671,13 @@ fn beforeMove(
|
|
|
642
671
|
try log.start(battle.active(player), .ConfusionSilent);
|
|
643
672
|
}
|
|
644
673
|
try log.move(ident, side.last_selected_move, battle.active(player.foe()), from);
|
|
645
|
-
if (showdown
|
|
646
|
-
|
|
674
|
+
if (showdown) {
|
|
675
|
+
// This shouldn't actually set last_used_move, but Pokémon Showdown sets last
|
|
676
|
+
// used in useMove and doesn't have the notion of skipping canMove semantics
|
|
677
|
+
side.last_used_move = side.last_selected_move;
|
|
678
|
+
if (handleThrashing(battle, active)) {
|
|
679
|
+
try log.start(battle.active(player), .ConfusionSilent);
|
|
680
|
+
}
|
|
647
681
|
}
|
|
648
682
|
return .skip_can;
|
|
649
683
|
}
|
|
@@ -658,7 +692,11 @@ fn beforeMove(
|
|
|
658
692
|
side.last_selected_move,
|
|
659
693
|
);
|
|
660
694
|
if (showdown or battle.last_damage != 0) {
|
|
695
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
661
696
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
697
|
+
if (battle.foe(player).stored().hp > 0 and !sub) {
|
|
698
|
+
try buildRage(battle, player.foe(), log);
|
|
699
|
+
}
|
|
662
700
|
}
|
|
663
701
|
return .done;
|
|
664
702
|
}
|
|
@@ -716,7 +754,7 @@ fn canMove(
|
|
|
716
754
|
Effects.thrashing(battle, player);
|
|
717
755
|
} else if (!showdown and move.effect == .Binding) {
|
|
718
756
|
// Pokémon Showdown handles this after hit/miss checks and damage calculation
|
|
719
|
-
Effects.binding(battle, player);
|
|
757
|
+
Effects.binding(battle, player, false);
|
|
720
758
|
}
|
|
721
759
|
|
|
722
760
|
return true;
|
|
@@ -757,6 +795,7 @@ fn doMove(
|
|
|
757
795
|
battle: anytype,
|
|
758
796
|
player: Player,
|
|
759
797
|
mslot: u4,
|
|
798
|
+
rewrap: bool,
|
|
760
799
|
auto: bool,
|
|
761
800
|
residual: *bool,
|
|
762
801
|
log: anytype,
|
|
@@ -787,12 +826,15 @@ fn doMove(
|
|
|
787
826
|
(@enumToInt(move.type.effectiveness(foe.active.types.type1)) == 0 or
|
|
788
827
|
@enumToInt(move.type.effectiveness(foe.active.types.type2)) == 0);
|
|
789
828
|
if (immune and move.effect != .Binding) break :miss true;
|
|
790
|
-
if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe)
|
|
829
|
+
if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) {
|
|
830
|
+
battle.last_damage = 0;
|
|
831
|
+
break :miss true;
|
|
832
|
+
}
|
|
791
833
|
break :miss move.target != .Self and !moveHit(battle, player, move, &immune, &mist);
|
|
792
834
|
};
|
|
793
835
|
assert(!immune or miss or (showdown and move.effect == .Binding));
|
|
794
836
|
|
|
795
|
-
var late = showdown;
|
|
837
|
+
var late = showdown and move.effect != .Explode;
|
|
796
838
|
const skip = status or immune;
|
|
797
839
|
if ((!showdown or (!skip or counter)) and !miss) blk: {
|
|
798
840
|
if (showdown and move.effect.isMulti()) {
|
|
@@ -845,9 +887,9 @@ fn doMove(
|
|
|
845
887
|
|
|
846
888
|
if (!showdown or !miss) {
|
|
847
889
|
if (move.effect == .MirrorMove) {
|
|
848
|
-
return mirrorMove(battle, player, mslot, auto, residual, log);
|
|
890
|
+
return mirrorMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
849
891
|
} else if (move.effect == .Metronome) {
|
|
850
|
-
return metronome(battle, player, mslot, auto, residual, log);
|
|
892
|
+
return metronome(battle, player, mslot, rewrap, auto, residual, log);
|
|
851
893
|
} else if (move.effect.onEnd()) {
|
|
852
894
|
try onEnd(battle, player, move, log);
|
|
853
895
|
return null;
|
|
@@ -862,7 +904,7 @@ fn doMove(
|
|
|
862
904
|
move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
|
|
863
905
|
if (ohko) {
|
|
864
906
|
try log.immune(foe_ident, .OHKO);
|
|
865
|
-
} else if (immune and !invulnerable) {
|
|
907
|
+
} else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
|
|
866
908
|
try log.immune(foe_ident, .None);
|
|
867
909
|
} else if (mist) {
|
|
868
910
|
if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
|
|
@@ -894,6 +936,9 @@ fn doMove(
|
|
|
894
936
|
hits = side.active.volatiles.attacks;
|
|
895
937
|
}
|
|
896
938
|
|
|
939
|
+
// Pokémon Showdown only builds Rage for Disable/Explosion (hit/miss) when attacking into a sub
|
|
940
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
941
|
+
|
|
897
942
|
var nullified = false;
|
|
898
943
|
var hit: u4 = 0;
|
|
899
944
|
while (hit < hits) {
|
|
@@ -909,7 +954,7 @@ fn doMove(
|
|
|
909
954
|
if (hit == 0 and ohko) try log.ohko();
|
|
910
955
|
hit += 1;
|
|
911
956
|
if (foe.stored().hp == 0) break;
|
|
912
|
-
if (!late) try buildRage(battle, player.foe(), log);
|
|
957
|
+
if (!late and (!sub or move.effect == .Explode)) try buildRage(battle, player.foe(), log);
|
|
913
958
|
// If the substitute breaks during a multi-hit attack, the attack ends
|
|
914
959
|
if (nullified) break;
|
|
915
960
|
}
|
|
@@ -925,11 +970,17 @@ fn doMove(
|
|
|
925
970
|
} else if (showdown) {
|
|
926
971
|
// This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
|
|
927
972
|
if (move.effect == .Binding) {
|
|
928
|
-
Effects.binding(battle, player);
|
|
973
|
+
Effects.binding(battle, player, rewrap);
|
|
929
974
|
if (immune) {
|
|
930
975
|
battle.last_damage = 0;
|
|
976
|
+
assert(foe.stored().hp > 0);
|
|
931
977
|
// Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
|
|
932
|
-
|
|
978
|
+
if (sub) {
|
|
979
|
+
try log.activate(battle.active(player.foe()), .Substitute);
|
|
980
|
+
} else {
|
|
981
|
+
try log.damage(battle.active(player.foe()), foe.stored(), .None);
|
|
982
|
+
try buildRage(battle, player.foe(), log);
|
|
983
|
+
}
|
|
933
984
|
return null;
|
|
934
985
|
}
|
|
935
986
|
}
|
|
@@ -946,8 +997,8 @@ fn doMove(
|
|
|
946
997
|
|
|
947
998
|
if (foe.stored().hp == 0) return null;
|
|
948
999
|
|
|
949
|
-
// Pokémon Showdown builds Rage at the wrong time for non-MultiHit
|
|
950
|
-
if (late and move.effect != .Disable) try buildRage(battle, player.foe(), log);
|
|
1000
|
+
// Pokémon Showdown builds Rage at the wrong time for non-MultiHit move
|
|
1001
|
+
if (late and !sub and move.effect != .Disable) try buildRage(battle, player.foe(), log);
|
|
951
1002
|
|
|
952
1003
|
if (!move.effect.isSpecial()) {
|
|
953
1004
|
// On the cartridge Rage is not considered to be "special" and thus gets executed for a
|
|
@@ -980,8 +1031,7 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
|
|
|
980
1031
|
chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
|
|
981
1032
|
chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
|
|
982
1033
|
|
|
983
|
-
|
|
984
|
-
return std.math.rotl(u8, battle.rng.next(), 3) < chance;
|
|
1034
|
+
return Rolls.criticalHit(battle, chance);
|
|
985
1035
|
}
|
|
986
1036
|
|
|
987
1037
|
fn calcDamage(
|
|
@@ -1085,50 +1135,36 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
|
1085
1135
|
|
|
1086
1136
|
fn randomizeDamage(battle: anytype) void {
|
|
1087
1137
|
if (battle.last_damage <= 1) return;
|
|
1088
|
-
|
|
1089
|
-
const random = if (showdown)
|
|
1090
|
-
battle.rng.range(u8, 217, 256)
|
|
1091
|
-
else loop: {
|
|
1092
|
-
while (true) {
|
|
1093
|
-
const r = std.math.rotr(u8, battle.rng.next(), 1);
|
|
1094
|
-
if (r >= 217) break :loop r;
|
|
1095
|
-
}
|
|
1096
|
-
};
|
|
1097
|
-
|
|
1138
|
+
const random = Rolls.damage(battle);
|
|
1098
1139
|
battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
|
|
1099
1140
|
}
|
|
1100
1141
|
|
|
1101
1142
|
fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
|
|
1102
1143
|
const side = battle.side(player);
|
|
1144
|
+
const foe = battle.foe(player);
|
|
1103
1145
|
|
|
1104
1146
|
if (!try checkHit(battle, player, move, log)) return null;
|
|
1105
1147
|
|
|
1106
1148
|
battle.last_damage = switch (side.last_selected_move) {
|
|
1107
|
-
.SuperFang => @max(
|
|
1149
|
+
.SuperFang => @max(foe.stored().hp / 2, 1),
|
|
1108
1150
|
.SeismicToss, .NightShade => side.stored().level,
|
|
1109
1151
|
.SonicBoom => 20,
|
|
1110
1152
|
.DragonRage => 40,
|
|
1111
1153
|
// GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
|
|
1112
1154
|
.Psywave => power: {
|
|
1113
1155
|
const max = @intCast(u8, @as(u16, side.stored().level) * 3 / 2);
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
// GLITCH: Psywave infinite glitch loop
|
|
1118
|
-
if (max <= 1) return Result.Error;
|
|
1119
|
-
while (true) {
|
|
1120
|
-
const r = battle.rng.next();
|
|
1121
|
-
if (r < max) break :power r;
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1156
|
+
// GLITCH: Psywave infinite glitch loop
|
|
1157
|
+
if (!showdown and max <= 1) return Result.Error;
|
|
1158
|
+
break :power Rolls.psywave(battle, max);
|
|
1124
1159
|
},
|
|
1125
1160
|
else => unreachable,
|
|
1126
1161
|
};
|
|
1127
1162
|
|
|
1128
1163
|
if (battle.last_damage == 0) return if (showdown) null else Result.Error;
|
|
1129
1164
|
|
|
1165
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1130
1166
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
1131
|
-
if (battle.foe(player).stored().hp > 0) try buildRage(battle, player.foe(), log);
|
|
1167
|
+
if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
|
|
1132
1168
|
|
|
1133
1169
|
return null;
|
|
1134
1170
|
}
|
|
@@ -1177,7 +1213,9 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
|
|
|
1177
1213
|
// Pokémon Showdown calls checkHit before Counter
|
|
1178
1214
|
if (!showdown and !try checkHit(battle, player, move, log)) return null;
|
|
1179
1215
|
|
|
1216
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1180
1217
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
1218
|
+
if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
|
|
1181
1219
|
return null;
|
|
1182
1220
|
}
|
|
1183
1221
|
|
|
@@ -1221,6 +1259,7 @@ fn mirrorMove(
|
|
|
1221
1259
|
battle: anytype,
|
|
1222
1260
|
player: Player,
|
|
1223
1261
|
mslot: u4,
|
|
1262
|
+
rewrap: bool,
|
|
1224
1263
|
auto: bool,
|
|
1225
1264
|
residual: *bool,
|
|
1226
1265
|
log: anytype,
|
|
@@ -1233,41 +1272,30 @@ fn mirrorMove(
|
|
|
1233
1272
|
if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
|
|
1234
1273
|
try log.fail(battle.active(player), .None);
|
|
1235
1274
|
return null;
|
|
1275
|
+
} else if (!showdown or foe.last_used_move != .Struggle) {
|
|
1276
|
+
incrementPP(side, mslot);
|
|
1236
1277
|
}
|
|
1237
1278
|
|
|
1238
|
-
incrementPP(side, mslot);
|
|
1239
|
-
|
|
1240
1279
|
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
|
|
1241
|
-
return doMove(battle, player, mslot, auto, residual, log);
|
|
1280
|
+
return doMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
1242
1281
|
}
|
|
1243
1282
|
|
|
1244
1283
|
fn metronome(
|
|
1245
1284
|
battle: anytype,
|
|
1246
1285
|
player: Player,
|
|
1247
1286
|
mslot: u4,
|
|
1287
|
+
rewrap: bool,
|
|
1248
1288
|
auto: bool,
|
|
1249
1289
|
residual: *bool,
|
|
1250
1290
|
log: anytype,
|
|
1251
1291
|
) !?Result {
|
|
1252
1292
|
var side = battle.side(player);
|
|
1253
1293
|
|
|
1254
|
-
side.last_selected_move =
|
|
1255
|
-
const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
|
|
1256
|
-
const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
|
|
1257
|
-
break :blk @intToEnum(Move, r + mod);
|
|
1258
|
-
} else loop: {
|
|
1259
|
-
while (true) {
|
|
1260
|
-
const r = battle.rng.next();
|
|
1261
|
-
if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
|
|
1262
|
-
if (r >= @enumToInt(Move.Struggle)) continue;
|
|
1263
|
-
break :loop @intToEnum(Move, r);
|
|
1264
|
-
}
|
|
1265
|
-
};
|
|
1266
|
-
|
|
1294
|
+
side.last_selected_move = Rolls.metronome(battle);
|
|
1267
1295
|
incrementPP(side, mslot);
|
|
1268
1296
|
|
|
1269
1297
|
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
|
|
1270
|
-
return doMove(battle, player, mslot, auto, residual, log);
|
|
1298
|
+
return doMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
1271
1299
|
}
|
|
1272
1300
|
|
|
1273
1301
|
fn checkHit(battle: anytype, player: Player, move: Move.Data, log: anytype) !bool {
|
|
@@ -1321,23 +1349,24 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
|
|
|
1321
1349
|
}
|
|
1322
1350
|
|
|
1323
1351
|
// GLITCH: Thrash / Petal Dance / Rage get their accuracy overwritten on subsequent hits
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1352
|
+
const state = side.active.volatiles.state;
|
|
1353
|
+
var overwrite = move.effect == .Rage or move.effect == .Thrashing;
|
|
1354
|
+
const overwritten = overwrite and state > 0;
|
|
1355
|
+
assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
|
|
1356
|
+
|
|
1357
|
+
var accuracy = if (overwritten) state else @as(u16, Gen12.percent(move.accuracy));
|
|
1328
1358
|
var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
|
|
1329
1359
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1330
1360
|
boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
|
|
1331
1361
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1332
1362
|
accuracy = @min(255, @max(1, accuracy));
|
|
1333
1363
|
|
|
1334
|
-
|
|
1364
|
+
// Pokémon Showdown only overwrites if the volatile is present
|
|
1365
|
+
if (showdown) overwrite = side.active.volatiles.Rage or side.active.volatiles.Thrashing;
|
|
1366
|
+
if (overwrite) side.active.volatiles.state = accuracy;
|
|
1335
1367
|
|
|
1336
1368
|
// GLITCH: max accuracy is 255 so 1/256 chance of miss
|
|
1337
|
-
break :miss
|
|
1338
|
-
!battle.rng.chance(u8, @intCast(u8, accuracy), 256)
|
|
1339
|
-
else
|
|
1340
|
-
battle.rng.next() >= accuracy;
|
|
1369
|
+
break :miss !Rolls.hit(battle, accuracy);
|
|
1341
1370
|
};
|
|
1342
1371
|
|
|
1343
1372
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1469,9 +1498,6 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
|
|
|
1469
1498
|
damage *= volatiles.toxic;
|
|
1470
1499
|
}
|
|
1471
1500
|
|
|
1472
|
-
// Pokémon Showdown erroneously updates last damage with uncapped Leech Seed damage
|
|
1473
|
-
if (showdown) battle.last_damage = damage;
|
|
1474
|
-
|
|
1475
1501
|
const amount = @min(damage, stored.hp);
|
|
1476
1502
|
stored.hp -= amount;
|
|
1477
1503
|
|
|
@@ -1563,6 +1589,42 @@ fn checkEBC(battle: anytype) bool {
|
|
|
1563
1589
|
return false;
|
|
1564
1590
|
}
|
|
1565
1591
|
|
|
1592
|
+
fn disabled(side: *Side, ident: ID, log: anytype) !bool {
|
|
1593
|
+
if (side.active.volatiles.disabled_move != 0) {
|
|
1594
|
+
// A Pokémon that transforms after being disabled may end up with less move slots
|
|
1595
|
+
const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
|
|
1596
|
+
// side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
|
|
1597
|
+
const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
|
|
1598
|
+
m
|
|
1599
|
+
else
|
|
1600
|
+
side.last_selected_move;
|
|
1601
|
+
if (m != .None and m == last) {
|
|
1602
|
+
side.active.volatiles.Charging = false;
|
|
1603
|
+
try log.disabled(ident, last);
|
|
1604
|
+
return true;
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
return false;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
inline fn buildRage(battle: anytype, who: Player, log: anytype) !void {
|
|
1611
|
+
const side = battle.side(who);
|
|
1612
|
+
if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
|
|
1613
|
+
try Effects.boost(battle, who, Move.get(.Rage), log);
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
|
|
1618
|
+
var volatiles = &active.volatiles;
|
|
1619
|
+
assert(volatiles.Thrashing);
|
|
1620
|
+
if (volatiles.attacks > 0) return false;
|
|
1621
|
+
|
|
1622
|
+
volatiles.Thrashing = false;
|
|
1623
|
+
volatiles.Confusion = true;
|
|
1624
|
+
volatiles.confusion = Rolls.confusionDuration(battle);
|
|
1625
|
+
return true;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1566
1628
|
inline fn onBegin(
|
|
1567
1629
|
battle: anytype,
|
|
1568
1630
|
player: Player,
|
|
@@ -1653,10 +1715,7 @@ pub const Effects = struct {
|
|
|
1653
1715
|
side.active.volatiles.Bide = true;
|
|
1654
1716
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1655
1717
|
side.active.volatiles.state = 0;
|
|
1656
|
-
side.active.volatiles.attacks =
|
|
1657
|
-
battle.rng.range(u4, 2, 4)
|
|
1658
|
-
else
|
|
1659
|
-
(battle.rng.next() & 1) + 2);
|
|
1718
|
+
side.active.volatiles.attacks = Rolls.bideThrashDuration(battle);
|
|
1660
1719
|
|
|
1661
1720
|
try log.start(battle.active(player), .Bide);
|
|
1662
1721
|
}
|
|
@@ -1678,14 +1737,8 @@ pub const Effects = struct {
|
|
|
1678
1737
|
return;
|
|
1679
1738
|
}
|
|
1680
1739
|
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
else
|
|
1684
|
-
battle.rng.next() < 1 + (if (move.effect == .BurnChance1)
|
|
1685
|
-
Gen12.percent(10)
|
|
1686
|
-
else
|
|
1687
|
-
Gen12.percent(30));
|
|
1688
|
-
if (!chance) return;
|
|
1740
|
+
if (foe.active.types.includes(move.type)) return;
|
|
1741
|
+
if (!Rolls.secondaryChance(battle, move.effect == .BurnChance1)) return;
|
|
1689
1742
|
|
|
1690
1743
|
foe_stored.status = Status.init(.BRN);
|
|
1691
1744
|
foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
|
|
@@ -1709,11 +1762,7 @@ pub const Effects = struct {
|
|
|
1709
1762
|
const sub = foe.active.volatiles.Substitute;
|
|
1710
1763
|
|
|
1711
1764
|
if (move.effect == .ConfusionChance) {
|
|
1712
|
-
|
|
1713
|
-
battle.rng.chance(u8, 25, 256)
|
|
1714
|
-
else
|
|
1715
|
-
battle.rng.next() < Gen12.percent(10);
|
|
1716
|
-
if (!chance) return;
|
|
1765
|
+
if (!Rolls.confusionChance(battle)) return;
|
|
1717
1766
|
} else {
|
|
1718
1767
|
if (showdown) {
|
|
1719
1768
|
if (!try checkHit(battle, player, move, log)) {
|
|
@@ -1732,10 +1781,7 @@ pub const Effects = struct {
|
|
|
1732
1781
|
|
|
1733
1782
|
if (foe.active.volatiles.Confusion) return;
|
|
1734
1783
|
foe.active.volatiles.Confusion = true;
|
|
1735
|
-
foe.active.volatiles.confusion =
|
|
1736
|
-
battle.rng.range(u8, 2, 6)
|
|
1737
|
-
else
|
|
1738
|
-
(battle.rng.next() & 3) + 2);
|
|
1784
|
+
foe.active.volatiles.confusion = Rolls.confusionDuration(battle);
|
|
1739
1785
|
|
|
1740
1786
|
try log.start(battle.active(player.foe()), .Confusion);
|
|
1741
1787
|
}
|
|
@@ -1785,11 +1831,8 @@ pub const Effects = struct {
|
|
|
1785
1831
|
return Result.Error;
|
|
1786
1832
|
}
|
|
1787
1833
|
|
|
1788
|
-
volatiles.disabled_move = @intCast(u3,
|
|
1789
|
-
volatiles.disabled_duration =
|
|
1790
|
-
battle.rng.range(u8, 1, 9)
|
|
1791
|
-
else
|
|
1792
|
-
(battle.rng.next() & 7) + 1);
|
|
1834
|
+
volatiles.disabled_move = @intCast(u3, Rolls.moveSlot(battle, &foe.active.moves, n));
|
|
1835
|
+
volatiles.disabled_duration = Rolls.disableDuration(battle);
|
|
1793
1836
|
|
|
1794
1837
|
try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
|
|
1795
1838
|
return null;
|
|
@@ -1801,7 +1844,7 @@ pub const Effects = struct {
|
|
|
1801
1844
|
|
|
1802
1845
|
if (battle.last_damage == 0) {
|
|
1803
1846
|
assert(showdown);
|
|
1804
|
-
return;
|
|
1847
|
+
if (!battle.foe(player).active.volatiles.Substitute) return;
|
|
1805
1848
|
}
|
|
1806
1849
|
|
|
1807
1850
|
const drain = @max(battle.last_damage / 2, 1);
|
|
@@ -1828,15 +1871,7 @@ pub const Effects = struct {
|
|
|
1828
1871
|
var volatiles = &battle.foe(player).active.volatiles;
|
|
1829
1872
|
|
|
1830
1873
|
if (volatiles.Substitute) return;
|
|
1831
|
-
|
|
1832
|
-
const chance = if (showdown)
|
|
1833
|
-
battle.rng.chance(u8, @as(u8, if (move.effect == .FlinchChance1) 26 else 77), 256)
|
|
1834
|
-
else
|
|
1835
|
-
battle.rng.next() < 1 + (if (move.effect == .FlinchChance1)
|
|
1836
|
-
Gen12.percent(10)
|
|
1837
|
-
else
|
|
1838
|
-
Gen12.percent(30));
|
|
1839
|
-
if (!chance) return;
|
|
1874
|
+
if (!Rolls.secondaryChance(battle, move.effect == .FlinchChance1)) return;
|
|
1840
1875
|
|
|
1841
1876
|
volatiles.Flinch = true;
|
|
1842
1877
|
volatiles.Recharging = false;
|
|
@@ -1862,12 +1897,8 @@ pub const Effects = struct {
|
|
|
1862
1897
|
return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
|
|
1863
1898
|
}
|
|
1864
1899
|
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
else
|
|
1868
|
-
battle.rng.next() < 1 + Gen12.percent(10);
|
|
1869
|
-
if (!chance) return;
|
|
1870
|
-
|
|
1900
|
+
if (foe.active.types.includes(move.type)) return;
|
|
1901
|
+
if (!Rolls.secondaryChance(battle, true)) return;
|
|
1871
1902
|
// Freeze Clause Mod
|
|
1872
1903
|
if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
1873
1904
|
|
|
@@ -2004,15 +2035,15 @@ pub const Effects = struct {
|
|
|
2004
2035
|
}
|
|
2005
2036
|
break :has_mimic false;
|
|
2006
2037
|
};
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2038
|
+
if (!has_mimic) {
|
|
2039
|
+
// Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
|
|
2040
|
+
if (!try checkHit(battle, player, move, log)) return;
|
|
2010
2041
|
return try log.fail(battle.active(player.foe()), .None);
|
|
2011
2042
|
}
|
|
2012
2043
|
}
|
|
2013
2044
|
if (!try checkHit(battle, player, move, log)) return;
|
|
2014
2045
|
|
|
2015
|
-
const rslot =
|
|
2046
|
+
const rslot = Rolls.moveSlot(battle, &foe.active.moves, 0);
|
|
2016
2047
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
2017
2048
|
|
|
2018
2049
|
try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
|
|
@@ -2033,7 +2064,8 @@ pub const Effects = struct {
|
|
|
2033
2064
|
assert(!side.active.volatiles.MultiHit);
|
|
2034
2065
|
side.active.volatiles.MultiHit = true;
|
|
2035
2066
|
|
|
2036
|
-
side.active.volatiles.attacks =
|
|
2067
|
+
side.active.volatiles.attacks =
|
|
2068
|
+
if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
|
|
2037
2069
|
}
|
|
2038
2070
|
|
|
2039
2071
|
fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2077,14 +2109,8 @@ pub const Effects = struct {
|
|
|
2077
2109
|
}
|
|
2078
2110
|
|
|
2079
2111
|
// Body Slam can't paralyze a Normal type Pokémon
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
else
|
|
2083
|
-
battle.rng.next() < 1 + (if (move.effect == .ParalyzeChance1)
|
|
2084
|
-
Gen12.percent(10)
|
|
2085
|
-
else
|
|
2086
|
-
Gen12.percent(30));
|
|
2087
|
-
if (!chance) return;
|
|
2112
|
+
if (foe.active.types.includes(move.type)) return;
|
|
2113
|
+
if (!Rolls.secondaryChance(battle, move.effect == .ParalyzeChance1)) return;
|
|
2088
2114
|
|
|
2089
2115
|
foe_stored.status = Status.init(.PAR);
|
|
2090
2116
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
@@ -2127,16 +2153,7 @@ pub const Effects = struct {
|
|
|
2127
2153
|
|
|
2128
2154
|
if (move.effect == .Poison) {
|
|
2129
2155
|
if (!showdown and !try checkHit(battle, player, move, log)) return;
|
|
2130
|
-
} else
|
|
2131
|
-
const chance = if (showdown)
|
|
2132
|
-
battle.rng.chance(u8, @as(u8, if (move.effect == .PoisonChance1) 52 else 103), 256)
|
|
2133
|
-
else
|
|
2134
|
-
battle.rng.next() < 1 + (if (move.effect == .PoisonChance1)
|
|
2135
|
-
Gen12.percent(20)
|
|
2136
|
-
else
|
|
2137
|
-
Gen12.percent(40));
|
|
2138
|
-
if (!chance) return;
|
|
2139
|
-
}
|
|
2156
|
+
} else if (!Rolls.poisonChance(battle, move.effect == .PoisonChance1)) return;
|
|
2140
2157
|
|
|
2141
2158
|
foe_stored.status = Status.init(.PSN);
|
|
2142
2159
|
if (toxic) {
|
|
@@ -2150,10 +2167,8 @@ pub const Effects = struct {
|
|
|
2150
2167
|
|
|
2151
2168
|
fn rage(battle: anytype, player: Player) !void {
|
|
2152
2169
|
var volatiles = &battle.side(player).active.volatiles;
|
|
2153
|
-
if (volatiles.Rage) return;
|
|
2154
|
-
volatiles.Rage = true;
|
|
2155
2170
|
assert(!volatiles.Bide);
|
|
2156
|
-
volatiles.
|
|
2171
|
+
volatiles.Rage = true;
|
|
2157
2172
|
}
|
|
2158
2173
|
|
|
2159
2174
|
fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
|
|
@@ -2205,16 +2220,7 @@ pub const Effects = struct {
|
|
|
2205
2220
|
}
|
|
2206
2221
|
foe.active.volatiles.Recharging = false;
|
|
2207
2222
|
|
|
2208
|
-
|
|
2209
|
-
battle.rng.range(u8, 1, 8)
|
|
2210
|
-
else loop: {
|
|
2211
|
-
while (true) {
|
|
2212
|
-
const r = battle.rng.next() & 7;
|
|
2213
|
-
if (r != 0) break :loop r;
|
|
2214
|
-
}
|
|
2215
|
-
});
|
|
2216
|
-
|
|
2217
|
-
foe_stored.status = Status.slp(duration);
|
|
2223
|
+
foe_stored.status = Status.slp(Rolls.sleepDuration(battle));
|
|
2218
2224
|
try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
|
|
2219
2225
|
}
|
|
2220
2226
|
|
|
@@ -2267,11 +2273,7 @@ pub const Effects = struct {
|
|
|
2267
2273
|
assert(!volatiles.Bide);
|
|
2268
2274
|
|
|
2269
2275
|
volatiles.Thrashing = true;
|
|
2270
|
-
volatiles.
|
|
2271
|
-
volatiles.attacks = @intCast(u3, if (showdown)
|
|
2272
|
-
battle.rng.range(u8, 2, 4)
|
|
2273
|
-
else
|
|
2274
|
-
(battle.rng.next() & 1) + 2);
|
|
2276
|
+
volatiles.attacks = Rolls.bideThrashDuration(battle);
|
|
2275
2277
|
}
|
|
2276
2278
|
|
|
2277
2279
|
fn transform(battle: anytype, player: Player, log: anytype) !void {
|
|
@@ -2303,7 +2305,7 @@ pub const Effects = struct {
|
|
|
2303
2305
|
try log.transform(battle.active(player), foe_ident);
|
|
2304
2306
|
}
|
|
2305
2307
|
|
|
2306
|
-
fn binding(battle: anytype, player: Player) void {
|
|
2308
|
+
fn binding(battle: anytype, player: Player, rewrap: bool) void {
|
|
2307
2309
|
var side = battle.side(player);
|
|
2308
2310
|
var foe = battle.foe(player);
|
|
2309
2311
|
|
|
@@ -2311,9 +2313,11 @@ pub const Effects = struct {
|
|
|
2311
2313
|
side.active.volatiles.Binding = true;
|
|
2312
2314
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2313
2315
|
// (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
|
|
2314
|
-
if (!showdown)
|
|
2316
|
+
if (!showdown) {
|
|
2317
|
+
foe.active.volatiles.Recharging = false;
|
|
2318
|
+
} else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
|
|
2315
2319
|
|
|
2316
|
-
side.active.volatiles.attacks = distribution(battle) - 1;
|
|
2320
|
+
side.active.volatiles.attacks = Rolls.distribution(battle) - 1;
|
|
2317
2321
|
}
|
|
2318
2322
|
|
|
2319
2323
|
fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2340,7 +2344,7 @@ pub const Effects = struct {
|
|
|
2340
2344
|
return try log.fail(ident, .None);
|
|
2341
2345
|
}
|
|
2342
2346
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2343
|
-
const stat = unmodifiedStats(battle,
|
|
2347
|
+
const stat = unmodifiedStats(battle, side).atk;
|
|
2344
2348
|
stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2345
2349
|
try log.boost(ident, reason, n);
|
|
2346
2350
|
// Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
|
|
@@ -2360,7 +2364,7 @@ pub const Effects = struct {
|
|
|
2360
2364
|
return try log.fail(ident, .None);
|
|
2361
2365
|
}
|
|
2362
2366
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2363
|
-
const stat = unmodifiedStats(battle,
|
|
2367
|
+
const stat = unmodifiedStats(battle, side).def;
|
|
2364
2368
|
stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2365
2369
|
try log.boost(ident, .Defense, n);
|
|
2366
2370
|
},
|
|
@@ -2377,7 +2381,7 @@ pub const Effects = struct {
|
|
|
2377
2381
|
return try log.fail(ident, .None);
|
|
2378
2382
|
}
|
|
2379
2383
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2380
|
-
const stat = unmodifiedStats(battle,
|
|
2384
|
+
const stat = unmodifiedStats(battle, side).spe;
|
|
2381
2385
|
stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2382
2386
|
try log.boost(ident, .Speed, 2);
|
|
2383
2387
|
},
|
|
@@ -2397,7 +2401,7 @@ pub const Effects = struct {
|
|
|
2397
2401
|
return try log.fail(ident, .None);
|
|
2398
2402
|
}
|
|
2399
2403
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2400
|
-
const stat = unmodifiedStats(battle,
|
|
2404
|
+
const stat = unmodifiedStats(battle, side).spc;
|
|
2401
2405
|
stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2402
2406
|
try log.boost(ident, .SpecialAttack, n);
|
|
2403
2407
|
try log.boost(ident, .SpecialDefense, n);
|
|
@@ -2424,11 +2428,7 @@ pub const Effects = struct {
|
|
|
2424
2428
|
}
|
|
2425
2429
|
|
|
2426
2430
|
if (move.effect.isStatDownChance()) {
|
|
2427
|
-
|
|
2428
|
-
battle.rng.chance(u8, 85, 256)
|
|
2429
|
-
else
|
|
2430
|
-
battle.rng.next() < Gen12.percent(33) + 1;
|
|
2431
|
-
if (!chance or foe.active.volatiles.Invulnerable) return;
|
|
2431
|
+
if (!Rolls.unboost(battle) or foe.active.volatiles.Invulnerable) return;
|
|
2432
2432
|
} else if (!showdown and !try checkHit(battle, player, move, log)) {
|
|
2433
2433
|
return; // checkHit already checks for Invulnerable
|
|
2434
2434
|
}
|
|
@@ -2450,7 +2450,7 @@ pub const Effects = struct {
|
|
|
2450
2450
|
return try log.fail(foe_ident, .None);
|
|
2451
2451
|
}
|
|
2452
2452
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2453
|
-
const stat = unmodifiedStats(battle,
|
|
2453
|
+
const stat = unmodifiedStats(battle, foe).atk;
|
|
2454
2454
|
stats.atk = @max(1, stat * mod[0] / mod[1]);
|
|
2455
2455
|
try log.boost(foe_ident, .Attack, -1);
|
|
2456
2456
|
},
|
|
@@ -2468,7 +2468,7 @@ pub const Effects = struct {
|
|
|
2468
2468
|
return try log.fail(foe_ident, .None);
|
|
2469
2469
|
}
|
|
2470
2470
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2471
|
-
const stat = unmodifiedStats(battle,
|
|
2471
|
+
const stat = unmodifiedStats(battle, foe).def;
|
|
2472
2472
|
stats.def = @max(1, stat * mod[0] / mod[1]);
|
|
2473
2473
|
try log.boost(foe_ident, .Defense, -@as(i8, n));
|
|
2474
2474
|
},
|
|
@@ -2485,7 +2485,7 @@ pub const Effects = struct {
|
|
|
2485
2485
|
return try log.fail(foe_ident, .None);
|
|
2486
2486
|
}
|
|
2487
2487
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2488
|
-
const stat = unmodifiedStats(battle,
|
|
2488
|
+
const stat = unmodifiedStats(battle, foe).spe;
|
|
2489
2489
|
stats.spe = @max(1, stat * mod[0] / mod[1]);
|
|
2490
2490
|
try log.boost(foe_ident, .Speed, -1);
|
|
2491
2491
|
assert(boosts.spe >= -6);
|
|
@@ -2505,7 +2505,7 @@ pub const Effects = struct {
|
|
|
2505
2505
|
return try log.fail(foe_ident, .None);
|
|
2506
2506
|
}
|
|
2507
2507
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2508
|
-
const stat = unmodifiedStats(battle,
|
|
2508
|
+
const stat = unmodifiedStats(battle, foe).spc;
|
|
2509
2509
|
stats.spc = @max(1, stat * mod[0] / mod[1]);
|
|
2510
2510
|
try log.boost(foe_ident, .SpecialAttack, -1);
|
|
2511
2511
|
try log.boost(foe_ident, .SpecialDefense, -1);
|
|
@@ -2524,8 +2524,7 @@ pub const Effects = struct {
|
|
|
2524
2524
|
}
|
|
2525
2525
|
};
|
|
2526
2526
|
|
|
2527
|
-
fn unmodifiedStats(battle: anytype,
|
|
2528
|
-
const side = battle.side(who);
|
|
2527
|
+
fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
|
|
2529
2528
|
if (!side.active.volatiles.Transform) return &side.stored().stats;
|
|
2530
2529
|
const id = ID.from(side.active.volatiles.transform);
|
|
2531
2530
|
return &battle.side(id.player).pokemon[id.id - 1].stats;
|
|
@@ -2591,80 +2590,140 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
|
|
|
2591
2590
|
}
|
|
2592
2591
|
}
|
|
2593
2592
|
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2593
|
+
pub const Rolls = struct {
|
|
2594
|
+
inline fn speedTie(battle: anytype) bool {
|
|
2595
|
+
if (showdown) return battle.rng.range(u8, 0, 2) == 0;
|
|
2596
|
+
return battle.rng.next() < Gen12.percent(50) + 1;
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
inline fn criticalHit(battle: anytype, chance: u16) bool {
|
|
2600
|
+
if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
|
|
2601
|
+
return std.math.rotl(u8, battle.rng.next(), 3) < chance;
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2604
|
+
inline fn damage(battle: anytype) u8 {
|
|
2605
|
+
if (showdown) return battle.rng.range(u8, 217, 256);
|
|
2606
|
+
while (true) {
|
|
2607
|
+
const r = std.math.rotr(u8, battle.rng.next(), 1);
|
|
2608
|
+
if (r >= 217) return r;
|
|
2607
2609
|
}
|
|
2608
2610
|
}
|
|
2609
|
-
return false;
|
|
2610
|
-
}
|
|
2611
2611
|
|
|
2612
|
-
inline fn
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
try Effects.boost(battle, who, Move.get(.Rage), log);
|
|
2612
|
+
inline fn hit(battle: anytype, accuracy: u16) bool {
|
|
2613
|
+
if (showdown) return battle.rng.chance(u8, @intCast(u8, accuracy), 256);
|
|
2614
|
+
return battle.rng.next() < accuracy;
|
|
2616
2615
|
}
|
|
2617
|
-
}
|
|
2618
2616
|
|
|
2619
|
-
fn
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2617
|
+
inline fn confusionDuration(battle: anytype) u3 {
|
|
2618
|
+
if (showdown) return @intCast(u3, battle.rng.range(u8, 2, 6));
|
|
2619
|
+
return @intCast(u3, (battle.rng.next() & 3) + 2);
|
|
2620
|
+
}
|
|
2623
2621
|
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
else
|
|
2629
|
-
(battle.rng.next() & 3) + 2);
|
|
2622
|
+
inline fn confused(battle: anytype) bool {
|
|
2623
|
+
if (showdown) return !battle.rng.chance(u8, 128, 256);
|
|
2624
|
+
return battle.rng.next() >= Gen12.percent(50) + 1;
|
|
2625
|
+
}
|
|
2630
2626
|
|
|
2631
|
-
|
|
2632
|
-
|
|
2627
|
+
inline fn paralyzed(battle: anytype) bool {
|
|
2628
|
+
if (showdown) return battle.rng.chance(u8, 63, 256);
|
|
2629
|
+
return battle.rng.next() < Gen12.percent(25);
|
|
2630
|
+
}
|
|
2633
2631
|
|
|
2634
|
-
|
|
2632
|
+
inline fn confusionChance(battle: anytype) bool {
|
|
2633
|
+
if (showdown) return battle.rng.chance(u8, 25, 256);
|
|
2634
|
+
return battle.rng.next() < Gen12.percent(10);
|
|
2635
|
+
}
|
|
2635
2636
|
|
|
2636
|
-
fn
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
}
|
|
2637
|
+
inline fn secondaryChance(battle: anytype, low: bool) bool {
|
|
2638
|
+
if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 26 else 77), 256);
|
|
2639
|
+
return battle.rng.next() < 1 + (if (low) Gen12.percent(10) else Gen12.percent(30));
|
|
2640
|
+
}
|
|
2641
2641
|
|
|
2642
|
-
fn
|
|
2643
|
-
|
|
2644
|
-
if (
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2642
|
+
inline fn poisonChance(battle: anytype, low: bool) bool {
|
|
2643
|
+
if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 52 else 103), 256);
|
|
2644
|
+
return battle.rng.next() < 1 + (if (low) Gen12.percent(20) else Gen12.percent(40));
|
|
2645
|
+
}
|
|
2646
|
+
|
|
2647
|
+
inline fn sleepDuration(battle: anytype) u3 {
|
|
2648
|
+
if (showdown) return @intCast(u3, battle.rng.range(u8, 1, 8));
|
|
2649
|
+
while (true) {
|
|
2650
|
+
const r = battle.rng.next() & 7;
|
|
2651
|
+
if (r != 0) return @intCast(u3, r);
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
inline fn bideThrashDuration(battle: anytype) u3 {
|
|
2656
|
+
if (showdown) return @intCast(u3, battle.rng.range(u4, 2, 4));
|
|
2657
|
+
return @intCast(u3, (battle.rng.next() & 1) + 2);
|
|
2658
|
+
}
|
|
2659
|
+
|
|
2660
|
+
fn moveSlot(battle: anytype, moves: []MoveSlot, check_pp: u4) u4 {
|
|
2661
|
+
if (showdown) {
|
|
2662
|
+
if (check_pp == 0) {
|
|
2663
|
+
var i: usize = moves.len;
|
|
2664
|
+
while (i > 0) {
|
|
2665
|
+
i -= 1;
|
|
2666
|
+
if (moves[i].id != .None) {
|
|
2667
|
+
return battle.rng.range(u4, 0, @intCast(u4, i + 1)) + 1;
|
|
2668
|
+
}
|
|
2657
2669
|
}
|
|
2670
|
+
} else {
|
|
2671
|
+
var r = battle.rng.range(u4, 0, @intCast(u4, check_pp)) + 1;
|
|
2672
|
+
var i: usize = 0;
|
|
2673
|
+
while (i < moves.len and r > 0) : (i += 1) {
|
|
2674
|
+
if (moves[i].pp > 0) {
|
|
2675
|
+
r -= 1;
|
|
2676
|
+
if (r == 0) break;
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
return @intCast(u4, i + 1);
|
|
2658
2680
|
}
|
|
2659
|
-
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2683
|
+
while (true) {
|
|
2684
|
+
const r = @intCast(u4, battle.rng.next() & 3);
|
|
2685
|
+
if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
|
|
2660
2686
|
}
|
|
2661
2687
|
}
|
|
2662
2688
|
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2689
|
+
inline fn disableDuration(battle: anytype) u4 {
|
|
2690
|
+
if (showdown) return @intCast(u4, battle.rng.range(u8, 1, 9));
|
|
2691
|
+
return @intCast(u4, (battle.rng.next() & 7) + 1);
|
|
2666
2692
|
}
|
|
2667
|
-
|
|
2693
|
+
|
|
2694
|
+
inline fn metronome(battle: anytype) Move {
|
|
2695
|
+
if (showdown) {
|
|
2696
|
+
const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
|
|
2697
|
+
const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
|
|
2698
|
+
return @intToEnum(Move, r + mod);
|
|
2699
|
+
}
|
|
2700
|
+
while (true) {
|
|
2701
|
+
const r = battle.rng.next();
|
|
2702
|
+
if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
|
|
2703
|
+
if (r >= @enumToInt(Move.Struggle)) continue;
|
|
2704
|
+
return @intToEnum(Move, r);
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
|
|
2708
|
+
inline fn psywave(battle: anytype, max: u8) u8 {
|
|
2709
|
+
if (showdown) return battle.rng.range(u8, 0, max);
|
|
2710
|
+
while (true) {
|
|
2711
|
+
const r = battle.rng.next();
|
|
2712
|
+
if (r < max) return r;
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
inline fn distribution(battle: anytype) u3 {
|
|
2717
|
+
if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
|
|
2718
|
+
const r = (battle.rng.next() & 3);
|
|
2719
|
+
return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
inline fn unboost(battle: anytype) bool {
|
|
2723
|
+
if (showdown) return battle.rng.chance(u8, 85, 256);
|
|
2724
|
+
return battle.rng.next() < Gen12.percent(33) + 1;
|
|
2725
|
+
}
|
|
2726
|
+
};
|
|
2668
2727
|
|
|
2669
2728
|
test "RNG agreement" {
|
|
2670
2729
|
if (!showdown) return;
|
|
@@ -2764,7 +2823,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
2764
2823
|
while (slot <= 4) : (slot += 1) {
|
|
2765
2824
|
const m = active.moves[slot - 1];
|
|
2766
2825
|
if (m.id == .None) break;
|
|
2767
|
-
if (m.id == side.last_selected_move) {
|
|
2826
|
+
if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
|
|
2768
2827
|
// Pokémon Showdown displays Struggle if limited to Bide but unable to pick
|
|
2769
2828
|
const struggle =
|
|
2770
2829
|
m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);
|