@pkmn/engine 0.1.0-dev.7f37a47c → 0.1.0-dev.7fe22fc0
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 +52 -24
- 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 +12 -11
- package/build/pkg/index.js +1 -1
- package/build/pkg/index.js.map +1 -1
- package/build/pkg/protocol.js +1 -1
- package/build/pkg/protocol.js.map +1 -1
- package/build/test/display.js +383 -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 +41 -30
- 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 +59 -35
- package/src/lib/gen1/data.zig +7 -7
- package/src/lib/gen1/helpers.zig +10 -11
- package/src/lib/gen1/mechanics.zig +448 -304
- 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;
|
|
@@ -127,14 +132,20 @@ fn selectMove(
|
|
|
127
132
|
if (volatiles.Recharging) return null;
|
|
128
133
|
if (volatiles.Rage) {
|
|
129
134
|
from.* = side.last_used_move;
|
|
130
|
-
if (showdown)
|
|
135
|
+
if (showdown) {
|
|
136
|
+
if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
|
|
137
|
+
saveMove(battle, player, null);
|
|
138
|
+
}
|
|
131
139
|
return null;
|
|
132
140
|
}
|
|
133
141
|
// Pokémon Showdown removes Flinch at the end-of-turn in its residual handler
|
|
134
142
|
if (!showdown) volatiles.Flinch = false;
|
|
135
143
|
if (volatiles.Thrashing or volatiles.Charging) {
|
|
136
144
|
from.* = side.last_selected_move;
|
|
137
|
-
if (showdown)
|
|
145
|
+
if (showdown) {
|
|
146
|
+
if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
|
|
147
|
+
saveMove(battle, player, null);
|
|
148
|
+
}
|
|
138
149
|
return null;
|
|
139
150
|
}
|
|
140
151
|
|
|
@@ -144,7 +155,10 @@ fn selectMove(
|
|
|
144
155
|
// pre-move select
|
|
145
156
|
if (Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP) or volatiles.Bide) {
|
|
146
157
|
assert(showdown or choice.data == 0);
|
|
147
|
-
if (showdown)
|
|
158
|
+
if (showdown) {
|
|
159
|
+
if (volatiles.Bide and battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
|
|
160
|
+
saveMove(battle, player, choice);
|
|
161
|
+
}
|
|
148
162
|
return null;
|
|
149
163
|
}
|
|
150
164
|
if (volatiles.Binding) {
|
|
@@ -175,6 +189,7 @@ fn selectMove(
|
|
|
175
189
|
}
|
|
176
190
|
|
|
177
191
|
if (battle.foe(player).active.volatiles.Binding) {
|
|
192
|
+
skip_turn.* = true;
|
|
178
193
|
if (showdown) {
|
|
179
194
|
saveMove(battle, player, choice);
|
|
180
195
|
} else {
|
|
@@ -185,6 +200,7 @@ fn selectMove(
|
|
|
185
200
|
}
|
|
186
201
|
|
|
187
202
|
// move select
|
|
203
|
+
volatiles.state = 0;
|
|
188
204
|
if (choice.data == 0) {
|
|
189
205
|
const struggle = ok: {
|
|
190
206
|
for (side.active.moves, 0..) |move, i| {
|
|
@@ -301,11 +317,7 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
|
|
|
301
317
|
// Pokémon Showdown's beforeTurnCallback shenanigans
|
|
302
318
|
if (showdown and m1 == .Counter and m2 == .Counter) battle.rng.advance(1);
|
|
303
319
|
|
|
304
|
-
const p1 =
|
|
305
|
-
battle.rng.range(u8, 0, 2) == 0
|
|
306
|
-
else
|
|
307
|
-
battle.rng.next() < Gen12.percent(50) + 1;
|
|
308
|
-
|
|
320
|
+
const p1 = Rolls.speedTie(battle);
|
|
309
321
|
if (!showdown) return if (p1) .P1 else .P2;
|
|
310
322
|
|
|
311
323
|
// Pokémon Showdown's "lockedmove" volatile's onBeforeTurn uses BattleQueue#changeAction,
|
|
@@ -315,7 +327,7 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
|
|
|
315
327
|
const t1 = battle.side(.P1).active.volatiles.Thrashing;
|
|
316
328
|
const t2 = battle.side(.P2).active.volatiles.Thrashing;
|
|
317
329
|
// 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
|
|
330
|
+
// that out and enforce host ordering of events, so P1 just goes first regardless of who
|
|
319
331
|
// won the original coin flip
|
|
320
332
|
if (t1 and t2) return .P1;
|
|
321
333
|
return if (p1) if (t1 and !t2) .P2 else .P1 else if (t2 and !t1) .P1 else .P2;
|
|
@@ -328,9 +340,13 @@ fn doTurn(
|
|
|
328
340
|
battle: anytype,
|
|
329
341
|
player: Player,
|
|
330
342
|
player_choice: Choice,
|
|
343
|
+
player_rewrap: bool,
|
|
344
|
+
player_skip: bool,
|
|
331
345
|
player_from: ?Move,
|
|
332
346
|
foe_player: Player,
|
|
333
347
|
foe_choice: Choice,
|
|
348
|
+
foe_rewrap: bool,
|
|
349
|
+
foe_skip: bool,
|
|
334
350
|
foe_from: ?Move,
|
|
335
351
|
log: anytype,
|
|
336
352
|
) !?Result {
|
|
@@ -338,7 +354,16 @@ fn doTurn(
|
|
|
338
354
|
|
|
339
355
|
var residual = true;
|
|
340
356
|
var replace = battle.side(player).stored().hp == 0;
|
|
341
|
-
if (try executeMove(
|
|
357
|
+
if (try executeMove(
|
|
358
|
+
battle,
|
|
359
|
+
player,
|
|
360
|
+
player_choice,
|
|
361
|
+
player_rewrap,
|
|
362
|
+
player_skip,
|
|
363
|
+
player_from,
|
|
364
|
+
&residual,
|
|
365
|
+
log,
|
|
366
|
+
)) |r| return r;
|
|
342
367
|
if (!replace) {
|
|
343
368
|
if (player_choice.type != .Switch) {
|
|
344
369
|
if (try checkFaint(battle, foe_player, log)) |r| return r;
|
|
@@ -347,8 +372,18 @@ fn doTurn(
|
|
|
347
372
|
if (try checkFaint(battle, player, log)) |r| return r;
|
|
348
373
|
} else if (foe_choice.type == .Pass) return null;
|
|
349
374
|
|
|
375
|
+
residual = true;
|
|
350
376
|
replace = battle.side(foe_player).stored().hp == 0;
|
|
351
|
-
if (try executeMove(
|
|
377
|
+
if (try executeMove(
|
|
378
|
+
battle,
|
|
379
|
+
foe_player,
|
|
380
|
+
foe_choice,
|
|
381
|
+
foe_rewrap,
|
|
382
|
+
foe_skip,
|
|
383
|
+
foe_from,
|
|
384
|
+
&residual,
|
|
385
|
+
log,
|
|
386
|
+
)) |r| return r;
|
|
352
387
|
if (!replace) {
|
|
353
388
|
if (foe_choice.type != .Switch) {
|
|
354
389
|
if (try checkFaint(battle, player, log)) |r| return r;
|
|
@@ -371,6 +406,8 @@ fn executeMove(
|
|
|
371
406
|
battle: anytype,
|
|
372
407
|
player: Player,
|
|
373
408
|
choice: Choice,
|
|
409
|
+
rewrap: bool,
|
|
410
|
+
skip: bool,
|
|
374
411
|
from: ?Move,
|
|
375
412
|
residual: *bool,
|
|
376
413
|
log: anytype,
|
|
@@ -382,9 +419,10 @@ fn executeMove(
|
|
|
382
419
|
return null;
|
|
383
420
|
}
|
|
384
421
|
|
|
422
|
+
// This is the correct place to check for SKIP_TURN and abort early, however since Pokémon
|
|
423
|
+
// Showdown overwrites the SKIP_TURN sentinel with its botched move select we need to add an
|
|
424
|
+
// additional skip boolean to accomplish the same thing in the Binding check of BeforeMove
|
|
385
425
|
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
426
|
assert(!showdown);
|
|
389
427
|
if (battle.foe(player).active.volatiles.Binding) {
|
|
390
428
|
try log.cant(battle.active(player), .Bound);
|
|
@@ -434,7 +472,7 @@ fn executeMove(
|
|
|
434
472
|
|
|
435
473
|
var skip_can = false;
|
|
436
474
|
var skip_pp = false;
|
|
437
|
-
switch (try beforeMove(battle, player, from, log)) {
|
|
475
|
+
switch (try beforeMove(battle, player, skip, from, residual, log)) {
|
|
438
476
|
.done => return null,
|
|
439
477
|
.skip_can => skip_can = true,
|
|
440
478
|
.skip_pp => skip_pp = true,
|
|
@@ -442,14 +480,23 @@ fn executeMove(
|
|
|
442
480
|
.err => return @as(?Result, Result.Error),
|
|
443
481
|
}
|
|
444
482
|
|
|
445
|
-
if (!skip_can and !try canMove(battle, player, mslot, auto, skip_pp, from, log))
|
|
483
|
+
if (!skip_can and !try canMove(battle, player, mslot, auto, skip_pp, from, residual, log)) {
|
|
484
|
+
return null;
|
|
485
|
+
}
|
|
446
486
|
|
|
447
|
-
return doMove(battle, player, mslot, auto, residual, log);
|
|
487
|
+
return doMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
448
488
|
}
|
|
449
489
|
|
|
450
|
-
const BeforeMove =
|
|
490
|
+
const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
|
|
451
491
|
|
|
452
|
-
fn beforeMove(
|
|
492
|
+
fn beforeMove(
|
|
493
|
+
battle: anytype,
|
|
494
|
+
player: Player,
|
|
495
|
+
skip: bool,
|
|
496
|
+
from: ?Move,
|
|
497
|
+
residual: *bool,
|
|
498
|
+
log: anytype,
|
|
499
|
+
) !BeforeMove {
|
|
453
500
|
var side = battle.side(player);
|
|
454
501
|
const foe = battle.foe(player);
|
|
455
502
|
var active = &side.active;
|
|
@@ -477,7 +524,13 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
|
|
|
477
524
|
return .done;
|
|
478
525
|
}
|
|
479
526
|
|
|
480
|
-
if (
|
|
527
|
+
if (showdown and volatiles.Recharging) {
|
|
528
|
+
volatiles.Recharging = false;
|
|
529
|
+
try log.cant(ident, .Recharge);
|
|
530
|
+
return .done;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (!showdown and foe.active.volatiles.Binding) {
|
|
481
534
|
try log.cant(ident, .Bound);
|
|
482
535
|
return .done;
|
|
483
536
|
}
|
|
@@ -488,7 +541,7 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
|
|
|
488
541
|
return .done;
|
|
489
542
|
}
|
|
490
543
|
|
|
491
|
-
if (volatiles.Recharging) {
|
|
544
|
+
if (!showdown and volatiles.Recharging) {
|
|
492
545
|
volatiles.Recharging = false;
|
|
493
546
|
try log.cant(ident, .Recharge);
|
|
494
547
|
return .done;
|
|
@@ -504,9 +557,15 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
|
|
|
504
557
|
// Pokémon Showdown's disable condition has a single onBeforeMove handler
|
|
505
558
|
if (showdown and try disabled(side, ident, log)) return .done;
|
|
506
559
|
|
|
507
|
-
|
|
560
|
+
if (showdown and skip or foe.active.volatiles.Binding) {
|
|
561
|
+
try log.cant(ident, .Bound);
|
|
562
|
+
return .done;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Pokémon Showdown checks for Flinch *after* instead of before... and doesn't clear Flinch
|
|
566
|
+
// until its imaginary "residual" phase, meaning Pokémon can sometimes flinch multiple times
|
|
567
|
+
// from the same original hit
|
|
508
568
|
if (showdown and volatiles.Flinch) {
|
|
509
|
-
volatiles.Flinch = false;
|
|
510
569
|
try log.cant(ident, .Flinch);
|
|
511
570
|
return .done;
|
|
512
571
|
}
|
|
@@ -528,14 +587,9 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
|
|
|
528
587
|
} else {
|
|
529
588
|
try log.activate(ident, .Confusion);
|
|
530
589
|
|
|
531
|
-
|
|
532
|
-
!battle.rng.chance(u8, 128, 256)
|
|
533
|
-
else
|
|
534
|
-
battle.rng.next() >= Gen12.percent(50) + 1;
|
|
535
|
-
|
|
536
|
-
if (confused) {
|
|
590
|
+
if (Rolls.confused(battle)) {
|
|
537
591
|
assert(!volatiles.MultiHit);
|
|
538
|
-
volatiles.state = 0;
|
|
592
|
+
if (!volatiles.Rage) volatiles.state = 0;
|
|
539
593
|
volatiles.Bide = false;
|
|
540
594
|
volatiles.Thrashing = false;
|
|
541
595
|
volatiles.MultiHit = false;
|
|
@@ -559,8 +613,12 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
|
|
|
559
613
|
player
|
|
560
614
|
else
|
|
561
615
|
player.foe();
|
|
616
|
+
|
|
617
|
+
const uncapped = battle.last_damage;
|
|
562
618
|
// Skipping adjustDamage / randomizeDamage / checkHit
|
|
563
619
|
_ = try applyDamage(battle, player, target, .Confusion, log);
|
|
620
|
+
// Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
|
|
621
|
+
if (showdown) battle.last_damage = uncapped;
|
|
564
622
|
|
|
565
623
|
return .done;
|
|
566
624
|
}
|
|
@@ -569,22 +627,15 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
|
|
|
569
627
|
|
|
570
628
|
if (!showdown and try disabled(side, ident, log)) return .done;
|
|
571
629
|
|
|
572
|
-
if (Status.is(stored.status, .PAR)) {
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
volatiles.Thrashing = false;
|
|
582
|
-
volatiles.Charging = false;
|
|
583
|
-
volatiles.Binding = false;
|
|
584
|
-
// GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
|
|
585
|
-
try log.cant(ident, .Paralysis);
|
|
586
|
-
return .done;
|
|
587
|
-
}
|
|
630
|
+
if (Status.is(stored.status, .PAR) and Rolls.paralyzed(battle)) {
|
|
631
|
+
if (!volatiles.Rage) volatiles.state = 0;
|
|
632
|
+
volatiles.Bide = false;
|
|
633
|
+
volatiles.Thrashing = false;
|
|
634
|
+
volatiles.Charging = false;
|
|
635
|
+
volatiles.Binding = false;
|
|
636
|
+
// GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
|
|
637
|
+
try log.cant(ident, .Paralysis);
|
|
638
|
+
return .done;
|
|
588
639
|
}
|
|
589
640
|
|
|
590
641
|
if (volatiles.Bide) {
|
|
@@ -611,7 +662,18 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
|
|
|
611
662
|
return .done;
|
|
612
663
|
}
|
|
613
664
|
|
|
665
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
614
666
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
667
|
+
if (foe.stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
|
|
668
|
+
|
|
669
|
+
// For reasons passing understanding, Pokémon Showdown still inflicts residual damage to
|
|
670
|
+
// Bide's user even if the above damage has caused the foe to faint. It's simpler to always
|
|
671
|
+
// run residual here regardless of whether the foe fainted and opt-out of the default flow
|
|
672
|
+
if (showdown) {
|
|
673
|
+
residual.* = false;
|
|
674
|
+
try handleResidual(battle, player, log);
|
|
675
|
+
}
|
|
676
|
+
|
|
615
677
|
return .done;
|
|
616
678
|
}
|
|
617
679
|
|
|
@@ -623,8 +685,13 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
|
|
|
623
685
|
try log.start(battle.active(player), .ConfusionSilent);
|
|
624
686
|
}
|
|
625
687
|
try log.move(ident, side.last_selected_move, battle.active(player.foe()), from);
|
|
626
|
-
if (showdown
|
|
627
|
-
|
|
688
|
+
if (showdown) {
|
|
689
|
+
// This shouldn't actually set last_used_move, but Pokémon Showdown sets last
|
|
690
|
+
// used in useMove and doesn't have the notion of skipping canMove semantics
|
|
691
|
+
side.last_used_move = side.last_selected_move;
|
|
692
|
+
if (handleThrashing(battle, active)) {
|
|
693
|
+
try log.start(battle.active(player), .ConfusionSilent);
|
|
694
|
+
}
|
|
628
695
|
}
|
|
629
696
|
return .skip_can;
|
|
630
697
|
}
|
|
@@ -639,7 +706,11 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
|
|
|
639
706
|
side.last_selected_move,
|
|
640
707
|
);
|
|
641
708
|
if (showdown or battle.last_damage != 0) {
|
|
709
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
642
710
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
711
|
+
if (battle.foe(player).stored().hp > 0 and !sub) {
|
|
712
|
+
try buildRage(battle, player.foe(), log);
|
|
713
|
+
}
|
|
643
714
|
}
|
|
644
715
|
return .done;
|
|
645
716
|
}
|
|
@@ -654,6 +725,7 @@ fn canMove(
|
|
|
654
725
|
auto: bool,
|
|
655
726
|
skip_pp: bool,
|
|
656
727
|
from: ?Move,
|
|
728
|
+
residual: *bool,
|
|
657
729
|
log: anytype,
|
|
658
730
|
) !bool {
|
|
659
731
|
var side = battle.side(player);
|
|
@@ -688,7 +760,7 @@ fn canMove(
|
|
|
688
760
|
try log.move(player_ident, side.last_selected_move, battle.active(target), f);
|
|
689
761
|
|
|
690
762
|
if (move.effect.onBegin()) {
|
|
691
|
-
try onBegin(battle, player, move, mslot, log);
|
|
763
|
+
try onBegin(battle, player, move, mslot, residual, log);
|
|
692
764
|
return false;
|
|
693
765
|
}
|
|
694
766
|
|
|
@@ -696,7 +768,7 @@ fn canMove(
|
|
|
696
768
|
Effects.thrashing(battle, player);
|
|
697
769
|
} else if (!showdown and move.effect == .Binding) {
|
|
698
770
|
// Pokémon Showdown handles this after hit/miss checks and damage calculation
|
|
699
|
-
Effects.binding(battle, player);
|
|
771
|
+
Effects.binding(battle, player, false);
|
|
700
772
|
}
|
|
701
773
|
|
|
702
774
|
return true;
|
|
@@ -737,6 +809,7 @@ fn doMove(
|
|
|
737
809
|
battle: anytype,
|
|
738
810
|
player: Player,
|
|
739
811
|
mslot: u4,
|
|
812
|
+
rewrap: bool,
|
|
740
813
|
auto: bool,
|
|
741
814
|
residual: *bool,
|
|
742
815
|
log: anytype,
|
|
@@ -767,16 +840,21 @@ fn doMove(
|
|
|
767
840
|
(@enumToInt(move.type.effectiveness(foe.active.types.type1)) == 0 or
|
|
768
841
|
@enumToInt(move.type.effectiveness(foe.active.types.type2)) == 0);
|
|
769
842
|
if (immune and move.effect != .Binding) break :miss true;
|
|
770
|
-
if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe)
|
|
843
|
+
if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) {
|
|
844
|
+
battle.last_damage = 0;
|
|
845
|
+
break :miss true;
|
|
846
|
+
}
|
|
771
847
|
break :miss move.target != .Self and !moveHit(battle, player, move, &immune, &mist);
|
|
772
848
|
};
|
|
773
849
|
assert(!immune or miss or (showdown and move.effect == .Binding));
|
|
774
850
|
|
|
851
|
+
var late = showdown and move.effect != .Explode;
|
|
775
852
|
const skip = status or immune;
|
|
776
853
|
if ((!showdown or (!skip or counter)) and !miss) blk: {
|
|
777
854
|
if (showdown and move.effect.isMulti()) {
|
|
778
855
|
Effects.multiHit(battle, player, move);
|
|
779
856
|
hits = side.active.volatiles.attacks;
|
|
857
|
+
late = false;
|
|
780
858
|
}
|
|
781
859
|
|
|
782
860
|
// Cartridge rolls for crit even for moves that can't crit (Counter/Metronome/status/OHKO)
|
|
@@ -810,7 +888,7 @@ fn doMove(
|
|
|
810
888
|
}
|
|
811
889
|
|
|
812
890
|
// Due to control flow shenanigans we need to clear last_damage for Pokémon Showdown
|
|
813
|
-
if (showdown and skip
|
|
891
|
+
if (showdown and skip) battle.last_damage = 0;
|
|
814
892
|
|
|
815
893
|
miss = if (showdown or skip)
|
|
816
894
|
miss
|
|
@@ -823,9 +901,9 @@ fn doMove(
|
|
|
823
901
|
|
|
824
902
|
if (!showdown or !miss) {
|
|
825
903
|
if (move.effect == .MirrorMove) {
|
|
826
|
-
return mirrorMove(battle, player, mslot, auto, residual, log);
|
|
904
|
+
return mirrorMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
827
905
|
} else if (move.effect == .Metronome) {
|
|
828
|
-
return metronome(battle, player, mslot, auto, residual, log);
|
|
906
|
+
return metronome(battle, player, mslot, rewrap, auto, residual, log);
|
|
829
907
|
} else if (move.effect.onEnd()) {
|
|
830
908
|
try onEnd(battle, player, move, log);
|
|
831
909
|
return null;
|
|
@@ -840,7 +918,7 @@ fn doMove(
|
|
|
840
918
|
move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
|
|
841
919
|
if (ohko) {
|
|
842
920
|
try log.immune(foe_ident, .OHKO);
|
|
843
|
-
} else if (immune and !invulnerable) {
|
|
921
|
+
} else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
|
|
844
922
|
try log.immune(foe_ident, .None);
|
|
845
923
|
} else if (mist) {
|
|
846
924
|
if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
|
|
@@ -857,15 +935,9 @@ fn doMove(
|
|
|
857
935
|
if (showdown and side.stored().hp == 0) residual.* = false;
|
|
858
936
|
} else if (move.effect == .Explode) {
|
|
859
937
|
try Effects.explode(battle, player);
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
} else if (showdown) {
|
|
864
|
-
if (move.effect == .Disable) {
|
|
865
|
-
if (foe.active.volatiles.Rage and foe.active.boosts.atk < 6) {
|
|
866
|
-
try Effects.boost(battle, player.foe(), Move.get(.Rage), log);
|
|
867
|
-
}
|
|
868
|
-
}
|
|
938
|
+
try buildRage(battle, player.foe(), log);
|
|
939
|
+
} else if (showdown and move.effect == .Disable) {
|
|
940
|
+
try buildRage(battle, player.foe(), log);
|
|
869
941
|
}
|
|
870
942
|
return null;
|
|
871
943
|
}
|
|
@@ -878,6 +950,9 @@ fn doMove(
|
|
|
878
950
|
hits = side.active.volatiles.attacks;
|
|
879
951
|
}
|
|
880
952
|
|
|
953
|
+
// Pokémon Showdown only builds Rage for Disable/Explosion (hit/miss) when attacking into a sub
|
|
954
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
955
|
+
|
|
881
956
|
var nullified = false;
|
|
882
957
|
var hit: u4 = 0;
|
|
883
958
|
while (hit < hits) {
|
|
@@ -893,9 +968,7 @@ fn doMove(
|
|
|
893
968
|
if (hit == 0 and ohko) try log.ohko();
|
|
894
969
|
hit += 1;
|
|
895
970
|
if (foe.stored().hp == 0) break;
|
|
896
|
-
if (
|
|
897
|
-
try Effects.boost(battle, player.foe(), Move.get(.Rage), log);
|
|
898
|
-
}
|
|
971
|
+
if (!late and (!sub or move.effect == .Explode)) try buildRage(battle, player.foe(), log);
|
|
899
972
|
// If the substitute breaks during a multi-hit attack, the attack ends
|
|
900
973
|
if (nullified) break;
|
|
901
974
|
}
|
|
@@ -911,7 +984,7 @@ fn doMove(
|
|
|
911
984
|
} else if (showdown) {
|
|
912
985
|
// This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
|
|
913
986
|
if (move.effect == .Binding) {
|
|
914
|
-
Effects.binding(battle, player);
|
|
987
|
+
Effects.binding(battle, player, rewrap);
|
|
915
988
|
if (immune) {
|
|
916
989
|
battle.last_damage = 0;
|
|
917
990
|
// Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
|
|
@@ -932,6 +1005,9 @@ fn doMove(
|
|
|
932
1005
|
|
|
933
1006
|
if (foe.stored().hp == 0) return null;
|
|
934
1007
|
|
|
1008
|
+
// Pokémon Showdown builds Rage at the wrong time for non-MultiHit move
|
|
1009
|
+
if (late and !sub and move.effect != .Disable) try buildRage(battle, player.foe(), log);
|
|
1010
|
+
|
|
935
1011
|
if (!move.effect.isSpecial()) {
|
|
936
1012
|
// On the cartridge Rage is not considered to be "special" and thus gets executed for a
|
|
937
1013
|
// second time here (after being executed in the "always happens" block above) but that
|
|
@@ -943,7 +1019,9 @@ fn doMove(
|
|
|
943
1019
|
if (move.effect == .Twineedle) {
|
|
944
1020
|
if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), log);
|
|
945
1021
|
} else if (move.effect == .Disable) {
|
|
946
|
-
|
|
1022
|
+
const result = try Effects.disable(battle, player, move, log);
|
|
1023
|
+
if (showdown) try buildRage(battle, player.foe(), log);
|
|
1024
|
+
return result;
|
|
947
1025
|
} else {
|
|
948
1026
|
try moveEffect(battle, player, move, log);
|
|
949
1027
|
}
|
|
@@ -961,8 +1039,7 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
|
|
|
961
1039
|
chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
|
|
962
1040
|
chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
|
|
963
1041
|
|
|
964
|
-
|
|
965
|
-
return std.math.rotl(u8, battle.rng.next(), 3) < chance;
|
|
1042
|
+
return Rolls.criticalHit(battle, chance);
|
|
966
1043
|
}
|
|
967
1044
|
|
|
968
1045
|
fn calcDamage(
|
|
@@ -1066,49 +1143,37 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
|
1066
1143
|
|
|
1067
1144
|
fn randomizeDamage(battle: anytype) void {
|
|
1068
1145
|
if (battle.last_damage <= 1) return;
|
|
1069
|
-
|
|
1070
|
-
const random = if (showdown)
|
|
1071
|
-
battle.rng.range(u8, 217, 256)
|
|
1072
|
-
else loop: {
|
|
1073
|
-
while (true) {
|
|
1074
|
-
const r = std.math.rotr(u8, battle.rng.next(), 1);
|
|
1075
|
-
if (r >= 217) break :loop r;
|
|
1076
|
-
}
|
|
1077
|
-
};
|
|
1078
|
-
|
|
1146
|
+
const random = Rolls.damage(battle);
|
|
1079
1147
|
battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
|
|
1080
1148
|
}
|
|
1081
1149
|
|
|
1082
1150
|
fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
|
|
1083
1151
|
const side = battle.side(player);
|
|
1152
|
+
const foe = battle.foe(player);
|
|
1084
1153
|
|
|
1085
1154
|
if (!try checkHit(battle, player, move, log)) return null;
|
|
1086
1155
|
|
|
1087
1156
|
battle.last_damage = switch (side.last_selected_move) {
|
|
1088
|
-
.SuperFang => @max(
|
|
1157
|
+
.SuperFang => @max(foe.stored().hp / 2, 1),
|
|
1089
1158
|
.SeismicToss, .NightShade => side.stored().level,
|
|
1090
1159
|
.SonicBoom => 20,
|
|
1091
1160
|
.DragonRage => 40,
|
|
1092
1161
|
// GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
|
|
1093
1162
|
.Psywave => power: {
|
|
1094
1163
|
const max = @intCast(u8, @as(u16, side.stored().level) * 3 / 2);
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
// GLITCH: Psywave infinite glitch loop
|
|
1099
|
-
if (max <= 1) return Result.Error;
|
|
1100
|
-
while (true) {
|
|
1101
|
-
const r = battle.rng.next();
|
|
1102
|
-
if (r < max) break :power r;
|
|
1103
|
-
}
|
|
1104
|
-
}
|
|
1164
|
+
// GLITCH: Psywave infinite glitch loop
|
|
1165
|
+
if (!showdown and max <= 1) return Result.Error;
|
|
1166
|
+
break :power Rolls.psywave(battle, max);
|
|
1105
1167
|
},
|
|
1106
1168
|
else => unreachable,
|
|
1107
1169
|
};
|
|
1108
1170
|
|
|
1109
1171
|
if (battle.last_damage == 0) return if (showdown) null else Result.Error;
|
|
1110
1172
|
|
|
1173
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1111
1174
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
1175
|
+
if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
|
|
1176
|
+
|
|
1112
1177
|
return null;
|
|
1113
1178
|
}
|
|
1114
1179
|
|
|
@@ -1156,7 +1221,9 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
|
|
|
1156
1221
|
// Pokémon Showdown calls checkHit before Counter
|
|
1157
1222
|
if (!showdown and !try checkHit(battle, player, move, log)) return null;
|
|
1158
1223
|
|
|
1224
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1159
1225
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
1226
|
+
if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
|
|
1160
1227
|
return null;
|
|
1161
1228
|
}
|
|
1162
1229
|
|
|
@@ -1200,6 +1267,7 @@ fn mirrorMove(
|
|
|
1200
1267
|
battle: anytype,
|
|
1201
1268
|
player: Player,
|
|
1202
1269
|
mslot: u4,
|
|
1270
|
+
rewrap: bool,
|
|
1203
1271
|
auto: bool,
|
|
1204
1272
|
residual: *bool,
|
|
1205
1273
|
log: anytype,
|
|
@@ -1216,37 +1284,26 @@ fn mirrorMove(
|
|
|
1216
1284
|
|
|
1217
1285
|
incrementPP(side, mslot);
|
|
1218
1286
|
|
|
1219
|
-
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, log)) return null;
|
|
1220
|
-
return doMove(battle, player, mslot, auto, residual, log);
|
|
1287
|
+
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
|
|
1288
|
+
return doMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
1221
1289
|
}
|
|
1222
1290
|
|
|
1223
1291
|
fn metronome(
|
|
1224
1292
|
battle: anytype,
|
|
1225
1293
|
player: Player,
|
|
1226
1294
|
mslot: u4,
|
|
1295
|
+
rewrap: bool,
|
|
1227
1296
|
auto: bool,
|
|
1228
1297
|
residual: *bool,
|
|
1229
1298
|
log: anytype,
|
|
1230
1299
|
) !?Result {
|
|
1231
1300
|
var side = battle.side(player);
|
|
1232
1301
|
|
|
1233
|
-
side.last_selected_move =
|
|
1234
|
-
const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
|
|
1235
|
-
const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
|
|
1236
|
-
break :blk @intToEnum(Move, r + mod);
|
|
1237
|
-
} else loop: {
|
|
1238
|
-
while (true) {
|
|
1239
|
-
const r = battle.rng.next();
|
|
1240
|
-
if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
|
|
1241
|
-
if (r >= @enumToInt(Move.Struggle)) continue;
|
|
1242
|
-
break :loop @intToEnum(Move, r);
|
|
1243
|
-
}
|
|
1244
|
-
};
|
|
1245
|
-
|
|
1302
|
+
side.last_selected_move = Rolls.metronome(battle);
|
|
1246
1303
|
incrementPP(side, mslot);
|
|
1247
1304
|
|
|
1248
|
-
if (!try canMove(battle, player, mslot, auto, false, .Metronome, log)) return null;
|
|
1249
|
-
return doMove(battle, player, mslot, auto, residual, log);
|
|
1305
|
+
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
|
|
1306
|
+
return doMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
1250
1307
|
}
|
|
1251
1308
|
|
|
1252
1309
|
fn checkHit(battle: anytype, player: Player, move: Move.Data, log: anytype) !bool {
|
|
@@ -1278,7 +1335,9 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
|
|
|
1278
1335
|
if (move.effect == .Swift) return true;
|
|
1279
1336
|
if (foe.active.volatiles.Invulnerable) break :miss true;
|
|
1280
1337
|
}
|
|
1281
|
-
if (move.effect == .DreamEater and !Status.is(foe.stored().status, .SLP)
|
|
1338
|
+
if (move.effect == .DreamEater and (!Status.is(foe.stored().status, .SLP) or
|
|
1339
|
+
(showdown and foe.active.volatiles.Substitute)))
|
|
1340
|
+
{
|
|
1282
1341
|
immune.* = true;
|
|
1283
1342
|
if (showdown) return false;
|
|
1284
1343
|
break :miss true;
|
|
@@ -1298,23 +1357,24 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
|
|
|
1298
1357
|
}
|
|
1299
1358
|
|
|
1300
1359
|
// GLITCH: Thrash / Petal Dance / Rage get their accuracy overwritten on subsequent hits
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1360
|
+
const state = side.active.volatiles.state;
|
|
1361
|
+
var overwrite = move.effect == .Rage or move.effect == .Thrashing;
|
|
1362
|
+
const overwritten = overwrite and state > 0;
|
|
1363
|
+
assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
|
|
1364
|
+
|
|
1365
|
+
var accuracy = if (overwritten) state else @as(u16, Gen12.percent(move.accuracy));
|
|
1305
1366
|
var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
|
|
1306
1367
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1307
1368
|
boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
|
|
1308
1369
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1309
1370
|
accuracy = @min(255, @max(1, accuracy));
|
|
1310
1371
|
|
|
1311
|
-
|
|
1372
|
+
// Pokémon Showdown only overwrites if the volatile is present
|
|
1373
|
+
if (showdown) overwrite = side.active.volatiles.Rage or side.active.volatiles.Thrashing;
|
|
1374
|
+
if (overwrite) side.active.volatiles.state = accuracy;
|
|
1312
1375
|
|
|
1313
1376
|
// GLITCH: max accuracy is 255 so 1/256 chance of miss
|
|
1314
|
-
break :miss
|
|
1315
|
-
!battle.rng.chance(u8, @intCast(u8, accuracy), 256)
|
|
1316
|
-
else
|
|
1317
|
-
battle.rng.next() >= accuracy;
|
|
1377
|
+
break :miss !Rolls.hit(battle, accuracy);
|
|
1318
1378
|
};
|
|
1319
1379
|
|
|
1320
1380
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1429,6 +1489,15 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
|
|
|
1429
1489
|
}
|
|
1430
1490
|
|
|
1431
1491
|
if (volatiles.LeechSeed) {
|
|
1492
|
+
var foe = battle.foe(player);
|
|
1493
|
+
var foe_stored = foe.stored();
|
|
1494
|
+
const foe_ident = battle.active(player.foe());
|
|
1495
|
+
|
|
1496
|
+
if (foe_stored.hp == 0) {
|
|
1497
|
+
assert(showdown);
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1432
1501
|
var damage = @max(stored.stats.hp / 16, 1);
|
|
1433
1502
|
|
|
1434
1503
|
// GLITCH: Leech Seed + Toxic glitch
|
|
@@ -1440,12 +1509,12 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
|
|
|
1440
1509
|
const amount = @min(damage, stored.hp);
|
|
1441
1510
|
stored.hp -= amount;
|
|
1442
1511
|
|
|
1443
|
-
var foe = battle.foe(player);
|
|
1444
|
-
var foe_stored = foe.stored();
|
|
1445
|
-
const foe_ident = battle.active(player.foe());
|
|
1446
|
-
|
|
1447
1512
|
// As above, Pokémon Showdown uses damageOf but its not relevant
|
|
1448
|
-
if (amount > 0)
|
|
1513
|
+
if (amount > 0) {
|
|
1514
|
+
try log.damage(ident, stored, .LeechSeed);
|
|
1515
|
+
// Pokémon Showdown erroneously updates last damage with uncapped Leech Seed damage
|
|
1516
|
+
if (showdown) battle.last_damage = damage;
|
|
1517
|
+
}
|
|
1449
1518
|
|
|
1450
1519
|
const before = foe_stored.hp;
|
|
1451
1520
|
// Uncapped damage is added back to the foe
|
|
@@ -1480,16 +1549,20 @@ fn endTurn(battle: anytype, log: anytype) @TypeOf(log).Error!Result {
|
|
|
1480
1549
|
|
|
1481
1550
|
fn checkEBC(battle: anytype) bool {
|
|
1482
1551
|
for (battle.sides, 0..) |side, i| {
|
|
1552
|
+
const foe = battle.sides[~@intCast(u1, i)];
|
|
1553
|
+
|
|
1483
1554
|
var foe_all_ghosts = true;
|
|
1484
1555
|
var foe_all_transform = true;
|
|
1556
|
+
for (foe.order, 0..) |id, j| {
|
|
1557
|
+
if (id == 0) break;
|
|
1558
|
+
const active = j == 0;
|
|
1559
|
+
const pokemon = foe.pokemon[id - 1];
|
|
1485
1560
|
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
const ghost = pokemon.hp == 0 or pokemon.types.includes(.Ghost);
|
|
1561
|
+
const ghost = pokemon.hp == 0 or
|
|
1562
|
+
(if (active) foe.active.types else pokemon.types).includes(.Ghost);
|
|
1490
1563
|
foe_all_ghosts = foe_all_ghosts and ghost;
|
|
1491
1564
|
foe_all_transform = foe_all_transform and pokemon.hp == 0 or transform: {
|
|
1492
|
-
for (pokemon.moves) |m| {
|
|
1565
|
+
for (if (active) foe.active.moves else pokemon.moves) |m| {
|
|
1493
1566
|
if (m.id == .None) break :transform true;
|
|
1494
1567
|
if (m.id != .Transform) break :transform false;
|
|
1495
1568
|
}
|
|
@@ -1497,10 +1570,14 @@ fn checkEBC(battle: anytype) bool {
|
|
|
1497
1570
|
};
|
|
1498
1571
|
}
|
|
1499
1572
|
|
|
1500
|
-
for (side.
|
|
1573
|
+
for (side.order, 0..) |id, j| {
|
|
1574
|
+
if (id == 0) break;
|
|
1575
|
+
const active = j == 0;
|
|
1576
|
+
const pokemon = side.pokemon[id - 1];
|
|
1577
|
+
|
|
1501
1578
|
if (pokemon.hp == 0 or Status.is(pokemon.status, .FRZ)) continue;
|
|
1502
1579
|
const transform = foe_all_transform and transform: {
|
|
1503
|
-
for (pokemon.moves) |m| {
|
|
1580
|
+
for (if (active) side.active.moves else pokemon.moves) |m| {
|
|
1504
1581
|
if (m.id == .None) break :transform true;
|
|
1505
1582
|
if (m.id != .Transform) break :transform false;
|
|
1506
1583
|
}
|
|
@@ -1508,7 +1585,7 @@ fn checkEBC(battle: anytype) bool {
|
|
|
1508
1585
|
};
|
|
1509
1586
|
if (transform) continue;
|
|
1510
1587
|
const no_pp = foe_all_ghosts and no_pp: {
|
|
1511
|
-
for (pokemon.moves) |m| {
|
|
1588
|
+
for (if (active) side.active.moves else pokemon.moves) |m| {
|
|
1512
1589
|
if (m.pp != 0) break :no_pp false;
|
|
1513
1590
|
}
|
|
1514
1591
|
break :no_pp true;
|
|
@@ -1524,7 +1601,50 @@ fn checkEBC(battle: anytype) bool {
|
|
|
1524
1601
|
return false;
|
|
1525
1602
|
}
|
|
1526
1603
|
|
|
1527
|
-
|
|
1604
|
+
fn disabled(side: *Side, ident: ID, log: anytype) !bool {
|
|
1605
|
+
if (side.active.volatiles.disabled_move != 0) {
|
|
1606
|
+
// A Pokémon that transforms after being disabled may end up with less move slots
|
|
1607
|
+
const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
|
|
1608
|
+
// side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
|
|
1609
|
+
const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
|
|
1610
|
+
m
|
|
1611
|
+
else
|
|
1612
|
+
side.last_selected_move;
|
|
1613
|
+
if (m != .None and m == last) {
|
|
1614
|
+
side.active.volatiles.Charging = false;
|
|
1615
|
+
try log.disabled(ident, last);
|
|
1616
|
+
return true;
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
return false;
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
inline fn buildRage(battle: anytype, who: Player, log: anytype) !void {
|
|
1623
|
+
const side = battle.side(who);
|
|
1624
|
+
if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
|
|
1625
|
+
try Effects.boost(battle, who, Move.get(.Rage), log);
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
|
|
1630
|
+
var volatiles = &active.volatiles;
|
|
1631
|
+
assert(volatiles.Thrashing);
|
|
1632
|
+
if (volatiles.attacks > 0) return false;
|
|
1633
|
+
|
|
1634
|
+
volatiles.Thrashing = false;
|
|
1635
|
+
volatiles.Confusion = true;
|
|
1636
|
+
volatiles.confusion = Rolls.confusionDuration(battle);
|
|
1637
|
+
return true;
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
inline fn onBegin(
|
|
1641
|
+
battle: anytype,
|
|
1642
|
+
player: Player,
|
|
1643
|
+
move: Move.Data,
|
|
1644
|
+
mslot: u8,
|
|
1645
|
+
residual: *bool,
|
|
1646
|
+
log: anytype,
|
|
1647
|
+
) !void {
|
|
1528
1648
|
assert(move.effect.onBegin());
|
|
1529
1649
|
return switch (move.effect) {
|
|
1530
1650
|
.Confusion => Effects.confusion(battle, player, move, log),
|
|
@@ -1540,7 +1660,7 @@ inline fn onBegin(battle: anytype, player: Player, move: Move.Data, mslot: u8, l
|
|
|
1540
1660
|
.Poison => Effects.poison(battle, player, move, log),
|
|
1541
1661
|
.Reflect => Effects.reflect(battle, player, log),
|
|
1542
1662
|
.Splash => Effects.splash(battle, player, log),
|
|
1543
|
-
.Substitute => Effects.substitute(battle, player, log),
|
|
1663
|
+
.Substitute => Effects.substitute(battle, player, residual, log),
|
|
1544
1664
|
.SwitchAndTeleport => Effects.switchAndTeleport(battle, player, move, log),
|
|
1545
1665
|
.Transform => Effects.transform(battle, player, log),
|
|
1546
1666
|
else => unreachable,
|
|
@@ -1607,10 +1727,7 @@ pub const Effects = struct {
|
|
|
1607
1727
|
side.active.volatiles.Bide = true;
|
|
1608
1728
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1609
1729
|
side.active.volatiles.state = 0;
|
|
1610
|
-
side.active.volatiles.attacks =
|
|
1611
|
-
battle.rng.range(u4, 2, 4)
|
|
1612
|
-
else
|
|
1613
|
-
(battle.rng.next() & 1) + 2);
|
|
1730
|
+
side.active.volatiles.attacks = Rolls.bideThrashDuration(battle);
|
|
1614
1731
|
|
|
1615
1732
|
try log.start(battle.active(player), .Bide);
|
|
1616
1733
|
}
|
|
@@ -1632,14 +1749,8 @@ pub const Effects = struct {
|
|
|
1632
1749
|
return;
|
|
1633
1750
|
}
|
|
1634
1751
|
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
else
|
|
1638
|
-
battle.rng.next() < 1 + (if (move.effect == .BurnChance1)
|
|
1639
|
-
Gen12.percent(10)
|
|
1640
|
-
else
|
|
1641
|
-
Gen12.percent(30));
|
|
1642
|
-
if (!chance) return;
|
|
1752
|
+
if (foe.active.types.includes(move.type)) return;
|
|
1753
|
+
if (!Rolls.secondaryChance(battle, move.effect == .BurnChance1)) return;
|
|
1643
1754
|
|
|
1644
1755
|
foe_stored.status = Status.init(.BRN);
|
|
1645
1756
|
foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
|
|
@@ -1663,11 +1774,7 @@ pub const Effects = struct {
|
|
|
1663
1774
|
const sub = foe.active.volatiles.Substitute;
|
|
1664
1775
|
|
|
1665
1776
|
if (move.effect == .ConfusionChance) {
|
|
1666
|
-
|
|
1667
|
-
battle.rng.chance(u8, 25, 256)
|
|
1668
|
-
else
|
|
1669
|
-
battle.rng.next() < Gen12.percent(10);
|
|
1670
|
-
if (!chance) return;
|
|
1777
|
+
if (!Rolls.confusionChance(battle)) return;
|
|
1671
1778
|
} else {
|
|
1672
1779
|
if (showdown) {
|
|
1673
1780
|
if (!try checkHit(battle, player, move, log)) {
|
|
@@ -1686,10 +1793,7 @@ pub const Effects = struct {
|
|
|
1686
1793
|
|
|
1687
1794
|
if (foe.active.volatiles.Confusion) return;
|
|
1688
1795
|
foe.active.volatiles.Confusion = true;
|
|
1689
|
-
foe.active.volatiles.confusion =
|
|
1690
|
-
battle.rng.range(u8, 2, 6)
|
|
1691
|
-
else
|
|
1692
|
-
(battle.rng.next() & 3) + 2);
|
|
1796
|
+
foe.active.volatiles.confusion = Rolls.confusionDuration(battle);
|
|
1693
1797
|
|
|
1694
1798
|
try log.start(battle.active(player.foe()), .Confusion);
|
|
1695
1799
|
}
|
|
@@ -1739,11 +1843,8 @@ pub const Effects = struct {
|
|
|
1739
1843
|
return Result.Error;
|
|
1740
1844
|
}
|
|
1741
1845
|
|
|
1742
|
-
volatiles.disabled_move = @intCast(u3,
|
|
1743
|
-
volatiles.disabled_duration =
|
|
1744
|
-
battle.rng.range(u8, 1, 9)
|
|
1745
|
-
else
|
|
1746
|
-
(battle.rng.next() & 7) + 1);
|
|
1846
|
+
volatiles.disabled_move = @intCast(u3, Rolls.moveSlot(battle, &foe.active.moves, n));
|
|
1847
|
+
volatiles.disabled_duration = Rolls.disableDuration(battle);
|
|
1747
1848
|
|
|
1748
1849
|
try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
|
|
1749
1850
|
return null;
|
|
@@ -1753,6 +1854,11 @@ pub const Effects = struct {
|
|
|
1753
1854
|
var side = battle.side(player);
|
|
1754
1855
|
var stored = side.stored();
|
|
1755
1856
|
|
|
1857
|
+
if (battle.last_damage == 0) {
|
|
1858
|
+
assert(showdown);
|
|
1859
|
+
if (!battle.foe(player).active.volatiles.Substitute) return;
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1756
1862
|
const drain = @max(battle.last_damage / 2, 1);
|
|
1757
1863
|
battle.last_damage = drain;
|
|
1758
1864
|
|
|
@@ -1777,15 +1883,7 @@ pub const Effects = struct {
|
|
|
1777
1883
|
var volatiles = &battle.foe(player).active.volatiles;
|
|
1778
1884
|
|
|
1779
1885
|
if (volatiles.Substitute) return;
|
|
1780
|
-
|
|
1781
|
-
const chance = if (showdown)
|
|
1782
|
-
battle.rng.chance(u8, @as(u8, if (move.effect == .FlinchChance1) 26 else 77), 256)
|
|
1783
|
-
else
|
|
1784
|
-
battle.rng.next() < 1 + (if (move.effect == .FlinchChance1)
|
|
1785
|
-
Gen12.percent(10)
|
|
1786
|
-
else
|
|
1787
|
-
Gen12.percent(30));
|
|
1788
|
-
if (!chance) return;
|
|
1886
|
+
if (!Rolls.secondaryChance(battle, move.effect == .FlinchChance1)) return;
|
|
1789
1887
|
|
|
1790
1888
|
volatiles.Flinch = true;
|
|
1791
1889
|
volatiles.Recharging = false;
|
|
@@ -1811,12 +1909,8 @@ pub const Effects = struct {
|
|
|
1811
1909
|
return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
|
|
1812
1910
|
}
|
|
1813
1911
|
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
else
|
|
1817
|
-
battle.rng.next() < 1 + Gen12.percent(10);
|
|
1818
|
-
if (!chance) return;
|
|
1819
|
-
|
|
1912
|
+
if (foe.active.types.includes(move.type)) return;
|
|
1913
|
+
if (!Rolls.secondaryChance(battle, true)) return;
|
|
1820
1914
|
// Freeze Clause Mod
|
|
1821
1915
|
if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
1822
1916
|
|
|
@@ -1961,7 +2055,7 @@ pub const Effects = struct {
|
|
|
1961
2055
|
}
|
|
1962
2056
|
if (!try checkHit(battle, player, move, log)) return;
|
|
1963
2057
|
|
|
1964
|
-
const rslot =
|
|
2058
|
+
const rslot = Rolls.moveSlot(battle, &foe.active.moves, 0);
|
|
1965
2059
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
1966
2060
|
|
|
1967
2061
|
try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
|
|
@@ -1982,7 +2076,8 @@ pub const Effects = struct {
|
|
|
1982
2076
|
assert(!side.active.volatiles.MultiHit);
|
|
1983
2077
|
side.active.volatiles.MultiHit = true;
|
|
1984
2078
|
|
|
1985
|
-
side.active.volatiles.attacks =
|
|
2079
|
+
side.active.volatiles.attacks =
|
|
2080
|
+
if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
|
|
1986
2081
|
}
|
|
1987
2082
|
|
|
1988
2083
|
fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2026,14 +2121,8 @@ pub const Effects = struct {
|
|
|
2026
2121
|
}
|
|
2027
2122
|
|
|
2028
2123
|
// Body Slam can't paralyze a Normal type Pokémon
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
else
|
|
2032
|
-
battle.rng.next() < 1 + (if (move.effect == .ParalyzeChance1)
|
|
2033
|
-
Gen12.percent(10)
|
|
2034
|
-
else
|
|
2035
|
-
Gen12.percent(30));
|
|
2036
|
-
if (!chance) return;
|
|
2124
|
+
if (foe.active.types.includes(move.type)) return;
|
|
2125
|
+
if (!Rolls.secondaryChance(battle, move.effect == .ParalyzeChance1)) return;
|
|
2037
2126
|
|
|
2038
2127
|
foe_stored.status = Status.init(.PAR);
|
|
2039
2128
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
@@ -2063,7 +2152,7 @@ pub const Effects = struct {
|
|
|
2063
2152
|
return log.fail(foe_ident, if (Status.is(foe_stored.status, .PSN))
|
|
2064
2153
|
if (!showdown)
|
|
2065
2154
|
.Poison
|
|
2066
|
-
else if (toxic ==
|
|
2155
|
+
else if (toxic == (foe_stored.status == Status.TOX))
|
|
2067
2156
|
if (toxic) .Toxic else .Poison
|
|
2068
2157
|
else
|
|
2069
2158
|
.None
|
|
@@ -2076,16 +2165,7 @@ pub const Effects = struct {
|
|
|
2076
2165
|
|
|
2077
2166
|
if (move.effect == .Poison) {
|
|
2078
2167
|
if (!showdown and !try checkHit(battle, player, move, log)) return;
|
|
2079
|
-
} else
|
|
2080
|
-
const chance = if (showdown)
|
|
2081
|
-
battle.rng.chance(u8, @as(u8, if (move.effect == .PoisonChance1) 52 else 103), 256)
|
|
2082
|
-
else
|
|
2083
|
-
battle.rng.next() < 1 + (if (move.effect == .PoisonChance1)
|
|
2084
|
-
Gen12.percent(20)
|
|
2085
|
-
else
|
|
2086
|
-
Gen12.percent(40));
|
|
2087
|
-
if (!chance) return;
|
|
2088
|
-
}
|
|
2168
|
+
} else if (!Rolls.poisonChance(battle, move.effect == .PoisonChance1)) return;
|
|
2089
2169
|
|
|
2090
2170
|
foe_stored.status = Status.init(.PSN);
|
|
2091
2171
|
if (toxic) {
|
|
@@ -2099,10 +2179,8 @@ pub const Effects = struct {
|
|
|
2099
2179
|
|
|
2100
2180
|
fn rage(battle: anytype, player: Player) !void {
|
|
2101
2181
|
var volatiles = &battle.side(player).active.volatiles;
|
|
2102
|
-
if (volatiles.Rage) return;
|
|
2103
|
-
volatiles.Rage = true;
|
|
2104
2182
|
assert(!volatiles.Bide);
|
|
2105
|
-
volatiles.
|
|
2183
|
+
volatiles.Rage = true;
|
|
2106
2184
|
}
|
|
2107
2185
|
|
|
2108
2186
|
fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
|
|
@@ -2154,16 +2232,7 @@ pub const Effects = struct {
|
|
|
2154
2232
|
}
|
|
2155
2233
|
foe.active.volatiles.Recharging = false;
|
|
2156
2234
|
|
|
2157
|
-
|
|
2158
|
-
battle.rng.range(u8, 1, 8)
|
|
2159
|
-
else loop: {
|
|
2160
|
-
while (true) {
|
|
2161
|
-
const r = battle.rng.next() & 7;
|
|
2162
|
-
if (r != 0) break :loop r;
|
|
2163
|
-
}
|
|
2164
|
-
});
|
|
2165
|
-
|
|
2166
|
-
foe_stored.status = Status.slp(duration);
|
|
2235
|
+
foe_stored.status = Status.slp(Rolls.sleepDuration(battle));
|
|
2167
2236
|
try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
|
|
2168
2237
|
}
|
|
2169
2238
|
|
|
@@ -2171,7 +2240,7 @@ pub const Effects = struct {
|
|
|
2171
2240
|
try log.activate(battle.active(player), .Splash);
|
|
2172
2241
|
}
|
|
2173
2242
|
|
|
2174
|
-
fn substitute(battle: anytype, player: Player, log: anytype) !void {
|
|
2243
|
+
fn substitute(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
|
|
2175
2244
|
var side = battle.side(player);
|
|
2176
2245
|
if (side.active.volatiles.Substitute) {
|
|
2177
2246
|
try log.fail(battle.active(player), .Substitute);
|
|
@@ -2196,14 +2265,18 @@ pub const Effects = struct {
|
|
|
2196
2265
|
side.active.volatiles.substitute = hp + 1;
|
|
2197
2266
|
side.active.volatiles.Substitute = true;
|
|
2198
2267
|
try log.start(battle.active(player), .Substitute);
|
|
2199
|
-
if (hp > 0)
|
|
2268
|
+
if (hp > 0) {
|
|
2269
|
+
try log.damage(battle.active(player), side.stored(), .None);
|
|
2270
|
+
if (showdown and side.stored().hp == 0) residual.* = false;
|
|
2271
|
+
}
|
|
2200
2272
|
}
|
|
2201
2273
|
|
|
2202
2274
|
fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
2203
2275
|
if (!showdown or battle.side(player).last_selected_move == .Teleport) return;
|
|
2204
2276
|
|
|
2205
|
-
// Whirlwind/Roar should not roll to hit but Pokémon Showdown does anyway
|
|
2277
|
+
// Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
|
|
2206
2278
|
_ = try checkHit(battle, player, move, log);
|
|
2279
|
+
battle.last_damage = 0;
|
|
2207
2280
|
}
|
|
2208
2281
|
|
|
2209
2282
|
fn thrashing(battle: anytype, player: Player) void {
|
|
@@ -2212,11 +2285,7 @@ pub const Effects = struct {
|
|
|
2212
2285
|
assert(!volatiles.Bide);
|
|
2213
2286
|
|
|
2214
2287
|
volatiles.Thrashing = true;
|
|
2215
|
-
volatiles.
|
|
2216
|
-
volatiles.attacks = @intCast(u3, if (showdown)
|
|
2217
|
-
battle.rng.range(u8, 2, 4)
|
|
2218
|
-
else
|
|
2219
|
-
(battle.rng.next() & 1) + 2);
|
|
2288
|
+
volatiles.attacks = Rolls.bideThrashDuration(battle);
|
|
2220
2289
|
}
|
|
2221
2290
|
|
|
2222
2291
|
fn transform(battle: anytype, player: Player, log: anytype) !void {
|
|
@@ -2248,7 +2317,7 @@ pub const Effects = struct {
|
|
|
2248
2317
|
try log.transform(battle.active(player), foe_ident);
|
|
2249
2318
|
}
|
|
2250
2319
|
|
|
2251
|
-
fn binding(battle: anytype, player: Player) void {
|
|
2320
|
+
fn binding(battle: anytype, player: Player, rewrap: bool) void {
|
|
2252
2321
|
var side = battle.side(player);
|
|
2253
2322
|
var foe = battle.foe(player);
|
|
2254
2323
|
|
|
@@ -2256,9 +2325,11 @@ pub const Effects = struct {
|
|
|
2256
2325
|
side.active.volatiles.Binding = true;
|
|
2257
2326
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2258
2327
|
// (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
|
|
2259
|
-
if (!showdown)
|
|
2328
|
+
if (!showdown) {
|
|
2329
|
+
foe.active.volatiles.Recharging = false;
|
|
2330
|
+
} else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
|
|
2260
2331
|
|
|
2261
|
-
side.active.volatiles.attacks = distribution(battle) - 1;
|
|
2332
|
+
side.active.volatiles.attacks = Rolls.distribution(battle) - 1;
|
|
2262
2333
|
}
|
|
2263
2334
|
|
|
2264
2335
|
fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2285,9 +2356,11 @@ pub const Effects = struct {
|
|
|
2285
2356
|
return try log.fail(ident, .None);
|
|
2286
2357
|
}
|
|
2287
2358
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2288
|
-
const stat = unmodifiedStats(battle,
|
|
2359
|
+
const stat = unmodifiedStats(battle, side).atk;
|
|
2289
2360
|
stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2290
2361
|
try log.boost(ident, reason, n);
|
|
2362
|
+
// Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
|
|
2363
|
+
if (showdown and move.effect == .Rage) return;
|
|
2291
2364
|
},
|
|
2292
2365
|
.DefenseUp1, .DefenseUp2 => {
|
|
2293
2366
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
@@ -2303,7 +2376,7 @@ pub const Effects = struct {
|
|
|
2303
2376
|
return try log.fail(ident, .None);
|
|
2304
2377
|
}
|
|
2305
2378
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2306
|
-
const stat = unmodifiedStats(battle,
|
|
2379
|
+
const stat = unmodifiedStats(battle, side).def;
|
|
2307
2380
|
stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2308
2381
|
try log.boost(ident, .Defense, n);
|
|
2309
2382
|
},
|
|
@@ -2320,7 +2393,7 @@ pub const Effects = struct {
|
|
|
2320
2393
|
return try log.fail(ident, .None);
|
|
2321
2394
|
}
|
|
2322
2395
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2323
|
-
const stat = unmodifiedStats(battle,
|
|
2396
|
+
const stat = unmodifiedStats(battle, side).spe;
|
|
2324
2397
|
stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2325
2398
|
try log.boost(ident, .Speed, 2);
|
|
2326
2399
|
},
|
|
@@ -2340,7 +2413,7 @@ pub const Effects = struct {
|
|
|
2340
2413
|
return try log.fail(ident, .None);
|
|
2341
2414
|
}
|
|
2342
2415
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2343
|
-
const stat = unmodifiedStats(battle,
|
|
2416
|
+
const stat = unmodifiedStats(battle, side).spc;
|
|
2344
2417
|
stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2345
2418
|
try log.boost(ident, .SpecialAttack, n);
|
|
2346
2419
|
try log.boost(ident, .SpecialDefense, n);
|
|
@@ -2367,11 +2440,7 @@ pub const Effects = struct {
|
|
|
2367
2440
|
}
|
|
2368
2441
|
|
|
2369
2442
|
if (move.effect.isStatDownChance()) {
|
|
2370
|
-
|
|
2371
|
-
battle.rng.chance(u8, 85, 256)
|
|
2372
|
-
else
|
|
2373
|
-
battle.rng.next() < Gen12.percent(33) + 1;
|
|
2374
|
-
if (!chance or foe.active.volatiles.Invulnerable) return;
|
|
2443
|
+
if (!Rolls.unboost(battle) or foe.active.volatiles.Invulnerable) return;
|
|
2375
2444
|
} else if (!showdown and !try checkHit(battle, player, move, log)) {
|
|
2376
2445
|
return; // checkHit already checks for Invulnerable
|
|
2377
2446
|
}
|
|
@@ -2393,7 +2462,7 @@ pub const Effects = struct {
|
|
|
2393
2462
|
return try log.fail(foe_ident, .None);
|
|
2394
2463
|
}
|
|
2395
2464
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2396
|
-
const stat = unmodifiedStats(battle,
|
|
2465
|
+
const stat = unmodifiedStats(battle, foe).atk;
|
|
2397
2466
|
stats.atk = @max(1, stat * mod[0] / mod[1]);
|
|
2398
2467
|
try log.boost(foe_ident, .Attack, -1);
|
|
2399
2468
|
},
|
|
@@ -2411,7 +2480,7 @@ pub const Effects = struct {
|
|
|
2411
2480
|
return try log.fail(foe_ident, .None);
|
|
2412
2481
|
}
|
|
2413
2482
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2414
|
-
const stat = unmodifiedStats(battle,
|
|
2483
|
+
const stat = unmodifiedStats(battle, foe).def;
|
|
2415
2484
|
stats.def = @max(1, stat * mod[0] / mod[1]);
|
|
2416
2485
|
try log.boost(foe_ident, .Defense, -@as(i8, n));
|
|
2417
2486
|
},
|
|
@@ -2428,7 +2497,7 @@ pub const Effects = struct {
|
|
|
2428
2497
|
return try log.fail(foe_ident, .None);
|
|
2429
2498
|
}
|
|
2430
2499
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2431
|
-
const stat = unmodifiedStats(battle,
|
|
2500
|
+
const stat = unmodifiedStats(battle, foe).spe;
|
|
2432
2501
|
stats.spe = @max(1, stat * mod[0] / mod[1]);
|
|
2433
2502
|
try log.boost(foe_ident, .Speed, -1);
|
|
2434
2503
|
assert(boosts.spe >= -6);
|
|
@@ -2448,7 +2517,7 @@ pub const Effects = struct {
|
|
|
2448
2517
|
return try log.fail(foe_ident, .None);
|
|
2449
2518
|
}
|
|
2450
2519
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2451
|
-
const stat = unmodifiedStats(battle,
|
|
2520
|
+
const stat = unmodifiedStats(battle, foe).spc;
|
|
2452
2521
|
stats.spc = @max(1, stat * mod[0] / mod[1]);
|
|
2453
2522
|
try log.boost(foe_ident, .SpecialAttack, -1);
|
|
2454
2523
|
try log.boost(foe_ident, .SpecialDefense, -1);
|
|
@@ -2467,8 +2536,7 @@ pub const Effects = struct {
|
|
|
2467
2536
|
}
|
|
2468
2537
|
};
|
|
2469
2538
|
|
|
2470
|
-
fn unmodifiedStats(battle: anytype,
|
|
2471
|
-
const side = battle.side(who);
|
|
2539
|
+
fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
|
|
2472
2540
|
if (!side.active.volatiles.Transform) return &side.stored().stats;
|
|
2473
2541
|
const id = ID.from(side.active.volatiles.transform);
|
|
2474
2542
|
return &battle.side(id.player).pokemon[id.id - 1].stats;
|
|
@@ -2482,7 +2550,7 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
|
|
|
2482
2550
|
}
|
|
2483
2551
|
}
|
|
2484
2552
|
|
|
2485
|
-
fn isForced(active: ActivePokemon) bool {
|
|
2553
|
+
inline fn isForced(active: ActivePokemon) bool {
|
|
2486
2554
|
return active.volatiles.Recharging or active.volatiles.Rage or
|
|
2487
2555
|
active.volatiles.Thrashing or active.volatiles.Charging;
|
|
2488
2556
|
}
|
|
@@ -2514,13 +2582,9 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
|
|
|
2514
2582
|
volatiles.LeechSeed = false;
|
|
2515
2583
|
try log.end(ident, .LeechSeed);
|
|
2516
2584
|
}
|
|
2517
|
-
if (volatiles.Toxic) {
|
|
2585
|
+
if (!showdown and volatiles.Toxic) {
|
|
2518
2586
|
volatiles.Toxic = false;
|
|
2519
|
-
// volatiles.toxic is left unchanged, except on Pokémon Showdown which clears it
|
|
2520
|
-
if (showdown) {
|
|
2521
|
-
volatiles.toxic = 0;
|
|
2522
|
-
try log.end(ident, .Toxic);
|
|
2523
|
-
}
|
|
2587
|
+
// volatiles.toxic is left unchanged, except on Pokémon Showdown which clears it (below)
|
|
2524
2588
|
}
|
|
2525
2589
|
if (volatiles.LightScreen) {
|
|
2526
2590
|
volatiles.LightScreen = false;
|
|
@@ -2530,70 +2594,148 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
|
|
|
2530
2594
|
volatiles.Reflect = false;
|
|
2531
2595
|
try log.end(ident, .Reflect);
|
|
2532
2596
|
}
|
|
2597
|
+
if (showdown and volatiles.Toxic) {
|
|
2598
|
+
volatiles.Toxic = false;
|
|
2599
|
+
// Pokémon Showdown erroneously clears the toxic counter
|
|
2600
|
+
volatiles.toxic = 0;
|
|
2601
|
+
try log.end(ident, .Toxic);
|
|
2602
|
+
}
|
|
2533
2603
|
}
|
|
2534
2604
|
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2605
|
+
pub const Rolls = struct {
|
|
2606
|
+
inline fn speedTie(battle: anytype) bool {
|
|
2607
|
+
if (showdown) return battle.rng.range(u8, 0, 2) == 0;
|
|
2608
|
+
return battle.rng.next() < Gen12.percent(50) + 1;
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2611
|
+
inline fn criticalHit(battle: anytype, chance: u16) bool {
|
|
2612
|
+
if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
|
|
2613
|
+
return std.math.rotl(u8, battle.rng.next(), 3) < chance;
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2616
|
+
inline fn damage(battle: anytype) u8 {
|
|
2617
|
+
if (showdown) return battle.rng.range(u8, 217, 256);
|
|
2618
|
+
while (true) {
|
|
2619
|
+
const r = std.math.rotr(u8, battle.rng.next(), 1);
|
|
2620
|
+
if (r >= 217) return r;
|
|
2543
2621
|
}
|
|
2544
2622
|
}
|
|
2545
|
-
return false;
|
|
2546
|
-
}
|
|
2547
2623
|
|
|
2548
|
-
fn
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2624
|
+
inline fn hit(battle: anytype, accuracy: u16) bool {
|
|
2625
|
+
if (showdown) return battle.rng.chance(u8, @intCast(u8, accuracy), 256);
|
|
2626
|
+
return battle.rng.next() < accuracy;
|
|
2627
|
+
}
|
|
2552
2628
|
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
else
|
|
2558
|
-
(battle.rng.next() & 3) + 2);
|
|
2629
|
+
inline fn confusionDuration(battle: anytype) u3 {
|
|
2630
|
+
if (showdown) return @intCast(u3, battle.rng.range(u8, 2, 6));
|
|
2631
|
+
return @intCast(u3, (battle.rng.next() & 3) + 2);
|
|
2632
|
+
}
|
|
2559
2633
|
|
|
2560
|
-
|
|
2561
|
-
|
|
2634
|
+
inline fn confused(battle: anytype) bool {
|
|
2635
|
+
if (showdown) return !battle.rng.chance(u8, 128, 256);
|
|
2636
|
+
return battle.rng.next() >= Gen12.percent(50) + 1;
|
|
2637
|
+
}
|
|
2562
2638
|
|
|
2563
|
-
|
|
2639
|
+
inline fn paralyzed(battle: anytype) bool {
|
|
2640
|
+
if (showdown) return battle.rng.chance(u8, 63, 256);
|
|
2641
|
+
return battle.rng.next() < Gen12.percent(25);
|
|
2642
|
+
}
|
|
2564
2643
|
|
|
2565
|
-
fn
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
}
|
|
2644
|
+
inline fn confusionChance(battle: anytype) bool {
|
|
2645
|
+
if (showdown) return battle.rng.chance(u8, 25, 256);
|
|
2646
|
+
return battle.rng.next() < Gen12.percent(10);
|
|
2647
|
+
}
|
|
2570
2648
|
|
|
2571
|
-
fn
|
|
2572
|
-
|
|
2573
|
-
if (
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2649
|
+
inline fn secondaryChance(battle: anytype, low: bool) bool {
|
|
2650
|
+
if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 26 else 77), 256);
|
|
2651
|
+
return battle.rng.next() < 1 + (if (low) Gen12.percent(10) else Gen12.percent(30));
|
|
2652
|
+
}
|
|
2653
|
+
|
|
2654
|
+
inline fn poisonChance(battle: anytype, low: bool) bool {
|
|
2655
|
+
if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 52 else 103), 256);
|
|
2656
|
+
return battle.rng.next() < 1 + (if (low) Gen12.percent(20) else Gen12.percent(40));
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2659
|
+
inline fn sleepDuration(battle: anytype) u3 {
|
|
2660
|
+
if (showdown) return @intCast(u3, battle.rng.range(u8, 1, 8));
|
|
2661
|
+
while (true) {
|
|
2662
|
+
const r = battle.rng.next() & 7;
|
|
2663
|
+
if (r != 0) return @intCast(u3, r);
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
|
|
2667
|
+
inline fn bideThrashDuration(battle: anytype) u3 {
|
|
2668
|
+
if (showdown) return @intCast(u3, battle.rng.range(u4, 2, 4));
|
|
2669
|
+
return @intCast(u3, (battle.rng.next() & 1) + 2);
|
|
2670
|
+
}
|
|
2671
|
+
|
|
2672
|
+
fn moveSlot(battle: anytype, moves: []MoveSlot, check_pp: u4) u4 {
|
|
2673
|
+
if (showdown) {
|
|
2674
|
+
if (check_pp == 0) {
|
|
2675
|
+
var i: usize = moves.len;
|
|
2676
|
+
while (i > 0) {
|
|
2677
|
+
i -= 1;
|
|
2678
|
+
if (moves[i].id != .None) {
|
|
2679
|
+
return battle.rng.range(u4, 0, @intCast(u4, i + 1)) + 1;
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
} else {
|
|
2683
|
+
var r = battle.rng.range(u4, 0, @intCast(u4, check_pp)) + 1;
|
|
2684
|
+
var i: usize = 0;
|
|
2685
|
+
while (i < moves.len and r > 0) : (i += 1) {
|
|
2686
|
+
if (moves[i].pp > 0) {
|
|
2687
|
+
r -= 1;
|
|
2688
|
+
if (r == 0) break;
|
|
2689
|
+
}
|
|
2586
2690
|
}
|
|
2691
|
+
return @intCast(u4, i + 1);
|
|
2587
2692
|
}
|
|
2588
|
-
|
|
2693
|
+
}
|
|
2694
|
+
|
|
2695
|
+
while (true) {
|
|
2696
|
+
const r = @intCast(u4, battle.rng.next() & 3);
|
|
2697
|
+
if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
|
|
2589
2698
|
}
|
|
2590
2699
|
}
|
|
2591
2700
|
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2701
|
+
inline fn disableDuration(battle: anytype) u4 {
|
|
2702
|
+
if (showdown) return @intCast(u4, battle.rng.range(u8, 1, 9));
|
|
2703
|
+
return @intCast(u4, (battle.rng.next() & 7) + 1);
|
|
2595
2704
|
}
|
|
2596
|
-
|
|
2705
|
+
|
|
2706
|
+
inline fn metronome(battle: anytype) Move {
|
|
2707
|
+
if (showdown) {
|
|
2708
|
+
const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
|
|
2709
|
+
const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
|
|
2710
|
+
return @intToEnum(Move, r + mod);
|
|
2711
|
+
}
|
|
2712
|
+
while (true) {
|
|
2713
|
+
const r = battle.rng.next();
|
|
2714
|
+
if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
|
|
2715
|
+
if (r >= @enumToInt(Move.Struggle)) continue;
|
|
2716
|
+
return @intToEnum(Move, r);
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
|
|
2720
|
+
inline fn psywave(battle: anytype, max: u8) u8 {
|
|
2721
|
+
if (showdown) return battle.rng.range(u8, 0, max);
|
|
2722
|
+
while (true) {
|
|
2723
|
+
const r = battle.rng.next();
|
|
2724
|
+
if (r < max) return r;
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
|
|
2728
|
+
inline fn distribution(battle: anytype) u3 {
|
|
2729
|
+
if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
|
|
2730
|
+
const r = (battle.rng.next() & 3);
|
|
2731
|
+
return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
|
|
2732
|
+
}
|
|
2733
|
+
|
|
2734
|
+
inline fn unboost(battle: anytype) bool {
|
|
2735
|
+
if (showdown) return battle.rng.chance(u8, 85, 256);
|
|
2736
|
+
return battle.rng.next() < Gen12.percent(33) + 1;
|
|
2737
|
+
}
|
|
2738
|
+
};
|
|
2597
2739
|
|
|
2598
2740
|
test "RNG agreement" {
|
|
2599
2741
|
if (!showdown) return;
|
|
@@ -2693,9 +2835,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
2693
2835
|
while (slot <= 4) : (slot += 1) {
|
|
2694
2836
|
const m = active.moves[slot - 1];
|
|
2695
2837
|
if (m.id == .None) break;
|
|
2696
|
-
if (m.id == side.last_selected_move) {
|
|
2697
|
-
// Pokémon Showdown displays Struggle if limited to Bide
|
|
2698
|
-
const
|
|
2838
|
+
if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
|
|
2839
|
+
// Pokémon Showdown displays Struggle if limited to Bide but unable to pick
|
|
2840
|
+
const struggle =
|
|
2841
|
+
m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);
|
|
2842
|
+
const s = if (struggle) 0 else slot;
|
|
2699
2843
|
out[n] = .{ .type = .Move, .data = s };
|
|
2700
2844
|
n += 1;
|
|
2701
2845
|
return n;
|