@pkmn/engine 0.1.0-dev.7f37a47c → 0.1.0-dev.83de29ee
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/layout.json +1 -1
- 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 +5 -4
- package/build/pkg/gen1.js +35 -24
- package/build/pkg/gen1.js.map +1 -1
- package/build/pkg/index.d.ts +16 -13
- 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 +401 -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 +109 -78
- package/src/lib/gen1/data.zig +20 -18
- package/src/lib/gen1/helpers.zig +11 -17
- package/src/lib/gen1/mechanics.zig +477 -334
- 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) {
|
|
@@ -163,9 +180,9 @@ fn selectMove(
|
|
|
163
180
|
// GLITCH: https://glitchcity.wiki/Partial_trapping_move_Mirror_Move_link_battle_glitch
|
|
164
181
|
if (foe_choice.type == .Switch) {
|
|
165
182
|
const slot = if (player == .P1)
|
|
166
|
-
battle.
|
|
183
|
+
battle.last_moves.p1_index
|
|
167
184
|
else
|
|
168
|
-
battle.
|
|
185
|
+
battle.last_moves.p2_index;
|
|
169
186
|
const last = side.active.move(@intCast(u4, slot));
|
|
170
187
|
if (last.id == .Metronome) side.last_selected_move = last.id;
|
|
171
188
|
if (last.id == .MirrorMove) return Result.Error;
|
|
@@ -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| {
|
|
@@ -218,9 +237,9 @@ fn saveMove(battle: anytype, player: Player, choice: ?Choice) void {
|
|
|
218
237
|
|
|
219
238
|
side.last_selected_move = move.id;
|
|
220
239
|
if (player == .P1) {
|
|
221
|
-
battle.
|
|
240
|
+
battle.last_moves.p1_index = @intCast(u3, c.data);
|
|
222
241
|
} else {
|
|
223
|
-
battle.
|
|
242
|
+
battle.last_moves.p2_index = @intCast(u3, c.data);
|
|
224
243
|
}
|
|
225
244
|
}
|
|
226
245
|
}
|
|
@@ -240,9 +259,9 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, log: anyty
|
|
|
240
259
|
side.order[slot - 1] = out;
|
|
241
260
|
|
|
242
261
|
if (player == .P1) {
|
|
243
|
-
battle.
|
|
262
|
+
battle.last_moves.p1_index = 1;
|
|
244
263
|
} else {
|
|
245
|
-
battle.
|
|
264
|
+
battle.last_moves.p2_index = 1;
|
|
246
265
|
}
|
|
247
266
|
|
|
248
267
|
side.last_used_move = .None;
|
|
@@ -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);
|
|
@@ -403,9 +444,9 @@ fn executeMove(
|
|
|
403
444
|
// choice.data == 0 only happens with Struggle on Pokémon Showdown
|
|
404
445
|
assert(!showdown);
|
|
405
446
|
mslot = @intCast(u4, if (player == .P1)
|
|
406
|
-
battle.
|
|
447
|
+
battle.last_moves.p1_index
|
|
407
448
|
else
|
|
408
|
-
battle.
|
|
449
|
+
battle.last_moves.p2_index);
|
|
409
450
|
const stored = side.stored();
|
|
410
451
|
// GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
|
|
411
452
|
auto = side.last_selected_move == .HyperBeam or side.active.volatiles.Bide or
|
|
@@ -427,14 +468,14 @@ fn executeMove(
|
|
|
427
468
|
// Incorrect mslot with Pokémon Showdown choice semantics so we need to recover from index
|
|
428
469
|
assert(mslot == 1);
|
|
429
470
|
mslot = @intCast(u4, if (player == .P1)
|
|
430
|
-
battle.
|
|
471
|
+
battle.last_moves.p1_index
|
|
431
472
|
else
|
|
432
|
-
battle.
|
|
473
|
+
battle.last_moves.p2_index);
|
|
433
474
|
}
|
|
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);
|
|
@@ -687,8 +745,21 @@ fn canMove(
|
|
|
687
745
|
const f = if (from != null and from.? == .None) null else from;
|
|
688
746
|
try log.move(player_ident, side.last_selected_move, battle.active(target), f);
|
|
689
747
|
|
|
748
|
+
// The Counter desync is caused by the cartridge not calling GetCurrentMove until now, meaning
|
|
749
|
+
// in cases where an early return happens the data for a players last selected move does not get
|
|
750
|
+
// reloaded and HandleCounterMove actually bases its success/failure off of stale information.
|
|
751
|
+
// This boolean state we track here doesn't exist on the cartridge because it instead manifests
|
|
752
|
+
// as the desync results from actually having two separate battle states that subtly disagree.
|
|
753
|
+
const counterable = side.last_selected_move != .Counter and move.bp > 0 and
|
|
754
|
+
(move.type == .Normal or move.type == .Fighting);
|
|
755
|
+
if (player == .P1) {
|
|
756
|
+
battle.last_moves.p1_counterable = @boolToInt(counterable);
|
|
757
|
+
} else {
|
|
758
|
+
battle.last_moves.p2_counterable = @boolToInt(counterable);
|
|
759
|
+
}
|
|
760
|
+
|
|
690
761
|
if (move.effect.onBegin()) {
|
|
691
|
-
try onBegin(battle, player, move, mslot, log);
|
|
762
|
+
try onBegin(battle, player, move, mslot, residual, log);
|
|
692
763
|
return false;
|
|
693
764
|
}
|
|
694
765
|
|
|
@@ -696,7 +767,7 @@ fn canMove(
|
|
|
696
767
|
Effects.thrashing(battle, player);
|
|
697
768
|
} else if (!showdown and move.effect == .Binding) {
|
|
698
769
|
// Pokémon Showdown handles this after hit/miss checks and damage calculation
|
|
699
|
-
Effects.binding(battle, player);
|
|
770
|
+
Effects.binding(battle, player, false);
|
|
700
771
|
}
|
|
701
772
|
|
|
702
773
|
return true;
|
|
@@ -737,6 +808,7 @@ fn doMove(
|
|
|
737
808
|
battle: anytype,
|
|
738
809
|
player: Player,
|
|
739
810
|
mslot: u4,
|
|
811
|
+
rewrap: bool,
|
|
740
812
|
auto: bool,
|
|
741
813
|
residual: *bool,
|
|
742
814
|
log: anytype,
|
|
@@ -767,16 +839,21 @@ fn doMove(
|
|
|
767
839
|
(@enumToInt(move.type.effectiveness(foe.active.types.type1)) == 0 or
|
|
768
840
|
@enumToInt(move.type.effectiveness(foe.active.types.type2)) == 0);
|
|
769
841
|
if (immune and move.effect != .Binding) break :miss true;
|
|
770
|
-
if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe)
|
|
842
|
+
if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) {
|
|
843
|
+
battle.last_damage = 0;
|
|
844
|
+
break :miss true;
|
|
845
|
+
}
|
|
771
846
|
break :miss move.target != .Self and !moveHit(battle, player, move, &immune, &mist);
|
|
772
847
|
};
|
|
773
848
|
assert(!immune or miss or (showdown and move.effect == .Binding));
|
|
774
849
|
|
|
850
|
+
var late = showdown and move.effect != .Explode;
|
|
775
851
|
const skip = status or immune;
|
|
776
852
|
if ((!showdown or (!skip or counter)) and !miss) blk: {
|
|
777
853
|
if (showdown and move.effect.isMulti()) {
|
|
778
854
|
Effects.multiHit(battle, player, move);
|
|
779
855
|
hits = side.active.volatiles.attacks;
|
|
856
|
+
late = false;
|
|
780
857
|
}
|
|
781
858
|
|
|
782
859
|
// Cartridge rolls for crit even for moves that can't crit (Counter/Metronome/status/OHKO)
|
|
@@ -810,7 +887,7 @@ fn doMove(
|
|
|
810
887
|
}
|
|
811
888
|
|
|
812
889
|
// Due to control flow shenanigans we need to clear last_damage for Pokémon Showdown
|
|
813
|
-
if (showdown and skip
|
|
890
|
+
if (showdown and skip) battle.last_damage = 0;
|
|
814
891
|
|
|
815
892
|
miss = if (showdown or skip)
|
|
816
893
|
miss
|
|
@@ -823,9 +900,9 @@ fn doMove(
|
|
|
823
900
|
|
|
824
901
|
if (!showdown or !miss) {
|
|
825
902
|
if (move.effect == .MirrorMove) {
|
|
826
|
-
return mirrorMove(battle, player, mslot, auto, residual, log);
|
|
903
|
+
return mirrorMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
827
904
|
} else if (move.effect == .Metronome) {
|
|
828
|
-
return metronome(battle, player, mslot, auto, residual, log);
|
|
905
|
+
return metronome(battle, player, mslot, rewrap, auto, residual, log);
|
|
829
906
|
} else if (move.effect.onEnd()) {
|
|
830
907
|
try onEnd(battle, player, move, log);
|
|
831
908
|
return null;
|
|
@@ -840,7 +917,7 @@ fn doMove(
|
|
|
840
917
|
move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
|
|
841
918
|
if (ohko) {
|
|
842
919
|
try log.immune(foe_ident, .OHKO);
|
|
843
|
-
} else if (immune and !invulnerable) {
|
|
920
|
+
} else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
|
|
844
921
|
try log.immune(foe_ident, .None);
|
|
845
922
|
} else if (mist) {
|
|
846
923
|
if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
|
|
@@ -857,15 +934,9 @@ fn doMove(
|
|
|
857
934
|
if (showdown and side.stored().hp == 0) residual.* = false;
|
|
858
935
|
} else if (move.effect == .Explode) {
|
|
859
936
|
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
|
-
}
|
|
937
|
+
try buildRage(battle, player.foe(), log);
|
|
938
|
+
} else if (showdown and move.effect == .Disable) {
|
|
939
|
+
try buildRage(battle, player.foe(), log);
|
|
869
940
|
}
|
|
870
941
|
return null;
|
|
871
942
|
}
|
|
@@ -878,6 +949,9 @@ fn doMove(
|
|
|
878
949
|
hits = side.active.volatiles.attacks;
|
|
879
950
|
}
|
|
880
951
|
|
|
952
|
+
// Pokémon Showdown only builds Rage for Disable/Explosion (hit/miss) when attacking into a sub
|
|
953
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
954
|
+
|
|
881
955
|
var nullified = false;
|
|
882
956
|
var hit: u4 = 0;
|
|
883
957
|
while (hit < hits) {
|
|
@@ -893,9 +967,7 @@ fn doMove(
|
|
|
893
967
|
if (hit == 0 and ohko) try log.ohko();
|
|
894
968
|
hit += 1;
|
|
895
969
|
if (foe.stored().hp == 0) break;
|
|
896
|
-
if (
|
|
897
|
-
try Effects.boost(battle, player.foe(), Move.get(.Rage), log);
|
|
898
|
-
}
|
|
970
|
+
if (!late and (!sub or move.effect == .Explode)) try buildRage(battle, player.foe(), log);
|
|
899
971
|
// If the substitute breaks during a multi-hit attack, the attack ends
|
|
900
972
|
if (nullified) break;
|
|
901
973
|
}
|
|
@@ -911,11 +983,17 @@ fn doMove(
|
|
|
911
983
|
} else if (showdown) {
|
|
912
984
|
// This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
|
|
913
985
|
if (move.effect == .Binding) {
|
|
914
|
-
Effects.binding(battle, player);
|
|
986
|
+
Effects.binding(battle, player, rewrap);
|
|
915
987
|
if (immune) {
|
|
916
988
|
battle.last_damage = 0;
|
|
989
|
+
assert(foe.stored().hp > 0);
|
|
917
990
|
// Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
|
|
918
|
-
|
|
991
|
+
if (sub) {
|
|
992
|
+
try log.activate(battle.active(player.foe()), .Substitute);
|
|
993
|
+
} else {
|
|
994
|
+
try log.damage(battle.active(player.foe()), foe.stored(), .None);
|
|
995
|
+
try buildRage(battle, player.foe(), log);
|
|
996
|
+
}
|
|
919
997
|
return null;
|
|
920
998
|
}
|
|
921
999
|
}
|
|
@@ -932,6 +1010,9 @@ fn doMove(
|
|
|
932
1010
|
|
|
933
1011
|
if (foe.stored().hp == 0) return null;
|
|
934
1012
|
|
|
1013
|
+
// Pokémon Showdown builds Rage at the wrong time for non-MultiHit move
|
|
1014
|
+
if (late and !sub and move.effect != .Disable) try buildRage(battle, player.foe(), log);
|
|
1015
|
+
|
|
935
1016
|
if (!move.effect.isSpecial()) {
|
|
936
1017
|
// On the cartridge Rage is not considered to be "special" and thus gets executed for a
|
|
937
1018
|
// second time here (after being executed in the "always happens" block above) but that
|
|
@@ -943,7 +1024,9 @@ fn doMove(
|
|
|
943
1024
|
if (move.effect == .Twineedle) {
|
|
944
1025
|
if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), log);
|
|
945
1026
|
} else if (move.effect == .Disable) {
|
|
946
|
-
|
|
1027
|
+
const result = try Effects.disable(battle, player, move, log);
|
|
1028
|
+
if (showdown) try buildRage(battle, player.foe(), log);
|
|
1029
|
+
return result;
|
|
947
1030
|
} else {
|
|
948
1031
|
try moveEffect(battle, player, move, log);
|
|
949
1032
|
}
|
|
@@ -961,8 +1044,7 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
|
|
|
961
1044
|
chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
|
|
962
1045
|
chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
|
|
963
1046
|
|
|
964
|
-
|
|
965
|
-
return std.math.rotl(u8, battle.rng.next(), 3) < chance;
|
|
1047
|
+
return Rolls.criticalHit(battle, chance);
|
|
966
1048
|
}
|
|
967
1049
|
|
|
968
1050
|
fn calcDamage(
|
|
@@ -1066,49 +1148,37 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
|
1066
1148
|
|
|
1067
1149
|
fn randomizeDamage(battle: anytype) void {
|
|
1068
1150
|
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
|
-
|
|
1151
|
+
const random = Rolls.damage(battle);
|
|
1079
1152
|
battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
|
|
1080
1153
|
}
|
|
1081
1154
|
|
|
1082
1155
|
fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
|
|
1083
1156
|
const side = battle.side(player);
|
|
1157
|
+
const foe = battle.foe(player);
|
|
1084
1158
|
|
|
1085
1159
|
if (!try checkHit(battle, player, move, log)) return null;
|
|
1086
1160
|
|
|
1087
1161
|
battle.last_damage = switch (side.last_selected_move) {
|
|
1088
|
-
.SuperFang => @max(
|
|
1162
|
+
.SuperFang => @max(foe.stored().hp / 2, 1),
|
|
1089
1163
|
.SeismicToss, .NightShade => side.stored().level,
|
|
1090
1164
|
.SonicBoom => 20,
|
|
1091
1165
|
.DragonRage => 40,
|
|
1092
1166
|
// GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
|
|
1093
1167
|
.Psywave => power: {
|
|
1094
1168
|
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
|
-
}
|
|
1169
|
+
// GLITCH: Psywave infinite glitch loop
|
|
1170
|
+
if (!showdown and max <= 1) return Result.Error;
|
|
1171
|
+
break :power Rolls.psywave(battle, max);
|
|
1105
1172
|
},
|
|
1106
1173
|
else => unreachable,
|
|
1107
1174
|
};
|
|
1108
1175
|
|
|
1109
1176
|
if (battle.last_damage == 0) return if (showdown) null else Result.Error;
|
|
1110
1177
|
|
|
1178
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1111
1179
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
1180
|
+
if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
|
|
1181
|
+
|
|
1112
1182
|
return null;
|
|
1113
1183
|
}
|
|
1114
1184
|
|
|
@@ -1120,19 +1190,17 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
|
|
|
1120
1190
|
return null;
|
|
1121
1191
|
}
|
|
1122
1192
|
|
|
1123
|
-
// Pretend
|
|
1124
|
-
const foe_last_used_move =
|
|
1125
|
-
Move.get(if (foe.last_used_move == .None) .Counter else foe.last_used_move);
|
|
1193
|
+
// Pretend Splash was used as a stand-in when no move has been used to fail below with 0 BP
|
|
1126
1194
|
const foe_last_selected_move =
|
|
1127
1195
|
Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
|
|
1128
|
-
.
|
|
1196
|
+
.Splash
|
|
1129
1197
|
else
|
|
1130
1198
|
foe.last_selected_move);
|
|
1131
1199
|
|
|
1132
|
-
const used =
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1200
|
+
const used = (if (player.foe() == .P1)
|
|
1201
|
+
battle.last_moves.p1_counterable
|
|
1202
|
+
else
|
|
1203
|
+
battle.last_moves.p2_counterable) != 0;
|
|
1136
1204
|
|
|
1137
1205
|
const selected = foe_last_selected_move.bp > 0 and
|
|
1138
1206
|
foe.last_selected_move != .Counter and
|
|
@@ -1156,7 +1224,9 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
|
|
|
1156
1224
|
// Pokémon Showdown calls checkHit before Counter
|
|
1157
1225
|
if (!showdown and !try checkHit(battle, player, move, log)) return null;
|
|
1158
1226
|
|
|
1227
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1159
1228
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
1229
|
+
if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
|
|
1160
1230
|
return null;
|
|
1161
1231
|
}
|
|
1162
1232
|
|
|
@@ -1200,6 +1270,7 @@ fn mirrorMove(
|
|
|
1200
1270
|
battle: anytype,
|
|
1201
1271
|
player: Player,
|
|
1202
1272
|
mslot: u4,
|
|
1273
|
+
rewrap: bool,
|
|
1203
1274
|
auto: bool,
|
|
1204
1275
|
residual: *bool,
|
|
1205
1276
|
log: anytype,
|
|
@@ -1212,41 +1283,30 @@ fn mirrorMove(
|
|
|
1212
1283
|
if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
|
|
1213
1284
|
try log.fail(battle.active(player), .None);
|
|
1214
1285
|
return null;
|
|
1286
|
+
} else if (!showdown or foe.last_used_move != .Struggle) {
|
|
1287
|
+
incrementPP(side, mslot);
|
|
1215
1288
|
}
|
|
1216
1289
|
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, log)) return null;
|
|
1220
|
-
return doMove(battle, player, mslot, auto, residual, log);
|
|
1290
|
+
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
|
|
1291
|
+
return doMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
1221
1292
|
}
|
|
1222
1293
|
|
|
1223
1294
|
fn metronome(
|
|
1224
1295
|
battle: anytype,
|
|
1225
1296
|
player: Player,
|
|
1226
1297
|
mslot: u4,
|
|
1298
|
+
rewrap: bool,
|
|
1227
1299
|
auto: bool,
|
|
1228
1300
|
residual: *bool,
|
|
1229
1301
|
log: anytype,
|
|
1230
1302
|
) !?Result {
|
|
1231
1303
|
var side = battle.side(player);
|
|
1232
1304
|
|
|
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
|
-
|
|
1305
|
+
side.last_selected_move = Rolls.metronome(battle);
|
|
1246
1306
|
incrementPP(side, mslot);
|
|
1247
1307
|
|
|
1248
|
-
if (!try canMove(battle, player, mslot, auto, false, .Metronome, log)) return null;
|
|
1249
|
-
return doMove(battle, player, mslot, auto, residual, log);
|
|
1308
|
+
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
|
|
1309
|
+
return doMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
1250
1310
|
}
|
|
1251
1311
|
|
|
1252
1312
|
fn checkHit(battle: anytype, player: Player, move: Move.Data, log: anytype) !bool {
|
|
@@ -1278,7 +1338,9 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
|
|
|
1278
1338
|
if (move.effect == .Swift) return true;
|
|
1279
1339
|
if (foe.active.volatiles.Invulnerable) break :miss true;
|
|
1280
1340
|
}
|
|
1281
|
-
if (move.effect == .DreamEater and !Status.is(foe.stored().status, .SLP)
|
|
1341
|
+
if (move.effect == .DreamEater and (!Status.is(foe.stored().status, .SLP) or
|
|
1342
|
+
(showdown and foe.active.volatiles.Substitute)))
|
|
1343
|
+
{
|
|
1282
1344
|
immune.* = true;
|
|
1283
1345
|
if (showdown) return false;
|
|
1284
1346
|
break :miss true;
|
|
@@ -1298,23 +1360,24 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
|
|
|
1298
1360
|
}
|
|
1299
1361
|
|
|
1300
1362
|
// GLITCH: Thrash / Petal Dance / Rage get their accuracy overwritten on subsequent hits
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1363
|
+
const state = side.active.volatiles.state;
|
|
1364
|
+
var overwrite = move.effect == .Rage or move.effect == .Thrashing;
|
|
1365
|
+
const overwritten = overwrite and state > 0;
|
|
1366
|
+
assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
|
|
1367
|
+
|
|
1368
|
+
var accuracy = if (overwritten) state else @as(u16, Gen12.percent(move.accuracy));
|
|
1305
1369
|
var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
|
|
1306
1370
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1307
1371
|
boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
|
|
1308
1372
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1309
1373
|
accuracy = @min(255, @max(1, accuracy));
|
|
1310
1374
|
|
|
1311
|
-
|
|
1375
|
+
// Pokémon Showdown only overwrites if the volatile is present
|
|
1376
|
+
if (showdown) overwrite = side.active.volatiles.Rage or side.active.volatiles.Thrashing;
|
|
1377
|
+
if (overwrite) side.active.volatiles.state = accuracy;
|
|
1312
1378
|
|
|
1313
1379
|
// 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;
|
|
1380
|
+
break :miss !Rolls.hit(battle, accuracy);
|
|
1318
1381
|
};
|
|
1319
1382
|
|
|
1320
1383
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1429,6 +1492,15 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
|
|
|
1429
1492
|
}
|
|
1430
1493
|
|
|
1431
1494
|
if (volatiles.LeechSeed) {
|
|
1495
|
+
var foe = battle.foe(player);
|
|
1496
|
+
var foe_stored = foe.stored();
|
|
1497
|
+
const foe_ident = battle.active(player.foe());
|
|
1498
|
+
|
|
1499
|
+
if (foe_stored.hp == 0) {
|
|
1500
|
+
assert(showdown);
|
|
1501
|
+
return;
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1432
1504
|
var damage = @max(stored.stats.hp / 16, 1);
|
|
1433
1505
|
|
|
1434
1506
|
// GLITCH: Leech Seed + Toxic glitch
|
|
@@ -1440,10 +1512,6 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
|
|
|
1440
1512
|
const amount = @min(damage, stored.hp);
|
|
1441
1513
|
stored.hp -= amount;
|
|
1442
1514
|
|
|
1443
|
-
var foe = battle.foe(player);
|
|
1444
|
-
var foe_stored = foe.stored();
|
|
1445
|
-
const foe_ident = battle.active(player.foe());
|
|
1446
|
-
|
|
1447
1515
|
// As above, Pokémon Showdown uses damageOf but its not relevant
|
|
1448
1516
|
if (amount > 0) try log.damage(ident, stored, .LeechSeed);
|
|
1449
1517
|
|
|
@@ -1480,16 +1548,20 @@ fn endTurn(battle: anytype, log: anytype) @TypeOf(log).Error!Result {
|
|
|
1480
1548
|
|
|
1481
1549
|
fn checkEBC(battle: anytype) bool {
|
|
1482
1550
|
for (battle.sides, 0..) |side, i| {
|
|
1551
|
+
const foe = battle.sides[~@intCast(u1, i)];
|
|
1552
|
+
|
|
1483
1553
|
var foe_all_ghosts = true;
|
|
1484
1554
|
var foe_all_transform = true;
|
|
1555
|
+
for (foe.order, 0..) |id, j| {
|
|
1556
|
+
if (id == 0) break;
|
|
1557
|
+
const active = j == 0;
|
|
1558
|
+
const pokemon = foe.pokemon[id - 1];
|
|
1485
1559
|
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
const ghost = pokemon.hp == 0 or pokemon.types.includes(.Ghost);
|
|
1560
|
+
const ghost = pokemon.hp == 0 or
|
|
1561
|
+
(if (active) foe.active.types else pokemon.types).includes(.Ghost);
|
|
1490
1562
|
foe_all_ghosts = foe_all_ghosts and ghost;
|
|
1491
1563
|
foe_all_transform = foe_all_transform and pokemon.hp == 0 or transform: {
|
|
1492
|
-
for (pokemon.moves) |m| {
|
|
1564
|
+
for (if (active) foe.active.moves else pokemon.moves) |m| {
|
|
1493
1565
|
if (m.id == .None) break :transform true;
|
|
1494
1566
|
if (m.id != .Transform) break :transform false;
|
|
1495
1567
|
}
|
|
@@ -1497,10 +1569,14 @@ fn checkEBC(battle: anytype) bool {
|
|
|
1497
1569
|
};
|
|
1498
1570
|
}
|
|
1499
1571
|
|
|
1500
|
-
for (side.
|
|
1572
|
+
for (side.order, 0..) |id, j| {
|
|
1573
|
+
if (id == 0) break;
|
|
1574
|
+
const active = j == 0;
|
|
1575
|
+
const pokemon = side.pokemon[id - 1];
|
|
1576
|
+
|
|
1501
1577
|
if (pokemon.hp == 0 or Status.is(pokemon.status, .FRZ)) continue;
|
|
1502
1578
|
const transform = foe_all_transform and transform: {
|
|
1503
|
-
for (pokemon.moves) |m| {
|
|
1579
|
+
for (if (active) side.active.moves else pokemon.moves) |m| {
|
|
1504
1580
|
if (m.id == .None) break :transform true;
|
|
1505
1581
|
if (m.id != .Transform) break :transform false;
|
|
1506
1582
|
}
|
|
@@ -1508,7 +1584,7 @@ fn checkEBC(battle: anytype) bool {
|
|
|
1508
1584
|
};
|
|
1509
1585
|
if (transform) continue;
|
|
1510
1586
|
const no_pp = foe_all_ghosts and no_pp: {
|
|
1511
|
-
for (pokemon.moves) |m| {
|
|
1587
|
+
for (if (active) side.active.moves else pokemon.moves) |m| {
|
|
1512
1588
|
if (m.pp != 0) break :no_pp false;
|
|
1513
1589
|
}
|
|
1514
1590
|
break :no_pp true;
|
|
@@ -1524,7 +1600,50 @@ fn checkEBC(battle: anytype) bool {
|
|
|
1524
1600
|
return false;
|
|
1525
1601
|
}
|
|
1526
1602
|
|
|
1527
|
-
|
|
1603
|
+
fn disabled(side: *Side, ident: ID, log: anytype) !bool {
|
|
1604
|
+
if (side.active.volatiles.disabled_move != 0) {
|
|
1605
|
+
// A Pokémon that transforms after being disabled may end up with less move slots
|
|
1606
|
+
const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
|
|
1607
|
+
// side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
|
|
1608
|
+
const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
|
|
1609
|
+
m
|
|
1610
|
+
else
|
|
1611
|
+
side.last_selected_move;
|
|
1612
|
+
if (m != .None and m == last) {
|
|
1613
|
+
side.active.volatiles.Charging = false;
|
|
1614
|
+
try log.disabled(ident, last);
|
|
1615
|
+
return true;
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
return false;
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
inline fn buildRage(battle: anytype, who: Player, log: anytype) !void {
|
|
1622
|
+
const side = battle.side(who);
|
|
1623
|
+
if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
|
|
1624
|
+
try Effects.boost(battle, who, Move.get(.Rage), log);
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
|
|
1629
|
+
var volatiles = &active.volatiles;
|
|
1630
|
+
assert(volatiles.Thrashing);
|
|
1631
|
+
if (volatiles.attacks > 0) return false;
|
|
1632
|
+
|
|
1633
|
+
volatiles.Thrashing = false;
|
|
1634
|
+
volatiles.Confusion = true;
|
|
1635
|
+
volatiles.confusion = Rolls.confusionDuration(battle);
|
|
1636
|
+
return true;
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
inline fn onBegin(
|
|
1640
|
+
battle: anytype,
|
|
1641
|
+
player: Player,
|
|
1642
|
+
move: Move.Data,
|
|
1643
|
+
mslot: u8,
|
|
1644
|
+
residual: *bool,
|
|
1645
|
+
log: anytype,
|
|
1646
|
+
) !void {
|
|
1528
1647
|
assert(move.effect.onBegin());
|
|
1529
1648
|
return switch (move.effect) {
|
|
1530
1649
|
.Confusion => Effects.confusion(battle, player, move, log),
|
|
@@ -1540,7 +1659,7 @@ inline fn onBegin(battle: anytype, player: Player, move: Move.Data, mslot: u8, l
|
|
|
1540
1659
|
.Poison => Effects.poison(battle, player, move, log),
|
|
1541
1660
|
.Reflect => Effects.reflect(battle, player, log),
|
|
1542
1661
|
.Splash => Effects.splash(battle, player, log),
|
|
1543
|
-
.Substitute => Effects.substitute(battle, player, log),
|
|
1662
|
+
.Substitute => Effects.substitute(battle, player, residual, log),
|
|
1544
1663
|
.SwitchAndTeleport => Effects.switchAndTeleport(battle, player, move, log),
|
|
1545
1664
|
.Transform => Effects.transform(battle, player, log),
|
|
1546
1665
|
else => unreachable,
|
|
@@ -1607,10 +1726,7 @@ pub const Effects = struct {
|
|
|
1607
1726
|
side.active.volatiles.Bide = true;
|
|
1608
1727
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1609
1728
|
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);
|
|
1729
|
+
side.active.volatiles.attacks = Rolls.bideThrashDuration(battle);
|
|
1614
1730
|
|
|
1615
1731
|
try log.start(battle.active(player), .Bide);
|
|
1616
1732
|
}
|
|
@@ -1632,14 +1748,8 @@ pub const Effects = struct {
|
|
|
1632
1748
|
return;
|
|
1633
1749
|
}
|
|
1634
1750
|
|
|
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;
|
|
1751
|
+
if (foe.active.types.includes(move.type)) return;
|
|
1752
|
+
if (!Rolls.secondaryChance(battle, move.effect == .BurnChance1)) return;
|
|
1643
1753
|
|
|
1644
1754
|
foe_stored.status = Status.init(.BRN);
|
|
1645
1755
|
foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
|
|
@@ -1663,11 +1773,7 @@ pub const Effects = struct {
|
|
|
1663
1773
|
const sub = foe.active.volatiles.Substitute;
|
|
1664
1774
|
|
|
1665
1775
|
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;
|
|
1776
|
+
if (!Rolls.confusionChance(battle)) return;
|
|
1671
1777
|
} else {
|
|
1672
1778
|
if (showdown) {
|
|
1673
1779
|
if (!try checkHit(battle, player, move, log)) {
|
|
@@ -1686,10 +1792,7 @@ pub const Effects = struct {
|
|
|
1686
1792
|
|
|
1687
1793
|
if (foe.active.volatiles.Confusion) return;
|
|
1688
1794
|
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);
|
|
1795
|
+
foe.active.volatiles.confusion = Rolls.confusionDuration(battle);
|
|
1693
1796
|
|
|
1694
1797
|
try log.start(battle.active(player.foe()), .Confusion);
|
|
1695
1798
|
}
|
|
@@ -1739,11 +1842,8 @@ pub const Effects = struct {
|
|
|
1739
1842
|
return Result.Error;
|
|
1740
1843
|
}
|
|
1741
1844
|
|
|
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);
|
|
1845
|
+
volatiles.disabled_move = @intCast(u3, Rolls.moveSlot(battle, &foe.active.moves, n));
|
|
1846
|
+
volatiles.disabled_duration = Rolls.disableDuration(battle);
|
|
1747
1847
|
|
|
1748
1848
|
try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
|
|
1749
1849
|
return null;
|
|
@@ -1753,6 +1853,11 @@ pub const Effects = struct {
|
|
|
1753
1853
|
var side = battle.side(player);
|
|
1754
1854
|
var stored = side.stored();
|
|
1755
1855
|
|
|
1856
|
+
if (battle.last_damage == 0) {
|
|
1857
|
+
assert(showdown);
|
|
1858
|
+
if (!battle.foe(player).active.volatiles.Substitute) return;
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1756
1861
|
const drain = @max(battle.last_damage / 2, 1);
|
|
1757
1862
|
battle.last_damage = drain;
|
|
1758
1863
|
|
|
@@ -1777,15 +1882,7 @@ pub const Effects = struct {
|
|
|
1777
1882
|
var volatiles = &battle.foe(player).active.volatiles;
|
|
1778
1883
|
|
|
1779
1884
|
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;
|
|
1885
|
+
if (!Rolls.secondaryChance(battle, move.effect == .FlinchChance1)) return;
|
|
1789
1886
|
|
|
1790
1887
|
volatiles.Flinch = true;
|
|
1791
1888
|
volatiles.Recharging = false;
|
|
@@ -1811,12 +1908,8 @@ pub const Effects = struct {
|
|
|
1811
1908
|
return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
|
|
1812
1909
|
}
|
|
1813
1910
|
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
else
|
|
1817
|
-
battle.rng.next() < 1 + Gen12.percent(10);
|
|
1818
|
-
if (!chance) return;
|
|
1819
|
-
|
|
1911
|
+
if (foe.active.types.includes(move.type)) return;
|
|
1912
|
+
if (!Rolls.secondaryChance(battle, true)) return;
|
|
1820
1913
|
// Freeze Clause Mod
|
|
1821
1914
|
if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
1822
1915
|
|
|
@@ -1953,15 +2046,15 @@ pub const Effects = struct {
|
|
|
1953
2046
|
}
|
|
1954
2047
|
break :has_mimic false;
|
|
1955
2048
|
};
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
2049
|
+
if (!has_mimic) {
|
|
2050
|
+
// Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
|
|
2051
|
+
if (!try checkHit(battle, player, move, log)) return;
|
|
1959
2052
|
return try log.fail(battle.active(player.foe()), .None);
|
|
1960
2053
|
}
|
|
1961
2054
|
}
|
|
1962
2055
|
if (!try checkHit(battle, player, move, log)) return;
|
|
1963
2056
|
|
|
1964
|
-
const rslot =
|
|
2057
|
+
const rslot = Rolls.moveSlot(battle, &foe.active.moves, 0);
|
|
1965
2058
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
1966
2059
|
|
|
1967
2060
|
try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
|
|
@@ -1982,7 +2075,8 @@ pub const Effects = struct {
|
|
|
1982
2075
|
assert(!side.active.volatiles.MultiHit);
|
|
1983
2076
|
side.active.volatiles.MultiHit = true;
|
|
1984
2077
|
|
|
1985
|
-
side.active.volatiles.attacks =
|
|
2078
|
+
side.active.volatiles.attacks =
|
|
2079
|
+
if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
|
|
1986
2080
|
}
|
|
1987
2081
|
|
|
1988
2082
|
fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2026,14 +2120,8 @@ pub const Effects = struct {
|
|
|
2026
2120
|
}
|
|
2027
2121
|
|
|
2028
2122
|
// 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;
|
|
2123
|
+
if (foe.active.types.includes(move.type)) return;
|
|
2124
|
+
if (!Rolls.secondaryChance(battle, move.effect == .ParalyzeChance1)) return;
|
|
2037
2125
|
|
|
2038
2126
|
foe_stored.status = Status.init(.PAR);
|
|
2039
2127
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
@@ -2063,7 +2151,7 @@ pub const Effects = struct {
|
|
|
2063
2151
|
return log.fail(foe_ident, if (Status.is(foe_stored.status, .PSN))
|
|
2064
2152
|
if (!showdown)
|
|
2065
2153
|
.Poison
|
|
2066
|
-
else if (toxic ==
|
|
2154
|
+
else if (toxic == (foe_stored.status == Status.TOX))
|
|
2067
2155
|
if (toxic) .Toxic else .Poison
|
|
2068
2156
|
else
|
|
2069
2157
|
.None
|
|
@@ -2076,16 +2164,7 @@ pub const Effects = struct {
|
|
|
2076
2164
|
|
|
2077
2165
|
if (move.effect == .Poison) {
|
|
2078
2166
|
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
|
-
}
|
|
2167
|
+
} else if (!Rolls.poisonChance(battle, move.effect == .PoisonChance1)) return;
|
|
2089
2168
|
|
|
2090
2169
|
foe_stored.status = Status.init(.PSN);
|
|
2091
2170
|
if (toxic) {
|
|
@@ -2099,10 +2178,8 @@ pub const Effects = struct {
|
|
|
2099
2178
|
|
|
2100
2179
|
fn rage(battle: anytype, player: Player) !void {
|
|
2101
2180
|
var volatiles = &battle.side(player).active.volatiles;
|
|
2102
|
-
if (volatiles.Rage) return;
|
|
2103
|
-
volatiles.Rage = true;
|
|
2104
2181
|
assert(!volatiles.Bide);
|
|
2105
|
-
volatiles.
|
|
2182
|
+
volatiles.Rage = true;
|
|
2106
2183
|
}
|
|
2107
2184
|
|
|
2108
2185
|
fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
|
|
@@ -2154,16 +2231,7 @@ pub const Effects = struct {
|
|
|
2154
2231
|
}
|
|
2155
2232
|
foe.active.volatiles.Recharging = false;
|
|
2156
2233
|
|
|
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);
|
|
2234
|
+
foe_stored.status = Status.slp(Rolls.sleepDuration(battle));
|
|
2167
2235
|
try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
|
|
2168
2236
|
}
|
|
2169
2237
|
|
|
@@ -2171,7 +2239,7 @@ pub const Effects = struct {
|
|
|
2171
2239
|
try log.activate(battle.active(player), .Splash);
|
|
2172
2240
|
}
|
|
2173
2241
|
|
|
2174
|
-
fn substitute(battle: anytype, player: Player, log: anytype) !void {
|
|
2242
|
+
fn substitute(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
|
|
2175
2243
|
var side = battle.side(player);
|
|
2176
2244
|
if (side.active.volatiles.Substitute) {
|
|
2177
2245
|
try log.fail(battle.active(player), .Substitute);
|
|
@@ -2196,14 +2264,18 @@ pub const Effects = struct {
|
|
|
2196
2264
|
side.active.volatiles.substitute = hp + 1;
|
|
2197
2265
|
side.active.volatiles.Substitute = true;
|
|
2198
2266
|
try log.start(battle.active(player), .Substitute);
|
|
2199
|
-
if (hp > 0)
|
|
2267
|
+
if (hp > 0) {
|
|
2268
|
+
try log.damage(battle.active(player), side.stored(), .None);
|
|
2269
|
+
if (showdown and side.stored().hp == 0) residual.* = false;
|
|
2270
|
+
}
|
|
2200
2271
|
}
|
|
2201
2272
|
|
|
2202
2273
|
fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
2203
2274
|
if (!showdown or battle.side(player).last_selected_move == .Teleport) return;
|
|
2204
2275
|
|
|
2205
|
-
// Whirlwind/Roar should not roll to hit but Pokémon Showdown does anyway
|
|
2276
|
+
// Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
|
|
2206
2277
|
_ = try checkHit(battle, player, move, log);
|
|
2278
|
+
battle.last_damage = 0;
|
|
2207
2279
|
}
|
|
2208
2280
|
|
|
2209
2281
|
fn thrashing(battle: anytype, player: Player) void {
|
|
@@ -2212,11 +2284,7 @@ pub const Effects = struct {
|
|
|
2212
2284
|
assert(!volatiles.Bide);
|
|
2213
2285
|
|
|
2214
2286
|
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);
|
|
2287
|
+
volatiles.attacks = Rolls.bideThrashDuration(battle);
|
|
2220
2288
|
}
|
|
2221
2289
|
|
|
2222
2290
|
fn transform(battle: anytype, player: Player, log: anytype) !void {
|
|
@@ -2248,7 +2316,7 @@ pub const Effects = struct {
|
|
|
2248
2316
|
try log.transform(battle.active(player), foe_ident);
|
|
2249
2317
|
}
|
|
2250
2318
|
|
|
2251
|
-
fn binding(battle: anytype, player: Player) void {
|
|
2319
|
+
fn binding(battle: anytype, player: Player, rewrap: bool) void {
|
|
2252
2320
|
var side = battle.side(player);
|
|
2253
2321
|
var foe = battle.foe(player);
|
|
2254
2322
|
|
|
@@ -2256,9 +2324,11 @@ pub const Effects = struct {
|
|
|
2256
2324
|
side.active.volatiles.Binding = true;
|
|
2257
2325
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2258
2326
|
// (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
|
|
2259
|
-
if (!showdown)
|
|
2327
|
+
if (!showdown) {
|
|
2328
|
+
foe.active.volatiles.Recharging = false;
|
|
2329
|
+
} else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
|
|
2260
2330
|
|
|
2261
|
-
side.active.volatiles.attacks = distribution(battle) - 1;
|
|
2331
|
+
side.active.volatiles.attacks = Rolls.distribution(battle) - 1;
|
|
2262
2332
|
}
|
|
2263
2333
|
|
|
2264
2334
|
fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2285,9 +2355,11 @@ pub const Effects = struct {
|
|
|
2285
2355
|
return try log.fail(ident, .None);
|
|
2286
2356
|
}
|
|
2287
2357
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2288
|
-
const stat = unmodifiedStats(battle,
|
|
2358
|
+
const stat = unmodifiedStats(battle, side).atk;
|
|
2289
2359
|
stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2290
2360
|
try log.boost(ident, reason, n);
|
|
2361
|
+
// Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
|
|
2362
|
+
if (showdown and move.effect == .Rage) return;
|
|
2291
2363
|
},
|
|
2292
2364
|
.DefenseUp1, .DefenseUp2 => {
|
|
2293
2365
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
@@ -2303,7 +2375,7 @@ pub const Effects = struct {
|
|
|
2303
2375
|
return try log.fail(ident, .None);
|
|
2304
2376
|
}
|
|
2305
2377
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2306
|
-
const stat = unmodifiedStats(battle,
|
|
2378
|
+
const stat = unmodifiedStats(battle, side).def;
|
|
2307
2379
|
stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2308
2380
|
try log.boost(ident, .Defense, n);
|
|
2309
2381
|
},
|
|
@@ -2320,7 +2392,7 @@ pub const Effects = struct {
|
|
|
2320
2392
|
return try log.fail(ident, .None);
|
|
2321
2393
|
}
|
|
2322
2394
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2323
|
-
const stat = unmodifiedStats(battle,
|
|
2395
|
+
const stat = unmodifiedStats(battle, side).spe;
|
|
2324
2396
|
stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2325
2397
|
try log.boost(ident, .Speed, 2);
|
|
2326
2398
|
},
|
|
@@ -2340,7 +2412,7 @@ pub const Effects = struct {
|
|
|
2340
2412
|
return try log.fail(ident, .None);
|
|
2341
2413
|
}
|
|
2342
2414
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2343
|
-
const stat = unmodifiedStats(battle,
|
|
2415
|
+
const stat = unmodifiedStats(battle, side).spc;
|
|
2344
2416
|
stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2345
2417
|
try log.boost(ident, .SpecialAttack, n);
|
|
2346
2418
|
try log.boost(ident, .SpecialDefense, n);
|
|
@@ -2367,11 +2439,7 @@ pub const Effects = struct {
|
|
|
2367
2439
|
}
|
|
2368
2440
|
|
|
2369
2441
|
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;
|
|
2442
|
+
if (!Rolls.unboost(battle) or foe.active.volatiles.Invulnerable) return;
|
|
2375
2443
|
} else if (!showdown and !try checkHit(battle, player, move, log)) {
|
|
2376
2444
|
return; // checkHit already checks for Invulnerable
|
|
2377
2445
|
}
|
|
@@ -2393,7 +2461,7 @@ pub const Effects = struct {
|
|
|
2393
2461
|
return try log.fail(foe_ident, .None);
|
|
2394
2462
|
}
|
|
2395
2463
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2396
|
-
const stat = unmodifiedStats(battle,
|
|
2464
|
+
const stat = unmodifiedStats(battle, foe).atk;
|
|
2397
2465
|
stats.atk = @max(1, stat * mod[0] / mod[1]);
|
|
2398
2466
|
try log.boost(foe_ident, .Attack, -1);
|
|
2399
2467
|
},
|
|
@@ -2411,7 +2479,7 @@ pub const Effects = struct {
|
|
|
2411
2479
|
return try log.fail(foe_ident, .None);
|
|
2412
2480
|
}
|
|
2413
2481
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2414
|
-
const stat = unmodifiedStats(battle,
|
|
2482
|
+
const stat = unmodifiedStats(battle, foe).def;
|
|
2415
2483
|
stats.def = @max(1, stat * mod[0] / mod[1]);
|
|
2416
2484
|
try log.boost(foe_ident, .Defense, -@as(i8, n));
|
|
2417
2485
|
},
|
|
@@ -2428,7 +2496,7 @@ pub const Effects = struct {
|
|
|
2428
2496
|
return try log.fail(foe_ident, .None);
|
|
2429
2497
|
}
|
|
2430
2498
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2431
|
-
const stat = unmodifiedStats(battle,
|
|
2499
|
+
const stat = unmodifiedStats(battle, foe).spe;
|
|
2432
2500
|
stats.spe = @max(1, stat * mod[0] / mod[1]);
|
|
2433
2501
|
try log.boost(foe_ident, .Speed, -1);
|
|
2434
2502
|
assert(boosts.spe >= -6);
|
|
@@ -2448,7 +2516,7 @@ pub const Effects = struct {
|
|
|
2448
2516
|
return try log.fail(foe_ident, .None);
|
|
2449
2517
|
}
|
|
2450
2518
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2451
|
-
const stat = unmodifiedStats(battle,
|
|
2519
|
+
const stat = unmodifiedStats(battle, foe).spc;
|
|
2452
2520
|
stats.spc = @max(1, stat * mod[0] / mod[1]);
|
|
2453
2521
|
try log.boost(foe_ident, .SpecialAttack, -1);
|
|
2454
2522
|
try log.boost(foe_ident, .SpecialDefense, -1);
|
|
@@ -2467,8 +2535,7 @@ pub const Effects = struct {
|
|
|
2467
2535
|
}
|
|
2468
2536
|
};
|
|
2469
2537
|
|
|
2470
|
-
fn unmodifiedStats(battle: anytype,
|
|
2471
|
-
const side = battle.side(who);
|
|
2538
|
+
fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
|
|
2472
2539
|
if (!side.active.volatiles.Transform) return &side.stored().stats;
|
|
2473
2540
|
const id = ID.from(side.active.volatiles.transform);
|
|
2474
2541
|
return &battle.side(id.player).pokemon[id.id - 1].stats;
|
|
@@ -2482,7 +2549,7 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
|
|
|
2482
2549
|
}
|
|
2483
2550
|
}
|
|
2484
2551
|
|
|
2485
|
-
fn isForced(active: ActivePokemon) bool {
|
|
2552
|
+
inline fn isForced(active: ActivePokemon) bool {
|
|
2486
2553
|
return active.volatiles.Recharging or active.volatiles.Rage or
|
|
2487
2554
|
active.volatiles.Thrashing or active.volatiles.Charging;
|
|
2488
2555
|
}
|
|
@@ -2514,13 +2581,9 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
|
|
|
2514
2581
|
volatiles.LeechSeed = false;
|
|
2515
2582
|
try log.end(ident, .LeechSeed);
|
|
2516
2583
|
}
|
|
2517
|
-
if (volatiles.Toxic) {
|
|
2584
|
+
if (!showdown and volatiles.Toxic) {
|
|
2518
2585
|
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
|
-
}
|
|
2586
|
+
// volatiles.toxic is left unchanged, except on Pokémon Showdown which clears it (below)
|
|
2524
2587
|
}
|
|
2525
2588
|
if (volatiles.LightScreen) {
|
|
2526
2589
|
volatiles.LightScreen = false;
|
|
@@ -2530,70 +2593,148 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
|
|
|
2530
2593
|
volatiles.Reflect = false;
|
|
2531
2594
|
try log.end(ident, .Reflect);
|
|
2532
2595
|
}
|
|
2596
|
+
if (showdown and volatiles.Toxic) {
|
|
2597
|
+
volatiles.Toxic = false;
|
|
2598
|
+
// Pokémon Showdown erroneously clears the toxic counter
|
|
2599
|
+
volatiles.toxic = 0;
|
|
2600
|
+
try log.end(ident, .Toxic);
|
|
2601
|
+
}
|
|
2533
2602
|
}
|
|
2534
2603
|
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2604
|
+
pub const Rolls = struct {
|
|
2605
|
+
inline fn speedTie(battle: anytype) bool {
|
|
2606
|
+
if (showdown) return battle.rng.range(u8, 0, 2) == 0;
|
|
2607
|
+
return battle.rng.next() < Gen12.percent(50) + 1;
|
|
2608
|
+
}
|
|
2609
|
+
|
|
2610
|
+
inline fn criticalHit(battle: anytype, chance: u16) bool {
|
|
2611
|
+
if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
|
|
2612
|
+
return std.math.rotl(u8, battle.rng.next(), 3) < chance;
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2615
|
+
inline fn damage(battle: anytype) u8 {
|
|
2616
|
+
if (showdown) return battle.rng.range(u8, 217, 256);
|
|
2617
|
+
while (true) {
|
|
2618
|
+
const r = std.math.rotr(u8, battle.rng.next(), 1);
|
|
2619
|
+
if (r >= 217) return r;
|
|
2543
2620
|
}
|
|
2544
2621
|
}
|
|
2545
|
-
return false;
|
|
2546
|
-
}
|
|
2547
2622
|
|
|
2548
|
-
fn
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2623
|
+
inline fn hit(battle: anytype, accuracy: u16) bool {
|
|
2624
|
+
if (showdown) return battle.rng.chance(u8, @intCast(u8, accuracy), 256);
|
|
2625
|
+
return battle.rng.next() < accuracy;
|
|
2626
|
+
}
|
|
2552
2627
|
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
else
|
|
2558
|
-
(battle.rng.next() & 3) + 2);
|
|
2628
|
+
inline fn confusionDuration(battle: anytype) u3 {
|
|
2629
|
+
if (showdown) return @intCast(u3, battle.rng.range(u8, 2, 6));
|
|
2630
|
+
return @intCast(u3, (battle.rng.next() & 3) + 2);
|
|
2631
|
+
}
|
|
2559
2632
|
|
|
2560
|
-
|
|
2561
|
-
|
|
2633
|
+
inline fn confused(battle: anytype) bool {
|
|
2634
|
+
if (showdown) return !battle.rng.chance(u8, 128, 256);
|
|
2635
|
+
return battle.rng.next() >= Gen12.percent(50) + 1;
|
|
2636
|
+
}
|
|
2562
2637
|
|
|
2563
|
-
|
|
2638
|
+
inline fn paralyzed(battle: anytype) bool {
|
|
2639
|
+
if (showdown) return battle.rng.chance(u8, 63, 256);
|
|
2640
|
+
return battle.rng.next() < Gen12.percent(25);
|
|
2641
|
+
}
|
|
2564
2642
|
|
|
2565
|
-
fn
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
}
|
|
2643
|
+
inline fn confusionChance(battle: anytype) bool {
|
|
2644
|
+
if (showdown) return battle.rng.chance(u8, 25, 256);
|
|
2645
|
+
return battle.rng.next() < Gen12.percent(10);
|
|
2646
|
+
}
|
|
2570
2647
|
|
|
2571
|
-
fn
|
|
2572
|
-
|
|
2573
|
-
if (
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2648
|
+
inline fn secondaryChance(battle: anytype, low: bool) bool {
|
|
2649
|
+
if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 26 else 77), 256);
|
|
2650
|
+
return battle.rng.next() < 1 + (if (low) Gen12.percent(10) else Gen12.percent(30));
|
|
2651
|
+
}
|
|
2652
|
+
|
|
2653
|
+
inline fn poisonChance(battle: anytype, low: bool) bool {
|
|
2654
|
+
if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 52 else 103), 256);
|
|
2655
|
+
return battle.rng.next() < 1 + (if (low) Gen12.percent(20) else Gen12.percent(40));
|
|
2656
|
+
}
|
|
2657
|
+
|
|
2658
|
+
inline fn sleepDuration(battle: anytype) u3 {
|
|
2659
|
+
if (showdown) return @intCast(u3, battle.rng.range(u8, 1, 8));
|
|
2660
|
+
while (true) {
|
|
2661
|
+
const r = battle.rng.next() & 7;
|
|
2662
|
+
if (r != 0) return @intCast(u3, r);
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2666
|
+
inline fn bideThrashDuration(battle: anytype) u3 {
|
|
2667
|
+
if (showdown) return @intCast(u3, battle.rng.range(u4, 2, 4));
|
|
2668
|
+
return @intCast(u3, (battle.rng.next() & 1) + 2);
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2671
|
+
fn moveSlot(battle: anytype, moves: []MoveSlot, check_pp: u4) u4 {
|
|
2672
|
+
if (showdown) {
|
|
2673
|
+
if (check_pp == 0) {
|
|
2674
|
+
var i: usize = moves.len;
|
|
2675
|
+
while (i > 0) {
|
|
2676
|
+
i -= 1;
|
|
2677
|
+
if (moves[i].id != .None) {
|
|
2678
|
+
return battle.rng.range(u4, 0, @intCast(u4, i + 1)) + 1;
|
|
2679
|
+
}
|
|
2586
2680
|
}
|
|
2681
|
+
} else {
|
|
2682
|
+
var r = battle.rng.range(u4, 0, @intCast(u4, check_pp)) + 1;
|
|
2683
|
+
var i: usize = 0;
|
|
2684
|
+
while (i < moves.len and r > 0) : (i += 1) {
|
|
2685
|
+
if (moves[i].pp > 0) {
|
|
2686
|
+
r -= 1;
|
|
2687
|
+
if (r == 0) break;
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
return @intCast(u4, i + 1);
|
|
2587
2691
|
}
|
|
2588
|
-
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2694
|
+
while (true) {
|
|
2695
|
+
const r = @intCast(u4, battle.rng.next() & 3);
|
|
2696
|
+
if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
|
|
2589
2697
|
}
|
|
2590
2698
|
}
|
|
2591
2699
|
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2700
|
+
inline fn disableDuration(battle: anytype) u4 {
|
|
2701
|
+
if (showdown) return @intCast(u4, battle.rng.range(u8, 1, 9));
|
|
2702
|
+
return @intCast(u4, (battle.rng.next() & 7) + 1);
|
|
2595
2703
|
}
|
|
2596
|
-
|
|
2704
|
+
|
|
2705
|
+
inline fn metronome(battle: anytype) Move {
|
|
2706
|
+
if (showdown) {
|
|
2707
|
+
const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
|
|
2708
|
+
const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
|
|
2709
|
+
return @intToEnum(Move, r + mod);
|
|
2710
|
+
}
|
|
2711
|
+
while (true) {
|
|
2712
|
+
const r = battle.rng.next();
|
|
2713
|
+
if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
|
|
2714
|
+
if (r >= @enumToInt(Move.Struggle)) continue;
|
|
2715
|
+
return @intToEnum(Move, r);
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
|
|
2719
|
+
inline fn psywave(battle: anytype, max: u8) u8 {
|
|
2720
|
+
if (showdown) return battle.rng.range(u8, 0, max);
|
|
2721
|
+
while (true) {
|
|
2722
|
+
const r = battle.rng.next();
|
|
2723
|
+
if (r < max) return r;
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
|
|
2727
|
+
inline fn distribution(battle: anytype) u3 {
|
|
2728
|
+
if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
|
|
2729
|
+
const r = (battle.rng.next() & 3);
|
|
2730
|
+
return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2733
|
+
inline fn unboost(battle: anytype) bool {
|
|
2734
|
+
if (showdown) return battle.rng.chance(u8, 85, 256);
|
|
2735
|
+
return battle.rng.next() < Gen12.percent(33) + 1;
|
|
2736
|
+
}
|
|
2737
|
+
};
|
|
2597
2738
|
|
|
2598
2739
|
test "RNG agreement" {
|
|
2599
2740
|
if (!showdown) return;
|
|
@@ -2693,9 +2834,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
2693
2834
|
while (slot <= 4) : (slot += 1) {
|
|
2694
2835
|
const m = active.moves[slot - 1];
|
|
2695
2836
|
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
|
|
2837
|
+
if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
|
|
2838
|
+
// Pokémon Showdown displays Struggle if limited to Bide but unable to pick
|
|
2839
|
+
const struggle =
|
|
2840
|
+
m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);
|
|
2841
|
+
const s = if (struggle) 0 else slot;
|
|
2699
2842
|
out[n] = .{ .type = .Move, .data = s };
|
|
2700
2843
|
n += 1;
|
|
2701
2844
|
return n;
|