@pkmn/engine 0.1.0-dev.c4dec0f7 → 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 +24 -34
- package/package.json +40 -29
- package/src/bin/install-pkmn-engine +60 -33
- package/src/bin/pkmn-debug +62 -0
- package/src/lib/binding/node.zig +5 -5
- package/src/lib/binding/wasm.zig +3 -3
- package/src/lib/common/debug.zig +1 -0
- package/src/lib/common/options.zig +3 -3
- package/src/lib/common/protocol.zig +38 -38
- package/src/lib/gen1/README.md +65 -39
- package/src/lib/gen1/data.zig +6 -6
- package/src/lib/gen1/helpers.zig +10 -7
- package/src/lib/gen1/mechanics.zig +384 -290
- 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;
|
|
@@ -347,8 +375,18 @@ fn doTurn(
|
|
|
347
375
|
if (try checkFaint(battle, player, log)) |r| return r;
|
|
348
376
|
} else if (foe_choice.type == .Pass) return null;
|
|
349
377
|
|
|
378
|
+
residual = true;
|
|
350
379
|
replace = battle.side(foe_player).stored().hp == 0;
|
|
351
|
-
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;
|
|
352
390
|
if (!replace) {
|
|
353
391
|
if (foe_choice.type != .Switch) {
|
|
354
392
|
if (try checkFaint(battle, player, log)) |r| return r;
|
|
@@ -371,6 +409,8 @@ fn executeMove(
|
|
|
371
409
|
battle: anytype,
|
|
372
410
|
player: Player,
|
|
373
411
|
choice: Choice,
|
|
412
|
+
rewrap: bool,
|
|
413
|
+
skip: bool,
|
|
374
414
|
from: ?Move,
|
|
375
415
|
residual: *bool,
|
|
376
416
|
log: anytype,
|
|
@@ -382,9 +422,10 @@ fn executeMove(
|
|
|
382
422
|
return null;
|
|
383
423
|
}
|
|
384
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
|
|
385
428
|
if (side.last_selected_move == .SKIP_TURN) {
|
|
386
|
-
// Pokémon Showdown overwrites the SKIP_TURN sentinel with its botched move select,
|
|
387
|
-
// Binding instead gets handled in beforeMove after sleep and freeze
|
|
388
429
|
assert(!showdown);
|
|
389
430
|
if (battle.foe(player).active.volatiles.Binding) {
|
|
390
431
|
try log.cant(battle.active(player), .Bound);
|
|
@@ -434,7 +475,7 @@ fn executeMove(
|
|
|
434
475
|
|
|
435
476
|
var skip_can = false;
|
|
436
477
|
var skip_pp = false;
|
|
437
|
-
switch (try beforeMove(battle, player, from, log)) {
|
|
478
|
+
switch (try beforeMove(battle, player, skip, from, residual, log)) {
|
|
438
479
|
.done => return null,
|
|
439
480
|
.skip_can => skip_can = true,
|
|
440
481
|
.skip_pp => skip_pp = true,
|
|
@@ -446,12 +487,19 @@ fn executeMove(
|
|
|
446
487
|
return null;
|
|
447
488
|
}
|
|
448
489
|
|
|
449
|
-
return doMove(battle, player, mslot, auto, residual, log);
|
|
490
|
+
return doMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
450
491
|
}
|
|
451
492
|
|
|
452
|
-
const BeforeMove =
|
|
493
|
+
const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
|
|
453
494
|
|
|
454
|
-
fn beforeMove(
|
|
495
|
+
fn beforeMove(
|
|
496
|
+
battle: anytype,
|
|
497
|
+
player: Player,
|
|
498
|
+
skip: bool,
|
|
499
|
+
from: ?Move,
|
|
500
|
+
residual: *bool,
|
|
501
|
+
log: anytype,
|
|
502
|
+
) !BeforeMove {
|
|
455
503
|
var side = battle.side(player);
|
|
456
504
|
const foe = battle.foe(player);
|
|
457
505
|
var active = &side.active;
|
|
@@ -479,13 +527,15 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
|
|
|
479
527
|
return .done;
|
|
480
528
|
}
|
|
481
529
|
|
|
482
|
-
if (foe.active.volatiles.Binding) {
|
|
530
|
+
if (skip or foe.active.volatiles.Binding) {
|
|
483
531
|
try log.cant(ident, .Bound);
|
|
484
532
|
return .done;
|
|
485
533
|
}
|
|
486
534
|
|
|
487
|
-
if (
|
|
488
|
-
|
|
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;
|
|
489
539
|
try log.cant(ident, .Flinch);
|
|
490
540
|
return .done;
|
|
491
541
|
}
|
|
@@ -506,13 +556,6 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
|
|
|
506
556
|
// Pokémon Showdown's disable condition has a single onBeforeMove handler
|
|
507
557
|
if (showdown and try disabled(side, ident, log)) return .done;
|
|
508
558
|
|
|
509
|
-
// Pokémon Showdown checks for Flinch *after* instead of before
|
|
510
|
-
if (showdown and volatiles.Flinch) {
|
|
511
|
-
volatiles.Flinch = false;
|
|
512
|
-
try log.cant(ident, .Flinch);
|
|
513
|
-
return .done;
|
|
514
|
-
}
|
|
515
|
-
|
|
516
559
|
// This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
|
|
517
560
|
// before the side had a selected a move - we simply need to assume this leads to a desync
|
|
518
561
|
if (side.last_selected_move == .None) {
|
|
@@ -530,14 +573,9 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
|
|
|
530
573
|
} else {
|
|
531
574
|
try log.activate(ident, .Confusion);
|
|
532
575
|
|
|
533
|
-
|
|
534
|
-
!battle.rng.chance(u8, 128, 256)
|
|
535
|
-
else
|
|
536
|
-
battle.rng.next() >= Gen12.percent(50) + 1;
|
|
537
|
-
|
|
538
|
-
if (confused) {
|
|
576
|
+
if (Rolls.confused(battle)) {
|
|
539
577
|
assert(!volatiles.MultiHit);
|
|
540
|
-
volatiles.state = 0;
|
|
578
|
+
if (!volatiles.Rage) volatiles.state = 0;
|
|
541
579
|
volatiles.Bide = false;
|
|
542
580
|
volatiles.Thrashing = false;
|
|
543
581
|
volatiles.MultiHit = false;
|
|
@@ -561,8 +599,12 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
|
|
|
561
599
|
player
|
|
562
600
|
else
|
|
563
601
|
player.foe();
|
|
602
|
+
|
|
603
|
+
const uncapped = battle.last_damage;
|
|
564
604
|
// Skipping adjustDamage / randomizeDamage / checkHit
|
|
565
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;
|
|
566
608
|
|
|
567
609
|
return .done;
|
|
568
610
|
}
|
|
@@ -571,22 +613,15 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
|
|
|
571
613
|
|
|
572
614
|
if (!showdown and try disabled(side, ident, log)) return .done;
|
|
573
615
|
|
|
574
|
-
if (Status.is(stored.status, .PAR)) {
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
volatiles.Thrashing = false;
|
|
584
|
-
volatiles.Charging = false;
|
|
585
|
-
volatiles.Binding = false;
|
|
586
|
-
// GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
|
|
587
|
-
try log.cant(ident, .Paralysis);
|
|
588
|
-
return .done;
|
|
589
|
-
}
|
|
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;
|
|
590
625
|
}
|
|
591
626
|
|
|
592
627
|
if (volatiles.Bide) {
|
|
@@ -613,8 +648,17 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
|
|
|
613
648
|
return .done;
|
|
614
649
|
}
|
|
615
650
|
|
|
651
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
616
652
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
617
|
-
try buildRage(battle, player.foe(), log);
|
|
653
|
+
if (foe.stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
|
|
654
|
+
|
|
655
|
+
// For reasons passing understanding, Pokémon Showdown still inflicts residual damage to
|
|
656
|
+
// Bide's user even if the above damage has caused the foe to faint. It's simpler to always
|
|
657
|
+
// run residual here regardless of whether the foe fainted and opt-out of the default flow
|
|
658
|
+
if (showdown) {
|
|
659
|
+
residual.* = false;
|
|
660
|
+
try handleResidual(battle, player, log);
|
|
661
|
+
}
|
|
618
662
|
|
|
619
663
|
return .done;
|
|
620
664
|
}
|
|
@@ -627,8 +671,13 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
|
|
|
627
671
|
try log.start(battle.active(player), .ConfusionSilent);
|
|
628
672
|
}
|
|
629
673
|
try log.move(ident, side.last_selected_move, battle.active(player.foe()), from);
|
|
630
|
-
if (showdown
|
|
631
|
-
|
|
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
|
+
}
|
|
632
681
|
}
|
|
633
682
|
return .skip_can;
|
|
634
683
|
}
|
|
@@ -643,7 +692,11 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
|
|
|
643
692
|
side.last_selected_move,
|
|
644
693
|
);
|
|
645
694
|
if (showdown or battle.last_damage != 0) {
|
|
695
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
646
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
|
+
}
|
|
647
700
|
}
|
|
648
701
|
return .done;
|
|
649
702
|
}
|
|
@@ -701,7 +754,7 @@ fn canMove(
|
|
|
701
754
|
Effects.thrashing(battle, player);
|
|
702
755
|
} else if (!showdown and move.effect == .Binding) {
|
|
703
756
|
// Pokémon Showdown handles this after hit/miss checks and damage calculation
|
|
704
|
-
Effects.binding(battle, player);
|
|
757
|
+
Effects.binding(battle, player, false);
|
|
705
758
|
}
|
|
706
759
|
|
|
707
760
|
return true;
|
|
@@ -742,6 +795,7 @@ fn doMove(
|
|
|
742
795
|
battle: anytype,
|
|
743
796
|
player: Player,
|
|
744
797
|
mslot: u4,
|
|
798
|
+
rewrap: bool,
|
|
745
799
|
auto: bool,
|
|
746
800
|
residual: *bool,
|
|
747
801
|
log: anytype,
|
|
@@ -772,12 +826,15 @@ fn doMove(
|
|
|
772
826
|
(@enumToInt(move.type.effectiveness(foe.active.types.type1)) == 0 or
|
|
773
827
|
@enumToInt(move.type.effectiveness(foe.active.types.type2)) == 0);
|
|
774
828
|
if (immune and move.effect != .Binding) break :miss true;
|
|
775
|
-
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
|
+
}
|
|
776
833
|
break :miss move.target != .Self and !moveHit(battle, player, move, &immune, &mist);
|
|
777
834
|
};
|
|
778
835
|
assert(!immune or miss or (showdown and move.effect == .Binding));
|
|
779
836
|
|
|
780
|
-
var late = showdown;
|
|
837
|
+
var late = showdown and move.effect != .Explode;
|
|
781
838
|
const skip = status or immune;
|
|
782
839
|
if ((!showdown or (!skip or counter)) and !miss) blk: {
|
|
783
840
|
if (showdown and move.effect.isMulti()) {
|
|
@@ -830,9 +887,9 @@ fn doMove(
|
|
|
830
887
|
|
|
831
888
|
if (!showdown or !miss) {
|
|
832
889
|
if (move.effect == .MirrorMove) {
|
|
833
|
-
return mirrorMove(battle, player, mslot, auto, residual, log);
|
|
890
|
+
return mirrorMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
834
891
|
} else if (move.effect == .Metronome) {
|
|
835
|
-
return metronome(battle, player, mslot, auto, residual, log);
|
|
892
|
+
return metronome(battle, player, mslot, rewrap, auto, residual, log);
|
|
836
893
|
} else if (move.effect.onEnd()) {
|
|
837
894
|
try onEnd(battle, player, move, log);
|
|
838
895
|
return null;
|
|
@@ -847,7 +904,7 @@ fn doMove(
|
|
|
847
904
|
move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
|
|
848
905
|
if (ohko) {
|
|
849
906
|
try log.immune(foe_ident, .OHKO);
|
|
850
|
-
} else if (immune and !invulnerable) {
|
|
907
|
+
} else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
|
|
851
908
|
try log.immune(foe_ident, .None);
|
|
852
909
|
} else if (mist) {
|
|
853
910
|
if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
|
|
@@ -879,6 +936,9 @@ fn doMove(
|
|
|
879
936
|
hits = side.active.volatiles.attacks;
|
|
880
937
|
}
|
|
881
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
|
+
|
|
882
942
|
var nullified = false;
|
|
883
943
|
var hit: u4 = 0;
|
|
884
944
|
while (hit < hits) {
|
|
@@ -894,7 +954,7 @@ fn doMove(
|
|
|
894
954
|
if (hit == 0 and ohko) try log.ohko();
|
|
895
955
|
hit += 1;
|
|
896
956
|
if (foe.stored().hp == 0) break;
|
|
897
|
-
if (!late) try buildRage(battle, player.foe(), log);
|
|
957
|
+
if (!late and (!sub or move.effect == .Explode)) try buildRage(battle, player.foe(), log);
|
|
898
958
|
// If the substitute breaks during a multi-hit attack, the attack ends
|
|
899
959
|
if (nullified) break;
|
|
900
960
|
}
|
|
@@ -910,11 +970,17 @@ fn doMove(
|
|
|
910
970
|
} else if (showdown) {
|
|
911
971
|
// This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
|
|
912
972
|
if (move.effect == .Binding) {
|
|
913
|
-
Effects.binding(battle, player);
|
|
973
|
+
Effects.binding(battle, player, rewrap);
|
|
914
974
|
if (immune) {
|
|
915
975
|
battle.last_damage = 0;
|
|
976
|
+
assert(foe.stored().hp > 0);
|
|
916
977
|
// Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
|
|
917
|
-
|
|
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
|
+
}
|
|
918
984
|
return null;
|
|
919
985
|
}
|
|
920
986
|
}
|
|
@@ -931,8 +997,8 @@ fn doMove(
|
|
|
931
997
|
|
|
932
998
|
if (foe.stored().hp == 0) return null;
|
|
933
999
|
|
|
934
|
-
// Pokémon Showdown builds Rage at the wrong time for non-MultiHit
|
|
935
|
-
if (late) 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);
|
|
936
1002
|
|
|
937
1003
|
if (!move.effect.isSpecial()) {
|
|
938
1004
|
// On the cartridge Rage is not considered to be "special" and thus gets executed for a
|
|
@@ -945,7 +1011,9 @@ fn doMove(
|
|
|
945
1011
|
if (move.effect == .Twineedle) {
|
|
946
1012
|
if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), log);
|
|
947
1013
|
} else if (move.effect == .Disable) {
|
|
948
|
-
|
|
1014
|
+
const result = try Effects.disable(battle, player, move, log);
|
|
1015
|
+
if (showdown) try buildRage(battle, player.foe(), log);
|
|
1016
|
+
return result;
|
|
949
1017
|
} else {
|
|
950
1018
|
try moveEffect(battle, player, move, log);
|
|
951
1019
|
}
|
|
@@ -963,8 +1031,7 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
|
|
|
963
1031
|
chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
|
|
964
1032
|
chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
|
|
965
1033
|
|
|
966
|
-
|
|
967
|
-
return std.math.rotl(u8, battle.rng.next(), 3) < chance;
|
|
1034
|
+
return Rolls.criticalHit(battle, chance);
|
|
968
1035
|
}
|
|
969
1036
|
|
|
970
1037
|
fn calcDamage(
|
|
@@ -1068,50 +1135,36 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
|
1068
1135
|
|
|
1069
1136
|
fn randomizeDamage(battle: anytype) void {
|
|
1070
1137
|
if (battle.last_damage <= 1) return;
|
|
1071
|
-
|
|
1072
|
-
const random = if (showdown)
|
|
1073
|
-
battle.rng.range(u8, 217, 256)
|
|
1074
|
-
else loop: {
|
|
1075
|
-
while (true) {
|
|
1076
|
-
const r = std.math.rotr(u8, battle.rng.next(), 1);
|
|
1077
|
-
if (r >= 217) break :loop r;
|
|
1078
|
-
}
|
|
1079
|
-
};
|
|
1080
|
-
|
|
1138
|
+
const random = Rolls.damage(battle);
|
|
1081
1139
|
battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
|
|
1082
1140
|
}
|
|
1083
1141
|
|
|
1084
1142
|
fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
|
|
1085
1143
|
const side = battle.side(player);
|
|
1144
|
+
const foe = battle.foe(player);
|
|
1086
1145
|
|
|
1087
1146
|
if (!try checkHit(battle, player, move, log)) return null;
|
|
1088
1147
|
|
|
1089
1148
|
battle.last_damage = switch (side.last_selected_move) {
|
|
1090
|
-
.SuperFang => @max(
|
|
1149
|
+
.SuperFang => @max(foe.stored().hp / 2, 1),
|
|
1091
1150
|
.SeismicToss, .NightShade => side.stored().level,
|
|
1092
1151
|
.SonicBoom => 20,
|
|
1093
1152
|
.DragonRage => 40,
|
|
1094
1153
|
// GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
|
|
1095
1154
|
.Psywave => power: {
|
|
1096
1155
|
const max = @intCast(u8, @as(u16, side.stored().level) * 3 / 2);
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
// GLITCH: Psywave infinite glitch loop
|
|
1101
|
-
if (max <= 1) return Result.Error;
|
|
1102
|
-
while (true) {
|
|
1103
|
-
const r = battle.rng.next();
|
|
1104
|
-
if (r < max) break :power r;
|
|
1105
|
-
}
|
|
1106
|
-
}
|
|
1156
|
+
// GLITCH: Psywave infinite glitch loop
|
|
1157
|
+
if (!showdown and max <= 1) return Result.Error;
|
|
1158
|
+
break :power Rolls.psywave(battle, max);
|
|
1107
1159
|
},
|
|
1108
1160
|
else => unreachable,
|
|
1109
1161
|
};
|
|
1110
1162
|
|
|
1111
1163
|
if (battle.last_damage == 0) return if (showdown) null else Result.Error;
|
|
1112
1164
|
|
|
1165
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1113
1166
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
1114
|
-
try buildRage(battle, player.foe(), log);
|
|
1167
|
+
if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
|
|
1115
1168
|
|
|
1116
1169
|
return null;
|
|
1117
1170
|
}
|
|
@@ -1160,7 +1213,9 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
|
|
|
1160
1213
|
// Pokémon Showdown calls checkHit before Counter
|
|
1161
1214
|
if (!showdown and !try checkHit(battle, player, move, log)) return null;
|
|
1162
1215
|
|
|
1216
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1163
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);
|
|
1164
1219
|
return null;
|
|
1165
1220
|
}
|
|
1166
1221
|
|
|
@@ -1204,6 +1259,7 @@ fn mirrorMove(
|
|
|
1204
1259
|
battle: anytype,
|
|
1205
1260
|
player: Player,
|
|
1206
1261
|
mslot: u4,
|
|
1262
|
+
rewrap: bool,
|
|
1207
1263
|
auto: bool,
|
|
1208
1264
|
residual: *bool,
|
|
1209
1265
|
log: anytype,
|
|
@@ -1216,41 +1272,30 @@ fn mirrorMove(
|
|
|
1216
1272
|
if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
|
|
1217
1273
|
try log.fail(battle.active(player), .None);
|
|
1218
1274
|
return null;
|
|
1275
|
+
} else if (!showdown or foe.last_used_move != .Struggle) {
|
|
1276
|
+
incrementPP(side, mslot);
|
|
1219
1277
|
}
|
|
1220
1278
|
|
|
1221
|
-
incrementPP(side, mslot);
|
|
1222
|
-
|
|
1223
1279
|
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
|
|
1224
|
-
return doMove(battle, player, mslot, auto, residual, log);
|
|
1280
|
+
return doMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
1225
1281
|
}
|
|
1226
1282
|
|
|
1227
1283
|
fn metronome(
|
|
1228
1284
|
battle: anytype,
|
|
1229
1285
|
player: Player,
|
|
1230
1286
|
mslot: u4,
|
|
1287
|
+
rewrap: bool,
|
|
1231
1288
|
auto: bool,
|
|
1232
1289
|
residual: *bool,
|
|
1233
1290
|
log: anytype,
|
|
1234
1291
|
) !?Result {
|
|
1235
1292
|
var side = battle.side(player);
|
|
1236
1293
|
|
|
1237
|
-
side.last_selected_move =
|
|
1238
|
-
const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
|
|
1239
|
-
const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
|
|
1240
|
-
break :blk @intToEnum(Move, r + mod);
|
|
1241
|
-
} else loop: {
|
|
1242
|
-
while (true) {
|
|
1243
|
-
const r = battle.rng.next();
|
|
1244
|
-
if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
|
|
1245
|
-
if (r >= @enumToInt(Move.Struggle)) continue;
|
|
1246
|
-
break :loop @intToEnum(Move, r);
|
|
1247
|
-
}
|
|
1248
|
-
};
|
|
1249
|
-
|
|
1294
|
+
side.last_selected_move = Rolls.metronome(battle);
|
|
1250
1295
|
incrementPP(side, mslot);
|
|
1251
1296
|
|
|
1252
1297
|
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
|
|
1253
|
-
return doMove(battle, player, mslot, auto, residual, log);
|
|
1298
|
+
return doMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
1254
1299
|
}
|
|
1255
1300
|
|
|
1256
1301
|
fn checkHit(battle: anytype, player: Player, move: Move.Data, log: anytype) !bool {
|
|
@@ -1304,23 +1349,24 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
|
|
|
1304
1349
|
}
|
|
1305
1350
|
|
|
1306
1351
|
// GLITCH: Thrash / Petal Dance / Rage get their accuracy overwritten on subsequent hits
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
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));
|
|
1311
1358
|
var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
|
|
1312
1359
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1313
1360
|
boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
|
|
1314
1361
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1315
1362
|
accuracy = @min(255, @max(1, accuracy));
|
|
1316
1363
|
|
|
1317
|
-
|
|
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;
|
|
1318
1367
|
|
|
1319
1368
|
// GLITCH: max accuracy is 255 so 1/256 chance of miss
|
|
1320
|
-
break :miss
|
|
1321
|
-
!battle.rng.chance(u8, @intCast(u8, accuracy), 256)
|
|
1322
|
-
else
|
|
1323
|
-
battle.rng.next() >= accuracy;
|
|
1369
|
+
break :miss !Rolls.hit(battle, accuracy);
|
|
1324
1370
|
};
|
|
1325
1371
|
|
|
1326
1372
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1435,6 +1481,15 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
|
|
|
1435
1481
|
}
|
|
1436
1482
|
|
|
1437
1483
|
if (volatiles.LeechSeed) {
|
|
1484
|
+
var foe = battle.foe(player);
|
|
1485
|
+
var foe_stored = foe.stored();
|
|
1486
|
+
const foe_ident = battle.active(player.foe());
|
|
1487
|
+
|
|
1488
|
+
if (foe_stored.hp == 0) {
|
|
1489
|
+
assert(showdown);
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1438
1493
|
var damage = @max(stored.stats.hp / 16, 1);
|
|
1439
1494
|
|
|
1440
1495
|
// GLITCH: Leech Seed + Toxic glitch
|
|
@@ -1446,10 +1501,6 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
|
|
|
1446
1501
|
const amount = @min(damage, stored.hp);
|
|
1447
1502
|
stored.hp -= amount;
|
|
1448
1503
|
|
|
1449
|
-
var foe = battle.foe(player);
|
|
1450
|
-
var foe_stored = foe.stored();
|
|
1451
|
-
const foe_ident = battle.active(player.foe());
|
|
1452
|
-
|
|
1453
1504
|
// As above, Pokémon Showdown uses damageOf but its not relevant
|
|
1454
1505
|
if (amount > 0) try log.damage(ident, stored, .LeechSeed);
|
|
1455
1506
|
|
|
@@ -1538,6 +1589,42 @@ fn checkEBC(battle: anytype) bool {
|
|
|
1538
1589
|
return false;
|
|
1539
1590
|
}
|
|
1540
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
|
+
|
|
1541
1628
|
inline fn onBegin(
|
|
1542
1629
|
battle: anytype,
|
|
1543
1630
|
player: Player,
|
|
@@ -1628,10 +1715,7 @@ pub const Effects = struct {
|
|
|
1628
1715
|
side.active.volatiles.Bide = true;
|
|
1629
1716
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1630
1717
|
side.active.volatiles.state = 0;
|
|
1631
|
-
side.active.volatiles.attacks =
|
|
1632
|
-
battle.rng.range(u4, 2, 4)
|
|
1633
|
-
else
|
|
1634
|
-
(battle.rng.next() & 1) + 2);
|
|
1718
|
+
side.active.volatiles.attacks = Rolls.bideThrashDuration(battle);
|
|
1635
1719
|
|
|
1636
1720
|
try log.start(battle.active(player), .Bide);
|
|
1637
1721
|
}
|
|
@@ -1653,14 +1737,8 @@ pub const Effects = struct {
|
|
|
1653
1737
|
return;
|
|
1654
1738
|
}
|
|
1655
1739
|
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
else
|
|
1659
|
-
battle.rng.next() < 1 + (if (move.effect == .BurnChance1)
|
|
1660
|
-
Gen12.percent(10)
|
|
1661
|
-
else
|
|
1662
|
-
Gen12.percent(30));
|
|
1663
|
-
if (!chance) return;
|
|
1740
|
+
if (foe.active.types.includes(move.type)) return;
|
|
1741
|
+
if (!Rolls.secondaryChance(battle, move.effect == .BurnChance1)) return;
|
|
1664
1742
|
|
|
1665
1743
|
foe_stored.status = Status.init(.BRN);
|
|
1666
1744
|
foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
|
|
@@ -1684,11 +1762,7 @@ pub const Effects = struct {
|
|
|
1684
1762
|
const sub = foe.active.volatiles.Substitute;
|
|
1685
1763
|
|
|
1686
1764
|
if (move.effect == .ConfusionChance) {
|
|
1687
|
-
|
|
1688
|
-
battle.rng.chance(u8, 25, 256)
|
|
1689
|
-
else
|
|
1690
|
-
battle.rng.next() < Gen12.percent(10);
|
|
1691
|
-
if (!chance) return;
|
|
1765
|
+
if (!Rolls.confusionChance(battle)) return;
|
|
1692
1766
|
} else {
|
|
1693
1767
|
if (showdown) {
|
|
1694
1768
|
if (!try checkHit(battle, player, move, log)) {
|
|
@@ -1707,10 +1781,7 @@ pub const Effects = struct {
|
|
|
1707
1781
|
|
|
1708
1782
|
if (foe.active.volatiles.Confusion) return;
|
|
1709
1783
|
foe.active.volatiles.Confusion = true;
|
|
1710
|
-
foe.active.volatiles.confusion =
|
|
1711
|
-
battle.rng.range(u8, 2, 6)
|
|
1712
|
-
else
|
|
1713
|
-
(battle.rng.next() & 3) + 2);
|
|
1784
|
+
foe.active.volatiles.confusion = Rolls.confusionDuration(battle);
|
|
1714
1785
|
|
|
1715
1786
|
try log.start(battle.active(player.foe()), .Confusion);
|
|
1716
1787
|
}
|
|
@@ -1760,11 +1831,8 @@ pub const Effects = struct {
|
|
|
1760
1831
|
return Result.Error;
|
|
1761
1832
|
}
|
|
1762
1833
|
|
|
1763
|
-
volatiles.disabled_move = @intCast(u3,
|
|
1764
|
-
volatiles.disabled_duration =
|
|
1765
|
-
battle.rng.range(u8, 1, 9)
|
|
1766
|
-
else
|
|
1767
|
-
(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);
|
|
1768
1836
|
|
|
1769
1837
|
try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
|
|
1770
1838
|
return null;
|
|
@@ -1776,7 +1844,7 @@ pub const Effects = struct {
|
|
|
1776
1844
|
|
|
1777
1845
|
if (battle.last_damage == 0) {
|
|
1778
1846
|
assert(showdown);
|
|
1779
|
-
return;
|
|
1847
|
+
if (!battle.foe(player).active.volatiles.Substitute) return;
|
|
1780
1848
|
}
|
|
1781
1849
|
|
|
1782
1850
|
const drain = @max(battle.last_damage / 2, 1);
|
|
@@ -1803,15 +1871,7 @@ pub const Effects = struct {
|
|
|
1803
1871
|
var volatiles = &battle.foe(player).active.volatiles;
|
|
1804
1872
|
|
|
1805
1873
|
if (volatiles.Substitute) return;
|
|
1806
|
-
|
|
1807
|
-
const chance = if (showdown)
|
|
1808
|
-
battle.rng.chance(u8, @as(u8, if (move.effect == .FlinchChance1) 26 else 77), 256)
|
|
1809
|
-
else
|
|
1810
|
-
battle.rng.next() < 1 + (if (move.effect == .FlinchChance1)
|
|
1811
|
-
Gen12.percent(10)
|
|
1812
|
-
else
|
|
1813
|
-
Gen12.percent(30));
|
|
1814
|
-
if (!chance) return;
|
|
1874
|
+
if (!Rolls.secondaryChance(battle, move.effect == .FlinchChance1)) return;
|
|
1815
1875
|
|
|
1816
1876
|
volatiles.Flinch = true;
|
|
1817
1877
|
volatiles.Recharging = false;
|
|
@@ -1837,12 +1897,8 @@ pub const Effects = struct {
|
|
|
1837
1897
|
return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
|
|
1838
1898
|
}
|
|
1839
1899
|
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
else
|
|
1843
|
-
battle.rng.next() < 1 + Gen12.percent(10);
|
|
1844
|
-
if (!chance) return;
|
|
1845
|
-
|
|
1900
|
+
if (foe.active.types.includes(move.type)) return;
|
|
1901
|
+
if (!Rolls.secondaryChance(battle, true)) return;
|
|
1846
1902
|
// Freeze Clause Mod
|
|
1847
1903
|
if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
1848
1904
|
|
|
@@ -1979,15 +2035,15 @@ pub const Effects = struct {
|
|
|
1979
2035
|
}
|
|
1980
2036
|
break :has_mimic false;
|
|
1981
2037
|
};
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
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;
|
|
1985
2041
|
return try log.fail(battle.active(player.foe()), .None);
|
|
1986
2042
|
}
|
|
1987
2043
|
}
|
|
1988
2044
|
if (!try checkHit(battle, player, move, log)) return;
|
|
1989
2045
|
|
|
1990
|
-
const rslot =
|
|
2046
|
+
const rslot = Rolls.moveSlot(battle, &foe.active.moves, 0);
|
|
1991
2047
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
1992
2048
|
|
|
1993
2049
|
try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
|
|
@@ -2008,7 +2064,8 @@ pub const Effects = struct {
|
|
|
2008
2064
|
assert(!side.active.volatiles.MultiHit);
|
|
2009
2065
|
side.active.volatiles.MultiHit = true;
|
|
2010
2066
|
|
|
2011
|
-
side.active.volatiles.attacks =
|
|
2067
|
+
side.active.volatiles.attacks =
|
|
2068
|
+
if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
|
|
2012
2069
|
}
|
|
2013
2070
|
|
|
2014
2071
|
fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2052,14 +2109,8 @@ pub const Effects = struct {
|
|
|
2052
2109
|
}
|
|
2053
2110
|
|
|
2054
2111
|
// Body Slam can't paralyze a Normal type Pokémon
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
else
|
|
2058
|
-
battle.rng.next() < 1 + (if (move.effect == .ParalyzeChance1)
|
|
2059
|
-
Gen12.percent(10)
|
|
2060
|
-
else
|
|
2061
|
-
Gen12.percent(30));
|
|
2062
|
-
if (!chance) return;
|
|
2112
|
+
if (foe.active.types.includes(move.type)) return;
|
|
2113
|
+
if (!Rolls.secondaryChance(battle, move.effect == .ParalyzeChance1)) return;
|
|
2063
2114
|
|
|
2064
2115
|
foe_stored.status = Status.init(.PAR);
|
|
2065
2116
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
@@ -2102,16 +2153,7 @@ pub const Effects = struct {
|
|
|
2102
2153
|
|
|
2103
2154
|
if (move.effect == .Poison) {
|
|
2104
2155
|
if (!showdown and !try checkHit(battle, player, move, log)) return;
|
|
2105
|
-
} else
|
|
2106
|
-
const chance = if (showdown)
|
|
2107
|
-
battle.rng.chance(u8, @as(u8, if (move.effect == .PoisonChance1) 52 else 103), 256)
|
|
2108
|
-
else
|
|
2109
|
-
battle.rng.next() < 1 + (if (move.effect == .PoisonChance1)
|
|
2110
|
-
Gen12.percent(20)
|
|
2111
|
-
else
|
|
2112
|
-
Gen12.percent(40));
|
|
2113
|
-
if (!chance) return;
|
|
2114
|
-
}
|
|
2156
|
+
} else if (!Rolls.poisonChance(battle, move.effect == .PoisonChance1)) return;
|
|
2115
2157
|
|
|
2116
2158
|
foe_stored.status = Status.init(.PSN);
|
|
2117
2159
|
if (toxic) {
|
|
@@ -2125,10 +2167,8 @@ pub const Effects = struct {
|
|
|
2125
2167
|
|
|
2126
2168
|
fn rage(battle: anytype, player: Player) !void {
|
|
2127
2169
|
var volatiles = &battle.side(player).active.volatiles;
|
|
2128
|
-
if (volatiles.Rage) return;
|
|
2129
|
-
volatiles.Rage = true;
|
|
2130
2170
|
assert(!volatiles.Bide);
|
|
2131
|
-
volatiles.
|
|
2171
|
+
volatiles.Rage = true;
|
|
2132
2172
|
}
|
|
2133
2173
|
|
|
2134
2174
|
fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
|
|
@@ -2180,16 +2220,7 @@ pub const Effects = struct {
|
|
|
2180
2220
|
}
|
|
2181
2221
|
foe.active.volatiles.Recharging = false;
|
|
2182
2222
|
|
|
2183
|
-
|
|
2184
|
-
battle.rng.range(u8, 1, 8)
|
|
2185
|
-
else loop: {
|
|
2186
|
-
while (true) {
|
|
2187
|
-
const r = battle.rng.next() & 7;
|
|
2188
|
-
if (r != 0) break :loop r;
|
|
2189
|
-
}
|
|
2190
|
-
});
|
|
2191
|
-
|
|
2192
|
-
foe_stored.status = Status.slp(duration);
|
|
2223
|
+
foe_stored.status = Status.slp(Rolls.sleepDuration(battle));
|
|
2193
2224
|
try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
|
|
2194
2225
|
}
|
|
2195
2226
|
|
|
@@ -2231,8 +2262,9 @@ pub const Effects = struct {
|
|
|
2231
2262
|
fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
2232
2263
|
if (!showdown or battle.side(player).last_selected_move == .Teleport) return;
|
|
2233
2264
|
|
|
2234
|
-
// Whirlwind/Roar should not roll to hit but Pokémon Showdown does anyway
|
|
2265
|
+
// Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
|
|
2235
2266
|
_ = try checkHit(battle, player, move, log);
|
|
2267
|
+
battle.last_damage = 0;
|
|
2236
2268
|
}
|
|
2237
2269
|
|
|
2238
2270
|
fn thrashing(battle: anytype, player: Player) void {
|
|
@@ -2241,11 +2273,7 @@ pub const Effects = struct {
|
|
|
2241
2273
|
assert(!volatiles.Bide);
|
|
2242
2274
|
|
|
2243
2275
|
volatiles.Thrashing = true;
|
|
2244
|
-
volatiles.
|
|
2245
|
-
volatiles.attacks = @intCast(u3, if (showdown)
|
|
2246
|
-
battle.rng.range(u8, 2, 4)
|
|
2247
|
-
else
|
|
2248
|
-
(battle.rng.next() & 1) + 2);
|
|
2276
|
+
volatiles.attacks = Rolls.bideThrashDuration(battle);
|
|
2249
2277
|
}
|
|
2250
2278
|
|
|
2251
2279
|
fn transform(battle: anytype, player: Player, log: anytype) !void {
|
|
@@ -2277,7 +2305,7 @@ pub const Effects = struct {
|
|
|
2277
2305
|
try log.transform(battle.active(player), foe_ident);
|
|
2278
2306
|
}
|
|
2279
2307
|
|
|
2280
|
-
fn binding(battle: anytype, player: Player) void {
|
|
2308
|
+
fn binding(battle: anytype, player: Player, rewrap: bool) void {
|
|
2281
2309
|
var side = battle.side(player);
|
|
2282
2310
|
var foe = battle.foe(player);
|
|
2283
2311
|
|
|
@@ -2285,9 +2313,11 @@ pub const Effects = struct {
|
|
|
2285
2313
|
side.active.volatiles.Binding = true;
|
|
2286
2314
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2287
2315
|
// (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
|
|
2288
|
-
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);
|
|
2289
2319
|
|
|
2290
|
-
side.active.volatiles.attacks = distribution(battle) - 1;
|
|
2320
|
+
side.active.volatiles.attacks = Rolls.distribution(battle) - 1;
|
|
2291
2321
|
}
|
|
2292
2322
|
|
|
2293
2323
|
fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2314,9 +2344,11 @@ pub const Effects = struct {
|
|
|
2314
2344
|
return try log.fail(ident, .None);
|
|
2315
2345
|
}
|
|
2316
2346
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2317
|
-
const stat = unmodifiedStats(battle,
|
|
2347
|
+
const stat = unmodifiedStats(battle, side).atk;
|
|
2318
2348
|
stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2319
2349
|
try log.boost(ident, reason, n);
|
|
2350
|
+
// Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
|
|
2351
|
+
if (showdown and move.effect == .Rage) return;
|
|
2320
2352
|
},
|
|
2321
2353
|
.DefenseUp1, .DefenseUp2 => {
|
|
2322
2354
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
@@ -2332,7 +2364,7 @@ pub const Effects = struct {
|
|
|
2332
2364
|
return try log.fail(ident, .None);
|
|
2333
2365
|
}
|
|
2334
2366
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2335
|
-
const stat = unmodifiedStats(battle,
|
|
2367
|
+
const stat = unmodifiedStats(battle, side).def;
|
|
2336
2368
|
stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2337
2369
|
try log.boost(ident, .Defense, n);
|
|
2338
2370
|
},
|
|
@@ -2349,7 +2381,7 @@ pub const Effects = struct {
|
|
|
2349
2381
|
return try log.fail(ident, .None);
|
|
2350
2382
|
}
|
|
2351
2383
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2352
|
-
const stat = unmodifiedStats(battle,
|
|
2384
|
+
const stat = unmodifiedStats(battle, side).spe;
|
|
2353
2385
|
stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2354
2386
|
try log.boost(ident, .Speed, 2);
|
|
2355
2387
|
},
|
|
@@ -2369,7 +2401,7 @@ pub const Effects = struct {
|
|
|
2369
2401
|
return try log.fail(ident, .None);
|
|
2370
2402
|
}
|
|
2371
2403
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2372
|
-
const stat = unmodifiedStats(battle,
|
|
2404
|
+
const stat = unmodifiedStats(battle, side).spc;
|
|
2373
2405
|
stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2374
2406
|
try log.boost(ident, .SpecialAttack, n);
|
|
2375
2407
|
try log.boost(ident, .SpecialDefense, n);
|
|
@@ -2396,11 +2428,7 @@ pub const Effects = struct {
|
|
|
2396
2428
|
}
|
|
2397
2429
|
|
|
2398
2430
|
if (move.effect.isStatDownChance()) {
|
|
2399
|
-
|
|
2400
|
-
battle.rng.chance(u8, 85, 256)
|
|
2401
|
-
else
|
|
2402
|
-
battle.rng.next() < Gen12.percent(33) + 1;
|
|
2403
|
-
if (!chance or foe.active.volatiles.Invulnerable) return;
|
|
2431
|
+
if (!Rolls.unboost(battle) or foe.active.volatiles.Invulnerable) return;
|
|
2404
2432
|
} else if (!showdown and !try checkHit(battle, player, move, log)) {
|
|
2405
2433
|
return; // checkHit already checks for Invulnerable
|
|
2406
2434
|
}
|
|
@@ -2422,7 +2450,7 @@ pub const Effects = struct {
|
|
|
2422
2450
|
return try log.fail(foe_ident, .None);
|
|
2423
2451
|
}
|
|
2424
2452
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2425
|
-
const stat = unmodifiedStats(battle,
|
|
2453
|
+
const stat = unmodifiedStats(battle, foe).atk;
|
|
2426
2454
|
stats.atk = @max(1, stat * mod[0] / mod[1]);
|
|
2427
2455
|
try log.boost(foe_ident, .Attack, -1);
|
|
2428
2456
|
},
|
|
@@ -2440,7 +2468,7 @@ pub const Effects = struct {
|
|
|
2440
2468
|
return try log.fail(foe_ident, .None);
|
|
2441
2469
|
}
|
|
2442
2470
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2443
|
-
const stat = unmodifiedStats(battle,
|
|
2471
|
+
const stat = unmodifiedStats(battle, foe).def;
|
|
2444
2472
|
stats.def = @max(1, stat * mod[0] / mod[1]);
|
|
2445
2473
|
try log.boost(foe_ident, .Defense, -@as(i8, n));
|
|
2446
2474
|
},
|
|
@@ -2457,7 +2485,7 @@ pub const Effects = struct {
|
|
|
2457
2485
|
return try log.fail(foe_ident, .None);
|
|
2458
2486
|
}
|
|
2459
2487
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2460
|
-
const stat = unmodifiedStats(battle,
|
|
2488
|
+
const stat = unmodifiedStats(battle, foe).spe;
|
|
2461
2489
|
stats.spe = @max(1, stat * mod[0] / mod[1]);
|
|
2462
2490
|
try log.boost(foe_ident, .Speed, -1);
|
|
2463
2491
|
assert(boosts.spe >= -6);
|
|
@@ -2477,7 +2505,7 @@ pub const Effects = struct {
|
|
|
2477
2505
|
return try log.fail(foe_ident, .None);
|
|
2478
2506
|
}
|
|
2479
2507
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2480
|
-
const stat = unmodifiedStats(battle,
|
|
2508
|
+
const stat = unmodifiedStats(battle, foe).spc;
|
|
2481
2509
|
stats.spc = @max(1, stat * mod[0] / mod[1]);
|
|
2482
2510
|
try log.boost(foe_ident, .SpecialAttack, -1);
|
|
2483
2511
|
try log.boost(foe_ident, .SpecialDefense, -1);
|
|
@@ -2496,8 +2524,7 @@ pub const Effects = struct {
|
|
|
2496
2524
|
}
|
|
2497
2525
|
};
|
|
2498
2526
|
|
|
2499
|
-
fn unmodifiedStats(battle: anytype,
|
|
2500
|
-
const side = battle.side(who);
|
|
2527
|
+
fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
|
|
2501
2528
|
if (!side.active.volatiles.Transform) return &side.stored().stats;
|
|
2502
2529
|
const id = ID.from(side.active.volatiles.transform);
|
|
2503
2530
|
return &battle.side(id.player).pokemon[id.id - 1].stats;
|
|
@@ -2563,75 +2590,140 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
|
|
|
2563
2590
|
}
|
|
2564
2591
|
}
|
|
2565
2592
|
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
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;
|
|
2574
2609
|
}
|
|
2575
2610
|
}
|
|
2576
|
-
return false;
|
|
2577
|
-
}
|
|
2578
2611
|
|
|
2579
|
-
inline fn
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
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;
|
|
2583
2615
|
}
|
|
2584
|
-
}
|
|
2585
2616
|
|
|
2586
|
-
fn
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
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
|
+
}
|
|
2590
2621
|
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
else
|
|
2596
|
-
(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
|
+
}
|
|
2597
2626
|
|
|
2598
|
-
|
|
2599
|
-
|
|
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
|
+
}
|
|
2600
2631
|
|
|
2601
|
-
|
|
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
|
+
}
|
|
2602
2636
|
|
|
2603
|
-
fn
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
}
|
|
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
|
+
}
|
|
2608
2641
|
|
|
2609
|
-
fn
|
|
2610
|
-
|
|
2611
|
-
if (
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
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
|
+
}
|
|
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
|
+
}
|
|
2624
2678
|
}
|
|
2679
|
+
return @intCast(u4, i + 1);
|
|
2625
2680
|
}
|
|
2626
|
-
|
|
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;
|
|
2627
2686
|
}
|
|
2628
2687
|
}
|
|
2629
2688
|
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
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);
|
|
2633
2692
|
}
|
|
2634
|
-
|
|
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
|
+
};
|
|
2635
2727
|
|
|
2636
2728
|
test "RNG agreement" {
|
|
2637
2729
|
if (!showdown) return;
|
|
@@ -2731,9 +2823,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
2731
2823
|
while (slot <= 4) : (slot += 1) {
|
|
2732
2824
|
const m = active.moves[slot - 1];
|
|
2733
2825
|
if (m.id == .None) break;
|
|
2734
|
-
if (m.id == side.last_selected_move) {
|
|
2735
|
-
// Pokémon Showdown displays Struggle if limited to Bide
|
|
2736
|
-
const
|
|
2826
|
+
if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
|
|
2827
|
+
// Pokémon Showdown displays Struggle if limited to Bide but unable to pick
|
|
2828
|
+
const struggle =
|
|
2829
|
+
m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);
|
|
2830
|
+
const s = if (struggle) 0 else slot;
|
|
2737
2831
|
out[n] = .{ .type = .Move, .data = s };
|
|
2738
2832
|
n += 1;
|
|
2739
2833
|
return n;
|