@pkmn/engine 0.1.0-dev.63cef311 → 0.1.0-dev.7b099504
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 +51 -23
- 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/pkg/protocol.js +1 -1
- package/build/pkg/protocol.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 +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 +65 -39
- package/src/lib/gen1/data.zig +6 -6
- package/src/lib/gen1/helpers.zig +10 -11
- package/src/lib/gen1/mechanics.zig +416 -301
- 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,
|
|
@@ -442,14 +483,23 @@ fn executeMove(
|
|
|
442
483
|
.err => return @as(?Result, Result.Error),
|
|
443
484
|
}
|
|
444
485
|
|
|
445
|
-
if (!skip_can and !try canMove(battle, player, mslot, auto, skip_pp, from, log))
|
|
486
|
+
if (!skip_can and !try canMove(battle, player, mslot, auto, skip_pp, from, residual, log)) {
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
446
489
|
|
|
447
|
-
return doMove(battle, player, mslot, auto, residual, log);
|
|
490
|
+
return doMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
448
491
|
}
|
|
449
492
|
|
|
450
|
-
const BeforeMove =
|
|
493
|
+
const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
|
|
451
494
|
|
|
452
|
-
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 {
|
|
453
503
|
var side = battle.side(player);
|
|
454
504
|
const foe = battle.foe(player);
|
|
455
505
|
var active = &side.active;
|
|
@@ -477,13 +527,15 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
|
|
|
477
527
|
return .done;
|
|
478
528
|
}
|
|
479
529
|
|
|
480
|
-
if (foe.active.volatiles.Binding) {
|
|
530
|
+
if (skip or foe.active.volatiles.Binding) {
|
|
481
531
|
try log.cant(ident, .Bound);
|
|
482
532
|
return .done;
|
|
483
533
|
}
|
|
484
534
|
|
|
485
|
-
if (
|
|
486
|
-
|
|
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;
|
|
487
539
|
try log.cant(ident, .Flinch);
|
|
488
540
|
return .done;
|
|
489
541
|
}
|
|
@@ -504,13 +556,6 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
|
|
|
504
556
|
// Pokémon Showdown's disable condition has a single onBeforeMove handler
|
|
505
557
|
if (showdown and try disabled(side, ident, log)) return .done;
|
|
506
558
|
|
|
507
|
-
// Pokémon Showdown checks for Flinch *after* instead of before
|
|
508
|
-
if (showdown and volatiles.Flinch) {
|
|
509
|
-
volatiles.Flinch = false;
|
|
510
|
-
try log.cant(ident, .Flinch);
|
|
511
|
-
return .done;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
559
|
// This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
|
|
515
560
|
// before the side had a selected a move - we simply need to assume this leads to a desync
|
|
516
561
|
if (side.last_selected_move == .None) {
|
|
@@ -528,14 +573,9 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
|
|
|
528
573
|
} else {
|
|
529
574
|
try log.activate(ident, .Confusion);
|
|
530
575
|
|
|
531
|
-
|
|
532
|
-
!battle.rng.chance(u8, 128, 256)
|
|
533
|
-
else
|
|
534
|
-
battle.rng.next() >= Gen12.percent(50) + 1;
|
|
535
|
-
|
|
536
|
-
if (confused) {
|
|
576
|
+
if (Rolls.confused(battle)) {
|
|
537
577
|
assert(!volatiles.MultiHit);
|
|
538
|
-
volatiles.state = 0;
|
|
578
|
+
if (!volatiles.Rage) volatiles.state = 0;
|
|
539
579
|
volatiles.Bide = false;
|
|
540
580
|
volatiles.Thrashing = false;
|
|
541
581
|
volatiles.MultiHit = false;
|
|
@@ -559,8 +599,12 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
|
|
|
559
599
|
player
|
|
560
600
|
else
|
|
561
601
|
player.foe();
|
|
602
|
+
|
|
603
|
+
const uncapped = battle.last_damage;
|
|
562
604
|
// Skipping adjustDamage / randomizeDamage / checkHit
|
|
563
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;
|
|
564
608
|
|
|
565
609
|
return .done;
|
|
566
610
|
}
|
|
@@ -569,22 +613,15 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
|
|
|
569
613
|
|
|
570
614
|
if (!showdown and try disabled(side, ident, log)) return .done;
|
|
571
615
|
|
|
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
|
-
}
|
|
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;
|
|
588
625
|
}
|
|
589
626
|
|
|
590
627
|
if (volatiles.Bide) {
|
|
@@ -611,7 +648,18 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
|
|
|
611
648
|
return .done;
|
|
612
649
|
}
|
|
613
650
|
|
|
651
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
614
652
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, 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
|
+
}
|
|
662
|
+
|
|
615
663
|
return .done;
|
|
616
664
|
}
|
|
617
665
|
|
|
@@ -623,8 +671,13 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
|
|
|
623
671
|
try log.start(battle.active(player), .ConfusionSilent);
|
|
624
672
|
}
|
|
625
673
|
try log.move(ident, side.last_selected_move, battle.active(player.foe()), from);
|
|
626
|
-
if (showdown
|
|
627
|
-
|
|
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
|
+
}
|
|
628
681
|
}
|
|
629
682
|
return .skip_can;
|
|
630
683
|
}
|
|
@@ -639,7 +692,11 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
|
|
|
639
692
|
side.last_selected_move,
|
|
640
693
|
);
|
|
641
694
|
if (showdown or battle.last_damage != 0) {
|
|
695
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
642
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
|
+
}
|
|
643
700
|
}
|
|
644
701
|
return .done;
|
|
645
702
|
}
|
|
@@ -654,6 +711,7 @@ fn canMove(
|
|
|
654
711
|
auto: bool,
|
|
655
712
|
skip_pp: bool,
|
|
656
713
|
from: ?Move,
|
|
714
|
+
residual: *bool,
|
|
657
715
|
log: anytype,
|
|
658
716
|
) !bool {
|
|
659
717
|
var side = battle.side(player);
|
|
@@ -688,7 +746,7 @@ fn canMove(
|
|
|
688
746
|
try log.move(player_ident, side.last_selected_move, battle.active(target), f);
|
|
689
747
|
|
|
690
748
|
if (move.effect.onBegin()) {
|
|
691
|
-
try onBegin(battle, player, move, mslot, log);
|
|
749
|
+
try onBegin(battle, player, move, mslot, residual, log);
|
|
692
750
|
return false;
|
|
693
751
|
}
|
|
694
752
|
|
|
@@ -696,7 +754,7 @@ fn canMove(
|
|
|
696
754
|
Effects.thrashing(battle, player);
|
|
697
755
|
} else if (!showdown and move.effect == .Binding) {
|
|
698
756
|
// Pokémon Showdown handles this after hit/miss checks and damage calculation
|
|
699
|
-
Effects.binding(battle, player);
|
|
757
|
+
Effects.binding(battle, player, false);
|
|
700
758
|
}
|
|
701
759
|
|
|
702
760
|
return true;
|
|
@@ -737,6 +795,7 @@ fn doMove(
|
|
|
737
795
|
battle: anytype,
|
|
738
796
|
player: Player,
|
|
739
797
|
mslot: u4,
|
|
798
|
+
rewrap: bool,
|
|
740
799
|
auto: bool,
|
|
741
800
|
residual: *bool,
|
|
742
801
|
log: anytype,
|
|
@@ -767,16 +826,21 @@ fn doMove(
|
|
|
767
826
|
(@enumToInt(move.type.effectiveness(foe.active.types.type1)) == 0 or
|
|
768
827
|
@enumToInt(move.type.effectiveness(foe.active.types.type2)) == 0);
|
|
769
828
|
if (immune and move.effect != .Binding) break :miss true;
|
|
770
|
-
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
|
+
}
|
|
771
833
|
break :miss move.target != .Self and !moveHit(battle, player, move, &immune, &mist);
|
|
772
834
|
};
|
|
773
835
|
assert(!immune or miss or (showdown and move.effect == .Binding));
|
|
774
836
|
|
|
837
|
+
var late = showdown and move.effect != .Explode;
|
|
775
838
|
const skip = status or immune;
|
|
776
839
|
if ((!showdown or (!skip or counter)) and !miss) blk: {
|
|
777
840
|
if (showdown and move.effect.isMulti()) {
|
|
778
841
|
Effects.multiHit(battle, player, move);
|
|
779
842
|
hits = side.active.volatiles.attacks;
|
|
843
|
+
late = false;
|
|
780
844
|
}
|
|
781
845
|
|
|
782
846
|
// Cartridge rolls for crit even for moves that can't crit (Counter/Metronome/status/OHKO)
|
|
@@ -810,7 +874,7 @@ fn doMove(
|
|
|
810
874
|
}
|
|
811
875
|
|
|
812
876
|
// Due to control flow shenanigans we need to clear last_damage for Pokémon Showdown
|
|
813
|
-
if (showdown and skip
|
|
877
|
+
if (showdown and skip) battle.last_damage = 0;
|
|
814
878
|
|
|
815
879
|
miss = if (showdown or skip)
|
|
816
880
|
miss
|
|
@@ -823,9 +887,9 @@ fn doMove(
|
|
|
823
887
|
|
|
824
888
|
if (!showdown or !miss) {
|
|
825
889
|
if (move.effect == .MirrorMove) {
|
|
826
|
-
return mirrorMove(battle, player, mslot, auto, residual, log);
|
|
890
|
+
return mirrorMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
827
891
|
} else if (move.effect == .Metronome) {
|
|
828
|
-
return metronome(battle, player, mslot, auto, residual, log);
|
|
892
|
+
return metronome(battle, player, mslot, rewrap, auto, residual, log);
|
|
829
893
|
} else if (move.effect.onEnd()) {
|
|
830
894
|
try onEnd(battle, player, move, log);
|
|
831
895
|
return null;
|
|
@@ -840,7 +904,7 @@ fn doMove(
|
|
|
840
904
|
move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
|
|
841
905
|
if (ohko) {
|
|
842
906
|
try log.immune(foe_ident, .OHKO);
|
|
843
|
-
} else if (immune and !invulnerable) {
|
|
907
|
+
} else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
|
|
844
908
|
try log.immune(foe_ident, .None);
|
|
845
909
|
} else if (mist) {
|
|
846
910
|
if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
|
|
@@ -857,15 +921,9 @@ fn doMove(
|
|
|
857
921
|
if (showdown and side.stored().hp == 0) residual.* = false;
|
|
858
922
|
} else if (move.effect == .Explode) {
|
|
859
923
|
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
|
-
}
|
|
924
|
+
try buildRage(battle, player.foe(), log);
|
|
925
|
+
} else if (showdown and move.effect == .Disable) {
|
|
926
|
+
try buildRage(battle, player.foe(), log);
|
|
869
927
|
}
|
|
870
928
|
return null;
|
|
871
929
|
}
|
|
@@ -878,6 +936,9 @@ fn doMove(
|
|
|
878
936
|
hits = side.active.volatiles.attacks;
|
|
879
937
|
}
|
|
880
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
|
+
|
|
881
942
|
var nullified = false;
|
|
882
943
|
var hit: u4 = 0;
|
|
883
944
|
while (hit < hits) {
|
|
@@ -893,9 +954,7 @@ fn doMove(
|
|
|
893
954
|
if (hit == 0 and ohko) try log.ohko();
|
|
894
955
|
hit += 1;
|
|
895
956
|
if (foe.stored().hp == 0) break;
|
|
896
|
-
if (
|
|
897
|
-
try Effects.boost(battle, player.foe(), Move.get(.Rage), log);
|
|
898
|
-
}
|
|
957
|
+
if (!late and (!sub or move.effect == .Explode)) try buildRage(battle, player.foe(), log);
|
|
899
958
|
// If the substitute breaks during a multi-hit attack, the attack ends
|
|
900
959
|
if (nullified) break;
|
|
901
960
|
}
|
|
@@ -911,11 +970,17 @@ fn doMove(
|
|
|
911
970
|
} else if (showdown) {
|
|
912
971
|
// This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
|
|
913
972
|
if (move.effect == .Binding) {
|
|
914
|
-
Effects.binding(battle, player);
|
|
973
|
+
Effects.binding(battle, player, rewrap);
|
|
915
974
|
if (immune) {
|
|
916
975
|
battle.last_damage = 0;
|
|
976
|
+
assert(foe.stored().hp > 0);
|
|
917
977
|
// Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
|
|
918
|
-
|
|
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
|
+
}
|
|
919
984
|
return null;
|
|
920
985
|
}
|
|
921
986
|
}
|
|
@@ -932,6 +997,9 @@ fn doMove(
|
|
|
932
997
|
|
|
933
998
|
if (foe.stored().hp == 0) return null;
|
|
934
999
|
|
|
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);
|
|
1002
|
+
|
|
935
1003
|
if (!move.effect.isSpecial()) {
|
|
936
1004
|
// On the cartridge Rage is not considered to be "special" and thus gets executed for a
|
|
937
1005
|
// second time here (after being executed in the "always happens" block above) but that
|
|
@@ -943,7 +1011,9 @@ fn doMove(
|
|
|
943
1011
|
if (move.effect == .Twineedle) {
|
|
944
1012
|
if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), log);
|
|
945
1013
|
} else if (move.effect == .Disable) {
|
|
946
|
-
|
|
1014
|
+
const result = try Effects.disable(battle, player, move, log);
|
|
1015
|
+
if (showdown) try buildRage(battle, player.foe(), log);
|
|
1016
|
+
return result;
|
|
947
1017
|
} else {
|
|
948
1018
|
try moveEffect(battle, player, move, log);
|
|
949
1019
|
}
|
|
@@ -961,8 +1031,7 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
|
|
|
961
1031
|
chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
|
|
962
1032
|
chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
|
|
963
1033
|
|
|
964
|
-
|
|
965
|
-
return std.math.rotl(u8, battle.rng.next(), 3) < chance;
|
|
1034
|
+
return Rolls.criticalHit(battle, chance);
|
|
966
1035
|
}
|
|
967
1036
|
|
|
968
1037
|
fn calcDamage(
|
|
@@ -1066,49 +1135,37 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
|
1066
1135
|
|
|
1067
1136
|
fn randomizeDamage(battle: anytype) void {
|
|
1068
1137
|
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
|
-
|
|
1138
|
+
const random = Rolls.damage(battle);
|
|
1079
1139
|
battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
|
|
1080
1140
|
}
|
|
1081
1141
|
|
|
1082
1142
|
fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
|
|
1083
1143
|
const side = battle.side(player);
|
|
1144
|
+
const foe = battle.foe(player);
|
|
1084
1145
|
|
|
1085
1146
|
if (!try checkHit(battle, player, move, log)) return null;
|
|
1086
1147
|
|
|
1087
1148
|
battle.last_damage = switch (side.last_selected_move) {
|
|
1088
|
-
.SuperFang => @max(
|
|
1149
|
+
.SuperFang => @max(foe.stored().hp / 2, 1),
|
|
1089
1150
|
.SeismicToss, .NightShade => side.stored().level,
|
|
1090
1151
|
.SonicBoom => 20,
|
|
1091
1152
|
.DragonRage => 40,
|
|
1092
1153
|
// GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
|
|
1093
1154
|
.Psywave => power: {
|
|
1094
1155
|
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
|
-
}
|
|
1156
|
+
// GLITCH: Psywave infinite glitch loop
|
|
1157
|
+
if (!showdown and max <= 1) return Result.Error;
|
|
1158
|
+
break :power Rolls.psywave(battle, max);
|
|
1105
1159
|
},
|
|
1106
1160
|
else => unreachable,
|
|
1107
1161
|
};
|
|
1108
1162
|
|
|
1109
1163
|
if (battle.last_damage == 0) return if (showdown) null else Result.Error;
|
|
1110
1164
|
|
|
1165
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1111
1166
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
1167
|
+
if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
|
|
1168
|
+
|
|
1112
1169
|
return null;
|
|
1113
1170
|
}
|
|
1114
1171
|
|
|
@@ -1156,7 +1213,9 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
|
|
|
1156
1213
|
// Pokémon Showdown calls checkHit before Counter
|
|
1157
1214
|
if (!showdown and !try checkHit(battle, player, move, log)) return null;
|
|
1158
1215
|
|
|
1216
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1159
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);
|
|
1160
1219
|
return null;
|
|
1161
1220
|
}
|
|
1162
1221
|
|
|
@@ -1200,6 +1259,7 @@ fn mirrorMove(
|
|
|
1200
1259
|
battle: anytype,
|
|
1201
1260
|
player: Player,
|
|
1202
1261
|
mslot: u4,
|
|
1262
|
+
rewrap: bool,
|
|
1203
1263
|
auto: bool,
|
|
1204
1264
|
residual: *bool,
|
|
1205
1265
|
log: anytype,
|
|
@@ -1212,41 +1272,30 @@ fn mirrorMove(
|
|
|
1212
1272
|
if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
|
|
1213
1273
|
try log.fail(battle.active(player), .None);
|
|
1214
1274
|
return null;
|
|
1275
|
+
} else if (!showdown or foe.last_used_move != .Struggle) {
|
|
1276
|
+
incrementPP(side, mslot);
|
|
1215
1277
|
}
|
|
1216
1278
|
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, log)) return null;
|
|
1220
|
-
return doMove(battle, player, mslot, auto, residual, log);
|
|
1279
|
+
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
|
|
1280
|
+
return doMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
1221
1281
|
}
|
|
1222
1282
|
|
|
1223
1283
|
fn metronome(
|
|
1224
1284
|
battle: anytype,
|
|
1225
1285
|
player: Player,
|
|
1226
1286
|
mslot: u4,
|
|
1287
|
+
rewrap: bool,
|
|
1227
1288
|
auto: bool,
|
|
1228
1289
|
residual: *bool,
|
|
1229
1290
|
log: anytype,
|
|
1230
1291
|
) !?Result {
|
|
1231
1292
|
var side = battle.side(player);
|
|
1232
1293
|
|
|
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
|
-
|
|
1294
|
+
side.last_selected_move = Rolls.metronome(battle);
|
|
1246
1295
|
incrementPP(side, mslot);
|
|
1247
1296
|
|
|
1248
|
-
if (!try canMove(battle, player, mslot, auto, false, .Metronome, log)) return null;
|
|
1249
|
-
return doMove(battle, player, mslot, auto, residual, log);
|
|
1297
|
+
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
|
|
1298
|
+
return doMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
1250
1299
|
}
|
|
1251
1300
|
|
|
1252
1301
|
fn checkHit(battle: anytype, player: Player, move: Move.Data, log: anytype) !bool {
|
|
@@ -1300,23 +1349,24 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
|
|
|
1300
1349
|
}
|
|
1301
1350
|
|
|
1302
1351
|
// GLITCH: Thrash / Petal Dance / Rage get their accuracy overwritten on subsequent hits
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
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));
|
|
1307
1358
|
var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
|
|
1308
1359
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1309
1360
|
boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
|
|
1310
1361
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1311
1362
|
accuracy = @min(255, @max(1, accuracy));
|
|
1312
1363
|
|
|
1313
|
-
|
|
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;
|
|
1314
1367
|
|
|
1315
1368
|
// GLITCH: max accuracy is 255 so 1/256 chance of miss
|
|
1316
|
-
break :miss
|
|
1317
|
-
!battle.rng.chance(u8, @intCast(u8, accuracy), 256)
|
|
1318
|
-
else
|
|
1319
|
-
battle.rng.next() >= accuracy;
|
|
1369
|
+
break :miss !Rolls.hit(battle, accuracy);
|
|
1320
1370
|
};
|
|
1321
1371
|
|
|
1322
1372
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1431,6 +1481,15 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
|
|
|
1431
1481
|
}
|
|
1432
1482
|
|
|
1433
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
|
+
|
|
1434
1493
|
var damage = @max(stored.stats.hp / 16, 1);
|
|
1435
1494
|
|
|
1436
1495
|
// GLITCH: Leech Seed + Toxic glitch
|
|
@@ -1442,10 +1501,6 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
|
|
|
1442
1501
|
const amount = @min(damage, stored.hp);
|
|
1443
1502
|
stored.hp -= amount;
|
|
1444
1503
|
|
|
1445
|
-
var foe = battle.foe(player);
|
|
1446
|
-
var foe_stored = foe.stored();
|
|
1447
|
-
const foe_ident = battle.active(player.foe());
|
|
1448
|
-
|
|
1449
1504
|
// As above, Pokémon Showdown uses damageOf but its not relevant
|
|
1450
1505
|
if (amount > 0) try log.damage(ident, stored, .LeechSeed);
|
|
1451
1506
|
|
|
@@ -1534,7 +1589,50 @@ fn checkEBC(battle: anytype) bool {
|
|
|
1534
1589
|
return false;
|
|
1535
1590
|
}
|
|
1536
1591
|
|
|
1537
|
-
|
|
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
|
+
|
|
1628
|
+
inline fn onBegin(
|
|
1629
|
+
battle: anytype,
|
|
1630
|
+
player: Player,
|
|
1631
|
+
move: Move.Data,
|
|
1632
|
+
mslot: u8,
|
|
1633
|
+
residual: *bool,
|
|
1634
|
+
log: anytype,
|
|
1635
|
+
) !void {
|
|
1538
1636
|
assert(move.effect.onBegin());
|
|
1539
1637
|
return switch (move.effect) {
|
|
1540
1638
|
.Confusion => Effects.confusion(battle, player, move, log),
|
|
@@ -1550,7 +1648,7 @@ inline fn onBegin(battle: anytype, player: Player, move: Move.Data, mslot: u8, l
|
|
|
1550
1648
|
.Poison => Effects.poison(battle, player, move, log),
|
|
1551
1649
|
.Reflect => Effects.reflect(battle, player, log),
|
|
1552
1650
|
.Splash => Effects.splash(battle, player, log),
|
|
1553
|
-
.Substitute => Effects.substitute(battle, player, log),
|
|
1651
|
+
.Substitute => Effects.substitute(battle, player, residual, log),
|
|
1554
1652
|
.SwitchAndTeleport => Effects.switchAndTeleport(battle, player, move, log),
|
|
1555
1653
|
.Transform => Effects.transform(battle, player, log),
|
|
1556
1654
|
else => unreachable,
|
|
@@ -1617,10 +1715,7 @@ pub const Effects = struct {
|
|
|
1617
1715
|
side.active.volatiles.Bide = true;
|
|
1618
1716
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1619
1717
|
side.active.volatiles.state = 0;
|
|
1620
|
-
side.active.volatiles.attacks =
|
|
1621
|
-
battle.rng.range(u4, 2, 4)
|
|
1622
|
-
else
|
|
1623
|
-
(battle.rng.next() & 1) + 2);
|
|
1718
|
+
side.active.volatiles.attacks = Rolls.bideThrashDuration(battle);
|
|
1624
1719
|
|
|
1625
1720
|
try log.start(battle.active(player), .Bide);
|
|
1626
1721
|
}
|
|
@@ -1642,14 +1737,8 @@ pub const Effects = struct {
|
|
|
1642
1737
|
return;
|
|
1643
1738
|
}
|
|
1644
1739
|
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
else
|
|
1648
|
-
battle.rng.next() < 1 + (if (move.effect == .BurnChance1)
|
|
1649
|
-
Gen12.percent(10)
|
|
1650
|
-
else
|
|
1651
|
-
Gen12.percent(30));
|
|
1652
|
-
if (!chance) return;
|
|
1740
|
+
if (foe.active.types.includes(move.type)) return;
|
|
1741
|
+
if (!Rolls.secondaryChance(battle, move.effect == .BurnChance1)) return;
|
|
1653
1742
|
|
|
1654
1743
|
foe_stored.status = Status.init(.BRN);
|
|
1655
1744
|
foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
|
|
@@ -1673,11 +1762,7 @@ pub const Effects = struct {
|
|
|
1673
1762
|
const sub = foe.active.volatiles.Substitute;
|
|
1674
1763
|
|
|
1675
1764
|
if (move.effect == .ConfusionChance) {
|
|
1676
|
-
|
|
1677
|
-
battle.rng.chance(u8, 25, 256)
|
|
1678
|
-
else
|
|
1679
|
-
battle.rng.next() < Gen12.percent(10);
|
|
1680
|
-
if (!chance) return;
|
|
1765
|
+
if (!Rolls.confusionChance(battle)) return;
|
|
1681
1766
|
} else {
|
|
1682
1767
|
if (showdown) {
|
|
1683
1768
|
if (!try checkHit(battle, player, move, log)) {
|
|
@@ -1696,10 +1781,7 @@ pub const Effects = struct {
|
|
|
1696
1781
|
|
|
1697
1782
|
if (foe.active.volatiles.Confusion) return;
|
|
1698
1783
|
foe.active.volatiles.Confusion = true;
|
|
1699
|
-
foe.active.volatiles.confusion =
|
|
1700
|
-
battle.rng.range(u8, 2, 6)
|
|
1701
|
-
else
|
|
1702
|
-
(battle.rng.next() & 3) + 2);
|
|
1784
|
+
foe.active.volatiles.confusion = Rolls.confusionDuration(battle);
|
|
1703
1785
|
|
|
1704
1786
|
try log.start(battle.active(player.foe()), .Confusion);
|
|
1705
1787
|
}
|
|
@@ -1749,11 +1831,8 @@ pub const Effects = struct {
|
|
|
1749
1831
|
return Result.Error;
|
|
1750
1832
|
}
|
|
1751
1833
|
|
|
1752
|
-
volatiles.disabled_move = @intCast(u3,
|
|
1753
|
-
volatiles.disabled_duration =
|
|
1754
|
-
battle.rng.range(u8, 1, 9)
|
|
1755
|
-
else
|
|
1756
|
-
(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);
|
|
1757
1836
|
|
|
1758
1837
|
try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
|
|
1759
1838
|
return null;
|
|
@@ -1765,7 +1844,7 @@ pub const Effects = struct {
|
|
|
1765
1844
|
|
|
1766
1845
|
if (battle.last_damage == 0) {
|
|
1767
1846
|
assert(showdown);
|
|
1768
|
-
return;
|
|
1847
|
+
if (!battle.foe(player).active.volatiles.Substitute) return;
|
|
1769
1848
|
}
|
|
1770
1849
|
|
|
1771
1850
|
const drain = @max(battle.last_damage / 2, 1);
|
|
@@ -1792,15 +1871,7 @@ pub const Effects = struct {
|
|
|
1792
1871
|
var volatiles = &battle.foe(player).active.volatiles;
|
|
1793
1872
|
|
|
1794
1873
|
if (volatiles.Substitute) return;
|
|
1795
|
-
|
|
1796
|
-
const chance = if (showdown)
|
|
1797
|
-
battle.rng.chance(u8, @as(u8, if (move.effect == .FlinchChance1) 26 else 77), 256)
|
|
1798
|
-
else
|
|
1799
|
-
battle.rng.next() < 1 + (if (move.effect == .FlinchChance1)
|
|
1800
|
-
Gen12.percent(10)
|
|
1801
|
-
else
|
|
1802
|
-
Gen12.percent(30));
|
|
1803
|
-
if (!chance) return;
|
|
1874
|
+
if (!Rolls.secondaryChance(battle, move.effect == .FlinchChance1)) return;
|
|
1804
1875
|
|
|
1805
1876
|
volatiles.Flinch = true;
|
|
1806
1877
|
volatiles.Recharging = false;
|
|
@@ -1826,12 +1897,8 @@ pub const Effects = struct {
|
|
|
1826
1897
|
return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
|
|
1827
1898
|
}
|
|
1828
1899
|
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
else
|
|
1832
|
-
battle.rng.next() < 1 + Gen12.percent(10);
|
|
1833
|
-
if (!chance) return;
|
|
1834
|
-
|
|
1900
|
+
if (foe.active.types.includes(move.type)) return;
|
|
1901
|
+
if (!Rolls.secondaryChance(battle, true)) return;
|
|
1835
1902
|
// Freeze Clause Mod
|
|
1836
1903
|
if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
1837
1904
|
|
|
@@ -1968,15 +2035,15 @@ pub const Effects = struct {
|
|
|
1968
2035
|
}
|
|
1969
2036
|
break :has_mimic false;
|
|
1970
2037
|
};
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
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;
|
|
1974
2041
|
return try log.fail(battle.active(player.foe()), .None);
|
|
1975
2042
|
}
|
|
1976
2043
|
}
|
|
1977
2044
|
if (!try checkHit(battle, player, move, log)) return;
|
|
1978
2045
|
|
|
1979
|
-
const rslot =
|
|
2046
|
+
const rslot = Rolls.moveSlot(battle, &foe.active.moves, 0);
|
|
1980
2047
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
1981
2048
|
|
|
1982
2049
|
try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
|
|
@@ -1997,7 +2064,8 @@ pub const Effects = struct {
|
|
|
1997
2064
|
assert(!side.active.volatiles.MultiHit);
|
|
1998
2065
|
side.active.volatiles.MultiHit = true;
|
|
1999
2066
|
|
|
2000
|
-
side.active.volatiles.attacks =
|
|
2067
|
+
side.active.volatiles.attacks =
|
|
2068
|
+
if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
|
|
2001
2069
|
}
|
|
2002
2070
|
|
|
2003
2071
|
fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2041,14 +2109,8 @@ pub const Effects = struct {
|
|
|
2041
2109
|
}
|
|
2042
2110
|
|
|
2043
2111
|
// Body Slam can't paralyze a Normal type Pokémon
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
else
|
|
2047
|
-
battle.rng.next() < 1 + (if (move.effect == .ParalyzeChance1)
|
|
2048
|
-
Gen12.percent(10)
|
|
2049
|
-
else
|
|
2050
|
-
Gen12.percent(30));
|
|
2051
|
-
if (!chance) return;
|
|
2112
|
+
if (foe.active.types.includes(move.type)) return;
|
|
2113
|
+
if (!Rolls.secondaryChance(battle, move.effect == .ParalyzeChance1)) return;
|
|
2052
2114
|
|
|
2053
2115
|
foe_stored.status = Status.init(.PAR);
|
|
2054
2116
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
@@ -2091,16 +2153,7 @@ pub const Effects = struct {
|
|
|
2091
2153
|
|
|
2092
2154
|
if (move.effect == .Poison) {
|
|
2093
2155
|
if (!showdown and !try checkHit(battle, player, move, log)) return;
|
|
2094
|
-
} else
|
|
2095
|
-
const chance = if (showdown)
|
|
2096
|
-
battle.rng.chance(u8, @as(u8, if (move.effect == .PoisonChance1) 52 else 103), 256)
|
|
2097
|
-
else
|
|
2098
|
-
battle.rng.next() < 1 + (if (move.effect == .PoisonChance1)
|
|
2099
|
-
Gen12.percent(20)
|
|
2100
|
-
else
|
|
2101
|
-
Gen12.percent(40));
|
|
2102
|
-
if (!chance) return;
|
|
2103
|
-
}
|
|
2156
|
+
} else if (!Rolls.poisonChance(battle, move.effect == .PoisonChance1)) return;
|
|
2104
2157
|
|
|
2105
2158
|
foe_stored.status = Status.init(.PSN);
|
|
2106
2159
|
if (toxic) {
|
|
@@ -2114,10 +2167,8 @@ pub const Effects = struct {
|
|
|
2114
2167
|
|
|
2115
2168
|
fn rage(battle: anytype, player: Player) !void {
|
|
2116
2169
|
var volatiles = &battle.side(player).active.volatiles;
|
|
2117
|
-
if (volatiles.Rage) return;
|
|
2118
|
-
volatiles.Rage = true;
|
|
2119
2170
|
assert(!volatiles.Bide);
|
|
2120
|
-
volatiles.
|
|
2171
|
+
volatiles.Rage = true;
|
|
2121
2172
|
}
|
|
2122
2173
|
|
|
2123
2174
|
fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
|
|
@@ -2169,16 +2220,7 @@ pub const Effects = struct {
|
|
|
2169
2220
|
}
|
|
2170
2221
|
foe.active.volatiles.Recharging = false;
|
|
2171
2222
|
|
|
2172
|
-
|
|
2173
|
-
battle.rng.range(u8, 1, 8)
|
|
2174
|
-
else loop: {
|
|
2175
|
-
while (true) {
|
|
2176
|
-
const r = battle.rng.next() & 7;
|
|
2177
|
-
if (r != 0) break :loop r;
|
|
2178
|
-
}
|
|
2179
|
-
});
|
|
2180
|
-
|
|
2181
|
-
foe_stored.status = Status.slp(duration);
|
|
2223
|
+
foe_stored.status = Status.slp(Rolls.sleepDuration(battle));
|
|
2182
2224
|
try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
|
|
2183
2225
|
}
|
|
2184
2226
|
|
|
@@ -2186,7 +2228,7 @@ pub const Effects = struct {
|
|
|
2186
2228
|
try log.activate(battle.active(player), .Splash);
|
|
2187
2229
|
}
|
|
2188
2230
|
|
|
2189
|
-
fn substitute(battle: anytype, player: Player, log: anytype) !void {
|
|
2231
|
+
fn substitute(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
|
|
2190
2232
|
var side = battle.side(player);
|
|
2191
2233
|
if (side.active.volatiles.Substitute) {
|
|
2192
2234
|
try log.fail(battle.active(player), .Substitute);
|
|
@@ -2211,14 +2253,18 @@ pub const Effects = struct {
|
|
|
2211
2253
|
side.active.volatiles.substitute = hp + 1;
|
|
2212
2254
|
side.active.volatiles.Substitute = true;
|
|
2213
2255
|
try log.start(battle.active(player), .Substitute);
|
|
2214
|
-
if (hp > 0)
|
|
2256
|
+
if (hp > 0) {
|
|
2257
|
+
try log.damage(battle.active(player), side.stored(), .None);
|
|
2258
|
+
if (showdown and side.stored().hp == 0) residual.* = false;
|
|
2259
|
+
}
|
|
2215
2260
|
}
|
|
2216
2261
|
|
|
2217
2262
|
fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
2218
2263
|
if (!showdown or battle.side(player).last_selected_move == .Teleport) return;
|
|
2219
2264
|
|
|
2220
|
-
// 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
|
|
2221
2266
|
_ = try checkHit(battle, player, move, log);
|
|
2267
|
+
battle.last_damage = 0;
|
|
2222
2268
|
}
|
|
2223
2269
|
|
|
2224
2270
|
fn thrashing(battle: anytype, player: Player) void {
|
|
@@ -2227,11 +2273,7 @@ pub const Effects = struct {
|
|
|
2227
2273
|
assert(!volatiles.Bide);
|
|
2228
2274
|
|
|
2229
2275
|
volatiles.Thrashing = true;
|
|
2230
|
-
volatiles.
|
|
2231
|
-
volatiles.attacks = @intCast(u3, if (showdown)
|
|
2232
|
-
battle.rng.range(u8, 2, 4)
|
|
2233
|
-
else
|
|
2234
|
-
(battle.rng.next() & 1) + 2);
|
|
2276
|
+
volatiles.attacks = Rolls.bideThrashDuration(battle);
|
|
2235
2277
|
}
|
|
2236
2278
|
|
|
2237
2279
|
fn transform(battle: anytype, player: Player, log: anytype) !void {
|
|
@@ -2263,7 +2305,7 @@ pub const Effects = struct {
|
|
|
2263
2305
|
try log.transform(battle.active(player), foe_ident);
|
|
2264
2306
|
}
|
|
2265
2307
|
|
|
2266
|
-
fn binding(battle: anytype, player: Player) void {
|
|
2308
|
+
fn binding(battle: anytype, player: Player, rewrap: bool) void {
|
|
2267
2309
|
var side = battle.side(player);
|
|
2268
2310
|
var foe = battle.foe(player);
|
|
2269
2311
|
|
|
@@ -2271,9 +2313,11 @@ pub const Effects = struct {
|
|
|
2271
2313
|
side.active.volatiles.Binding = true;
|
|
2272
2314
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2273
2315
|
// (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
|
|
2274
|
-
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);
|
|
2275
2319
|
|
|
2276
|
-
side.active.volatiles.attacks = distribution(battle) - 1;
|
|
2320
|
+
side.active.volatiles.attacks = Rolls.distribution(battle) - 1;
|
|
2277
2321
|
}
|
|
2278
2322
|
|
|
2279
2323
|
fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2300,9 +2344,11 @@ pub const Effects = struct {
|
|
|
2300
2344
|
return try log.fail(ident, .None);
|
|
2301
2345
|
}
|
|
2302
2346
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2303
|
-
const stat = unmodifiedStats(battle,
|
|
2347
|
+
const stat = unmodifiedStats(battle, side).atk;
|
|
2304
2348
|
stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2305
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;
|
|
2306
2352
|
},
|
|
2307
2353
|
.DefenseUp1, .DefenseUp2 => {
|
|
2308
2354
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
@@ -2318,7 +2364,7 @@ pub const Effects = struct {
|
|
|
2318
2364
|
return try log.fail(ident, .None);
|
|
2319
2365
|
}
|
|
2320
2366
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2321
|
-
const stat = unmodifiedStats(battle,
|
|
2367
|
+
const stat = unmodifiedStats(battle, side).def;
|
|
2322
2368
|
stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2323
2369
|
try log.boost(ident, .Defense, n);
|
|
2324
2370
|
},
|
|
@@ -2335,7 +2381,7 @@ pub const Effects = struct {
|
|
|
2335
2381
|
return try log.fail(ident, .None);
|
|
2336
2382
|
}
|
|
2337
2383
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2338
|
-
const stat = unmodifiedStats(battle,
|
|
2384
|
+
const stat = unmodifiedStats(battle, side).spe;
|
|
2339
2385
|
stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2340
2386
|
try log.boost(ident, .Speed, 2);
|
|
2341
2387
|
},
|
|
@@ -2355,7 +2401,7 @@ pub const Effects = struct {
|
|
|
2355
2401
|
return try log.fail(ident, .None);
|
|
2356
2402
|
}
|
|
2357
2403
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2358
|
-
const stat = unmodifiedStats(battle,
|
|
2404
|
+
const stat = unmodifiedStats(battle, side).spc;
|
|
2359
2405
|
stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2360
2406
|
try log.boost(ident, .SpecialAttack, n);
|
|
2361
2407
|
try log.boost(ident, .SpecialDefense, n);
|
|
@@ -2382,11 +2428,7 @@ pub const Effects = struct {
|
|
|
2382
2428
|
}
|
|
2383
2429
|
|
|
2384
2430
|
if (move.effect.isStatDownChance()) {
|
|
2385
|
-
|
|
2386
|
-
battle.rng.chance(u8, 85, 256)
|
|
2387
|
-
else
|
|
2388
|
-
battle.rng.next() < Gen12.percent(33) + 1;
|
|
2389
|
-
if (!chance or foe.active.volatiles.Invulnerable) return;
|
|
2431
|
+
if (!Rolls.unboost(battle) or foe.active.volatiles.Invulnerable) return;
|
|
2390
2432
|
} else if (!showdown and !try checkHit(battle, player, move, log)) {
|
|
2391
2433
|
return; // checkHit already checks for Invulnerable
|
|
2392
2434
|
}
|
|
@@ -2408,7 +2450,7 @@ pub const Effects = struct {
|
|
|
2408
2450
|
return try log.fail(foe_ident, .None);
|
|
2409
2451
|
}
|
|
2410
2452
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2411
|
-
const stat = unmodifiedStats(battle,
|
|
2453
|
+
const stat = unmodifiedStats(battle, foe).atk;
|
|
2412
2454
|
stats.atk = @max(1, stat * mod[0] / mod[1]);
|
|
2413
2455
|
try log.boost(foe_ident, .Attack, -1);
|
|
2414
2456
|
},
|
|
@@ -2426,7 +2468,7 @@ pub const Effects = struct {
|
|
|
2426
2468
|
return try log.fail(foe_ident, .None);
|
|
2427
2469
|
}
|
|
2428
2470
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2429
|
-
const stat = unmodifiedStats(battle,
|
|
2471
|
+
const stat = unmodifiedStats(battle, foe).def;
|
|
2430
2472
|
stats.def = @max(1, stat * mod[0] / mod[1]);
|
|
2431
2473
|
try log.boost(foe_ident, .Defense, -@as(i8, n));
|
|
2432
2474
|
},
|
|
@@ -2443,7 +2485,7 @@ pub const Effects = struct {
|
|
|
2443
2485
|
return try log.fail(foe_ident, .None);
|
|
2444
2486
|
}
|
|
2445
2487
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2446
|
-
const stat = unmodifiedStats(battle,
|
|
2488
|
+
const stat = unmodifiedStats(battle, foe).spe;
|
|
2447
2489
|
stats.spe = @max(1, stat * mod[0] / mod[1]);
|
|
2448
2490
|
try log.boost(foe_ident, .Speed, -1);
|
|
2449
2491
|
assert(boosts.spe >= -6);
|
|
@@ -2463,7 +2505,7 @@ pub const Effects = struct {
|
|
|
2463
2505
|
return try log.fail(foe_ident, .None);
|
|
2464
2506
|
}
|
|
2465
2507
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2466
|
-
const stat = unmodifiedStats(battle,
|
|
2508
|
+
const stat = unmodifiedStats(battle, foe).spc;
|
|
2467
2509
|
stats.spc = @max(1, stat * mod[0] / mod[1]);
|
|
2468
2510
|
try log.boost(foe_ident, .SpecialAttack, -1);
|
|
2469
2511
|
try log.boost(foe_ident, .SpecialDefense, -1);
|
|
@@ -2482,8 +2524,7 @@ pub const Effects = struct {
|
|
|
2482
2524
|
}
|
|
2483
2525
|
};
|
|
2484
2526
|
|
|
2485
|
-
fn unmodifiedStats(battle: anytype,
|
|
2486
|
-
const side = battle.side(who);
|
|
2527
|
+
fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
|
|
2487
2528
|
if (!side.active.volatiles.Transform) return &side.stored().stats;
|
|
2488
2529
|
const id = ID.from(side.active.volatiles.transform);
|
|
2489
2530
|
return &battle.side(id.player).pokemon[id.id - 1].stats;
|
|
@@ -2497,7 +2538,7 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
|
|
|
2497
2538
|
}
|
|
2498
2539
|
}
|
|
2499
2540
|
|
|
2500
|
-
fn isForced(active: ActivePokemon) bool {
|
|
2541
|
+
inline fn isForced(active: ActivePokemon) bool {
|
|
2501
2542
|
return active.volatiles.Recharging or active.volatiles.Rage or
|
|
2502
2543
|
active.volatiles.Thrashing or active.volatiles.Charging;
|
|
2503
2544
|
}
|
|
@@ -2549,68 +2590,140 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
|
|
|
2549
2590
|
}
|
|
2550
2591
|
}
|
|
2551
2592
|
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
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;
|
|
2560
2609
|
}
|
|
2561
2610
|
}
|
|
2562
|
-
return false;
|
|
2563
|
-
}
|
|
2564
2611
|
|
|
2565
|
-
fn
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
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;
|
|
2615
|
+
}
|
|
2569
2616
|
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
else
|
|
2575
|
-
(battle.rng.next() & 3) + 2);
|
|
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
|
+
}
|
|
2576
2621
|
|
|
2577
|
-
|
|
2578
|
-
|
|
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
|
+
}
|
|
2579
2626
|
|
|
2580
|
-
|
|
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
|
+
}
|
|
2581
2631
|
|
|
2582
|
-
fn
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
}
|
|
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
|
+
}
|
|
2587
2636
|
|
|
2588
|
-
fn
|
|
2589
|
-
|
|
2590
|
-
if (
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2637
|
+
inline fn secondaryChance(battle: anytype, low: bool) bool {
|
|
2638
|
+
if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 26 else 77), 256);
|
|
2639
|
+
return battle.rng.next() < 1 + (if (low) Gen12.percent(10) else Gen12.percent(30));
|
|
2640
|
+
}
|
|
2641
|
+
|
|
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
|
+
}
|
|
2603
2669
|
}
|
|
2670
|
+
} else {
|
|
2671
|
+
var r = battle.rng.range(u4, 0, @intCast(u4, check_pp)) + 1;
|
|
2672
|
+
var i: usize = 0;
|
|
2673
|
+
while (i < moves.len and r > 0) : (i += 1) {
|
|
2674
|
+
if (moves[i].pp > 0) {
|
|
2675
|
+
r -= 1;
|
|
2676
|
+
if (r == 0) break;
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
return @intCast(u4, i + 1);
|
|
2604
2680
|
}
|
|
2605
|
-
|
|
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;
|
|
2606
2686
|
}
|
|
2607
2687
|
}
|
|
2608
2688
|
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
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);
|
|
2612
2692
|
}
|
|
2613
|
-
|
|
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
|
+
};
|
|
2614
2727
|
|
|
2615
2728
|
test "RNG agreement" {
|
|
2616
2729
|
if (!showdown) return;
|
|
@@ -2710,9 +2823,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
2710
2823
|
while (slot <= 4) : (slot += 1) {
|
|
2711
2824
|
const m = active.moves[slot - 1];
|
|
2712
2825
|
if (m.id == .None) break;
|
|
2713
|
-
if (m.id == side.last_selected_move) {
|
|
2714
|
-
// Pokémon Showdown displays Struggle if limited to Bide
|
|
2715
|
-
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;
|
|
2716
2831
|
out[n] = .{ .type = .Move, .data = s };
|
|
2717
2832
|
n += 1;
|
|
2718
2833
|
return n;
|