@pkmn/engine 0.1.0-dev.729b1e87 → 0.1.0-dev.7679dad9
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 +54 -26
- 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 +43 -24
- package/build/pkg/gen1.js.map +1 -1
- package/build/pkg/index.d.ts +15 -12
- package/build/pkg/index.js.map +1 -1
- package/build/pkg/protocol.d.ts +4 -2
- package/build/pkg/protocol.js +5 -3
- package/build/pkg/protocol.js.map +1 -1
- package/build/test/display.js +400 -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 +29 -39
- package/package.json +46 -35
- 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 +119 -85
- package/src/lib/gen1/data.zig +20 -18
- package/src/lib/gen1/helpers.zig +4 -19
- package/src/lib/gen1/mechanics.zig +475 -333
- 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;
|
|
@@ -299,13 +318,11 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
|
|
|
299
318
|
const spe2 = battle.side(.P2).active.stats.spe;
|
|
300
319
|
if (spe1 == spe2) {
|
|
301
320
|
// Pokémon Showdown's beforeTurnCallback shenanigans
|
|
302
|
-
if (showdown and m1 == .Counter and m2 == .Counter)
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
battle.rng.range(u8, 0, 2) == 0
|
|
306
|
-
else
|
|
307
|
-
battle.rng.next() < Gen12.percent(50) + 1;
|
|
321
|
+
if (showdown and !double_switch and m1 == .Counter and m2 == .Counter) {
|
|
322
|
+
battle.rng.advance(1);
|
|
323
|
+
}
|
|
308
324
|
|
|
325
|
+
const p1 = Rolls.speedTie(battle);
|
|
309
326
|
if (!showdown) return if (p1) .P1 else .P2;
|
|
310
327
|
|
|
311
328
|
// Pokémon Showdown's "lockedmove" volatile's onBeforeTurn uses BattleQueue#changeAction,
|
|
@@ -315,7 +332,7 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
|
|
|
315
332
|
const t1 = battle.side(.P1).active.volatiles.Thrashing;
|
|
316
333
|
const t2 = battle.side(.P2).active.volatiles.Thrashing;
|
|
317
334
|
// 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
|
|
335
|
+
// that out and enforce host ordering of events, so P1 just goes first regardless of who
|
|
319
336
|
// won the original coin flip
|
|
320
337
|
if (t1 and t2) return .P1;
|
|
321
338
|
return if (p1) if (t1 and !t2) .P2 else .P1 else if (t2 and !t1) .P1 else .P2;
|
|
@@ -328,9 +345,13 @@ fn doTurn(
|
|
|
328
345
|
battle: anytype,
|
|
329
346
|
player: Player,
|
|
330
347
|
player_choice: Choice,
|
|
348
|
+
player_rewrap: bool,
|
|
349
|
+
player_skip: bool,
|
|
331
350
|
player_from: ?Move,
|
|
332
351
|
foe_player: Player,
|
|
333
352
|
foe_choice: Choice,
|
|
353
|
+
foe_rewrap: bool,
|
|
354
|
+
foe_skip: bool,
|
|
334
355
|
foe_from: ?Move,
|
|
335
356
|
log: anytype,
|
|
336
357
|
) !?Result {
|
|
@@ -338,7 +359,16 @@ fn doTurn(
|
|
|
338
359
|
|
|
339
360
|
var residual = true;
|
|
340
361
|
var replace = battle.side(player).stored().hp == 0;
|
|
341
|
-
if (try executeMove(
|
|
362
|
+
if (try executeMove(
|
|
363
|
+
battle,
|
|
364
|
+
player,
|
|
365
|
+
player_choice,
|
|
366
|
+
player_rewrap,
|
|
367
|
+
player_skip,
|
|
368
|
+
player_from,
|
|
369
|
+
&residual,
|
|
370
|
+
log,
|
|
371
|
+
)) |r| return r;
|
|
342
372
|
if (!replace) {
|
|
343
373
|
if (player_choice.type != .Switch) {
|
|
344
374
|
if (try checkFaint(battle, foe_player, log)) |r| return r;
|
|
@@ -347,8 +377,18 @@ fn doTurn(
|
|
|
347
377
|
if (try checkFaint(battle, player, log)) |r| return r;
|
|
348
378
|
} else if (foe_choice.type == .Pass) return null;
|
|
349
379
|
|
|
380
|
+
residual = true;
|
|
350
381
|
replace = battle.side(foe_player).stored().hp == 0;
|
|
351
|
-
if (try executeMove(
|
|
382
|
+
if (try executeMove(
|
|
383
|
+
battle,
|
|
384
|
+
foe_player,
|
|
385
|
+
foe_choice,
|
|
386
|
+
foe_rewrap,
|
|
387
|
+
foe_skip,
|
|
388
|
+
foe_from,
|
|
389
|
+
&residual,
|
|
390
|
+
log,
|
|
391
|
+
)) |r| return r;
|
|
352
392
|
if (!replace) {
|
|
353
393
|
if (foe_choice.type != .Switch) {
|
|
354
394
|
if (try checkFaint(battle, player, log)) |r| return r;
|
|
@@ -371,6 +411,8 @@ fn executeMove(
|
|
|
371
411
|
battle: anytype,
|
|
372
412
|
player: Player,
|
|
373
413
|
choice: Choice,
|
|
414
|
+
rewrap: bool,
|
|
415
|
+
skip: bool,
|
|
374
416
|
from: ?Move,
|
|
375
417
|
residual: *bool,
|
|
376
418
|
log: anytype,
|
|
@@ -382,9 +424,10 @@ fn executeMove(
|
|
|
382
424
|
return null;
|
|
383
425
|
}
|
|
384
426
|
|
|
427
|
+
// This is the correct place to check for SKIP_TURN and abort early, however since Pokémon
|
|
428
|
+
// Showdown overwrites the SKIP_TURN sentinel with its botched move select we need to add an
|
|
429
|
+
// additional skip boolean to accomplish the same thing in the Binding check of BeforeMove
|
|
385
430
|
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
431
|
assert(!showdown);
|
|
389
432
|
if (battle.foe(player).active.volatiles.Binding) {
|
|
390
433
|
try log.cant(battle.active(player), .Bound);
|
|
@@ -403,9 +446,9 @@ fn executeMove(
|
|
|
403
446
|
// choice.data == 0 only happens with Struggle on Pokémon Showdown
|
|
404
447
|
assert(!showdown);
|
|
405
448
|
mslot = @intCast(u4, if (player == .P1)
|
|
406
|
-
battle.
|
|
449
|
+
battle.last_moves.p1_index
|
|
407
450
|
else
|
|
408
|
-
battle.
|
|
451
|
+
battle.last_moves.p2_index);
|
|
409
452
|
const stored = side.stored();
|
|
410
453
|
// GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
|
|
411
454
|
auto = side.last_selected_move == .HyperBeam or side.active.volatiles.Bide or
|
|
@@ -427,14 +470,14 @@ fn executeMove(
|
|
|
427
470
|
// Incorrect mslot with Pokémon Showdown choice semantics so we need to recover from index
|
|
428
471
|
assert(mslot == 1);
|
|
429
472
|
mslot = @intCast(u4, if (player == .P1)
|
|
430
|
-
battle.
|
|
473
|
+
battle.last_moves.p1_index
|
|
431
474
|
else
|
|
432
|
-
battle.
|
|
475
|
+
battle.last_moves.p2_index);
|
|
433
476
|
}
|
|
434
477
|
|
|
435
478
|
var skip_can = false;
|
|
436
479
|
var skip_pp = false;
|
|
437
|
-
switch (try beforeMove(battle, player, from, log)) {
|
|
480
|
+
switch (try beforeMove(battle, player, skip, from, residual, log)) {
|
|
438
481
|
.done => return null,
|
|
439
482
|
.skip_can => skip_can = true,
|
|
440
483
|
.skip_pp => skip_pp = true,
|
|
@@ -442,14 +485,23 @@ fn executeMove(
|
|
|
442
485
|
.err => return @as(?Result, Result.Error),
|
|
443
486
|
}
|
|
444
487
|
|
|
445
|
-
if (!skip_can and !try canMove(battle, player, mslot, auto, skip_pp, from, log))
|
|
488
|
+
if (!skip_can and !try canMove(battle, player, mslot, auto, skip_pp, from, residual, log)) {
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
446
491
|
|
|
447
|
-
return doMove(battle, player, mslot, auto, residual, log);
|
|
492
|
+
return doMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
448
493
|
}
|
|
449
494
|
|
|
450
|
-
const BeforeMove =
|
|
495
|
+
const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
|
|
451
496
|
|
|
452
|
-
fn beforeMove(
|
|
497
|
+
fn beforeMove(
|
|
498
|
+
battle: anytype,
|
|
499
|
+
player: Player,
|
|
500
|
+
skip: bool,
|
|
501
|
+
from: ?Move,
|
|
502
|
+
residual: *bool,
|
|
503
|
+
log: anytype,
|
|
504
|
+
) !BeforeMove {
|
|
453
505
|
var side = battle.side(player);
|
|
454
506
|
const foe = battle.foe(player);
|
|
455
507
|
var active = &side.active;
|
|
@@ -477,13 +529,15 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
|
|
|
477
529
|
return .done;
|
|
478
530
|
}
|
|
479
531
|
|
|
480
|
-
if (foe.active.volatiles.Binding) {
|
|
532
|
+
if (skip or foe.active.volatiles.Binding) {
|
|
481
533
|
try log.cant(ident, .Bound);
|
|
482
534
|
return .done;
|
|
483
535
|
}
|
|
484
536
|
|
|
485
|
-
if (
|
|
486
|
-
|
|
537
|
+
if (volatiles.Flinch) {
|
|
538
|
+
// Pokémon Showdown doesn't clear Flinch until its imaginary "residual" phase, meaning
|
|
539
|
+
// Pokémon can sometimes flinch multiple times from the same original hit
|
|
540
|
+
if (!showdown) volatiles.Flinch = false;
|
|
487
541
|
try log.cant(ident, .Flinch);
|
|
488
542
|
return .done;
|
|
489
543
|
}
|
|
@@ -504,13 +558,6 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
|
|
|
504
558
|
// Pokémon Showdown's disable condition has a single onBeforeMove handler
|
|
505
559
|
if (showdown and try disabled(side, ident, log)) return .done;
|
|
506
560
|
|
|
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
561
|
// This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
|
|
515
562
|
// before the side had a selected a move - we simply need to assume this leads to a desync
|
|
516
563
|
if (side.last_selected_move == .None) {
|
|
@@ -528,14 +575,9 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
|
|
|
528
575
|
} else {
|
|
529
576
|
try log.activate(ident, .Confusion);
|
|
530
577
|
|
|
531
|
-
|
|
532
|
-
!battle.rng.chance(u8, 128, 256)
|
|
533
|
-
else
|
|
534
|
-
battle.rng.next() >= Gen12.percent(50) + 1;
|
|
535
|
-
|
|
536
|
-
if (confused) {
|
|
578
|
+
if (Rolls.confused(battle)) {
|
|
537
579
|
assert(!volatiles.MultiHit);
|
|
538
|
-
volatiles.state = 0;
|
|
580
|
+
if (!volatiles.Rage) volatiles.state = 0;
|
|
539
581
|
volatiles.Bide = false;
|
|
540
582
|
volatiles.Thrashing = false;
|
|
541
583
|
volatiles.MultiHit = false;
|
|
@@ -559,8 +601,12 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
|
|
|
559
601
|
player
|
|
560
602
|
else
|
|
561
603
|
player.foe();
|
|
604
|
+
|
|
605
|
+
const uncapped = battle.last_damage;
|
|
562
606
|
// Skipping adjustDamage / randomizeDamage / checkHit
|
|
563
607
|
_ = try applyDamage(battle, player, target, .Confusion, log);
|
|
608
|
+
// Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
|
|
609
|
+
if (showdown) battle.last_damage = uncapped;
|
|
564
610
|
|
|
565
611
|
return .done;
|
|
566
612
|
}
|
|
@@ -569,28 +615,28 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
|
|
|
569
615
|
|
|
570
616
|
if (!showdown and try disabled(side, ident, log)) return .done;
|
|
571
617
|
|
|
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
|
-
}
|
|
618
|
+
if (Status.is(stored.status, .PAR) and Rolls.paralyzed(battle)) {
|
|
619
|
+
if (!volatiles.Rage) volatiles.state = 0;
|
|
620
|
+
volatiles.Bide = false;
|
|
621
|
+
volatiles.Thrashing = false;
|
|
622
|
+
volatiles.Charging = false;
|
|
623
|
+
volatiles.Binding = false;
|
|
624
|
+
// GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
|
|
625
|
+
try log.cant(ident, .Paralysis);
|
|
626
|
+
return .done;
|
|
588
627
|
}
|
|
589
628
|
|
|
590
629
|
if (volatiles.Bide) {
|
|
591
630
|
assert(!volatiles.Thrashing and !volatiles.Rage);
|
|
592
631
|
|
|
593
|
-
|
|
632
|
+
if (showdown) {
|
|
633
|
+
// Pokémon Showdown doesn't implement Bide potentially overflowing in the event of
|
|
634
|
+
// OHKO-move damage, but we can fake this incorrect behavior by simply saturating the
|
|
635
|
+
// addition because 65535 is sufficient to faint any Pokémon anyway
|
|
636
|
+
volatiles.state +|= battle.last_damage;
|
|
637
|
+
} else {
|
|
638
|
+
volatiles.state +%= battle.last_damage;
|
|
639
|
+
}
|
|
594
640
|
|
|
595
641
|
assert(volatiles.attacks > 0);
|
|
596
642
|
|
|
@@ -611,7 +657,18 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
|
|
|
611
657
|
return .done;
|
|
612
658
|
}
|
|
613
659
|
|
|
660
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
614
661
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
662
|
+
if (foe.stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
|
|
663
|
+
|
|
664
|
+
// For reasons passing understanding, Pokémon Showdown still inflicts residual damage to
|
|
665
|
+
// Bide's user even if the above damage has caused the foe to faint. It's simpler to always
|
|
666
|
+
// run residual here regardless of whether the foe fainted and opt-out of the default flow
|
|
667
|
+
if (showdown) {
|
|
668
|
+
residual.* = false;
|
|
669
|
+
try handleResidual(battle, player, log);
|
|
670
|
+
}
|
|
671
|
+
|
|
615
672
|
return .done;
|
|
616
673
|
}
|
|
617
674
|
|
|
@@ -623,8 +680,13 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
|
|
|
623
680
|
try log.start(battle.active(player), .ConfusionSilent);
|
|
624
681
|
}
|
|
625
682
|
try log.move(ident, side.last_selected_move, battle.active(player.foe()), from);
|
|
626
|
-
if (showdown
|
|
627
|
-
|
|
683
|
+
if (showdown) {
|
|
684
|
+
// This shouldn't actually set last_used_move, but Pokémon Showdown sets last
|
|
685
|
+
// used in useMove and doesn't have the notion of skipping canMove semantics
|
|
686
|
+
side.last_used_move = side.last_selected_move;
|
|
687
|
+
if (handleThrashing(battle, active)) {
|
|
688
|
+
try log.start(battle.active(player), .ConfusionSilent);
|
|
689
|
+
}
|
|
628
690
|
}
|
|
629
691
|
return .skip_can;
|
|
630
692
|
}
|
|
@@ -639,7 +701,11 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
|
|
|
639
701
|
side.last_selected_move,
|
|
640
702
|
);
|
|
641
703
|
if (showdown or battle.last_damage != 0) {
|
|
704
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
642
705
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
706
|
+
if (battle.foe(player).stored().hp > 0 and !sub) {
|
|
707
|
+
try buildRage(battle, player.foe(), log);
|
|
708
|
+
}
|
|
643
709
|
}
|
|
644
710
|
return .done;
|
|
645
711
|
}
|
|
@@ -654,6 +720,7 @@ fn canMove(
|
|
|
654
720
|
auto: bool,
|
|
655
721
|
skip_pp: bool,
|
|
656
722
|
from: ?Move,
|
|
723
|
+
residual: *bool,
|
|
657
724
|
log: anytype,
|
|
658
725
|
) !bool {
|
|
659
726
|
var side = battle.side(player);
|
|
@@ -670,6 +737,7 @@ fn canMove(
|
|
|
670
737
|
side.active.volatiles.Invulnerable = false;
|
|
671
738
|
} else if (move.effect == .Charge) {
|
|
672
739
|
try log.move(player_ident, side.last_selected_move, .{}, from);
|
|
740
|
+
setCounterable(battle, player, side, move);
|
|
673
741
|
try Effects.charge(battle, player, log);
|
|
674
742
|
return false;
|
|
675
743
|
}
|
|
@@ -686,9 +754,10 @@ fn canMove(
|
|
|
686
754
|
// regrettable, but this information is more "nice to have" than required
|
|
687
755
|
const f = if (from != null and from.? == .None) null else from;
|
|
688
756
|
try log.move(player_ident, side.last_selected_move, battle.active(target), f);
|
|
757
|
+
setCounterable(battle, player, side, move);
|
|
689
758
|
|
|
690
759
|
if (move.effect.onBegin()) {
|
|
691
|
-
try onBegin(battle, player, move, mslot, log);
|
|
760
|
+
try onBegin(battle, player, move, mslot, residual, log);
|
|
692
761
|
return false;
|
|
693
762
|
}
|
|
694
763
|
|
|
@@ -696,12 +765,27 @@ fn canMove(
|
|
|
696
765
|
Effects.thrashing(battle, player);
|
|
697
766
|
} else if (!showdown and move.effect == .Binding) {
|
|
698
767
|
// Pokémon Showdown handles this after hit/miss checks and damage calculation
|
|
699
|
-
Effects.binding(battle, player);
|
|
768
|
+
Effects.binding(battle, player, false);
|
|
700
769
|
}
|
|
701
770
|
|
|
702
771
|
return true;
|
|
703
772
|
}
|
|
704
773
|
|
|
774
|
+
fn setCounterable(battle: anytype, player: Player, side: *Side, move: Move.Data) void {
|
|
775
|
+
// The Counter desync is caused by the cartridge not calling GetCurrentMove until now, meaning
|
|
776
|
+
// in cases where an early return happens the data for a players last selected move does not get
|
|
777
|
+
// reloaded and HandleCounterMove actually bases its success/failure off of stale information.
|
|
778
|
+
// This boolean state we track here doesn't exist on the cartridge because it instead manifests
|
|
779
|
+
// as the desync results from actually having two separate battle states that subtly disagree
|
|
780
|
+
const counterable = side.last_selected_move != .Counter and move.bp > 0 and
|
|
781
|
+
(move.type == .Normal or move.type == .Fighting);
|
|
782
|
+
if (player == .P1) {
|
|
783
|
+
battle.last_moves.p1_counterable = @boolToInt(counterable);
|
|
784
|
+
} else {
|
|
785
|
+
battle.last_moves.p2_counterable = @boolToInt(counterable);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
705
789
|
fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
|
|
706
790
|
if (side.last_selected_move == .Struggle) return;
|
|
707
791
|
|
|
@@ -737,6 +821,7 @@ fn doMove(
|
|
|
737
821
|
battle: anytype,
|
|
738
822
|
player: Player,
|
|
739
823
|
mslot: u4,
|
|
824
|
+
rewrap: bool,
|
|
740
825
|
auto: bool,
|
|
741
826
|
residual: *bool,
|
|
742
827
|
log: anytype,
|
|
@@ -767,16 +852,21 @@ fn doMove(
|
|
|
767
852
|
(@enumToInt(move.type.effectiveness(foe.active.types.type1)) == 0 or
|
|
768
853
|
@enumToInt(move.type.effectiveness(foe.active.types.type2)) == 0);
|
|
769
854
|
if (immune and move.effect != .Binding) break :miss true;
|
|
770
|
-
if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe)
|
|
855
|
+
if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) {
|
|
856
|
+
battle.last_damage = 0;
|
|
857
|
+
break :miss true;
|
|
858
|
+
}
|
|
771
859
|
break :miss move.target != .Self and !moveHit(battle, player, move, &immune, &mist);
|
|
772
860
|
};
|
|
773
861
|
assert(!immune or miss or (showdown and move.effect == .Binding));
|
|
774
862
|
|
|
863
|
+
var late = showdown and move.effect != .Explode;
|
|
775
864
|
const skip = status or immune;
|
|
776
865
|
if ((!showdown or (!skip or counter)) and !miss) blk: {
|
|
777
866
|
if (showdown and move.effect.isMulti()) {
|
|
778
867
|
Effects.multiHit(battle, player, move);
|
|
779
868
|
hits = side.active.volatiles.attacks;
|
|
869
|
+
late = false;
|
|
780
870
|
}
|
|
781
871
|
|
|
782
872
|
// Cartridge rolls for crit even for moves that can't crit (Counter/Metronome/status/OHKO)
|
|
@@ -810,7 +900,7 @@ fn doMove(
|
|
|
810
900
|
}
|
|
811
901
|
|
|
812
902
|
// Due to control flow shenanigans we need to clear last_damage for Pokémon Showdown
|
|
813
|
-
if (showdown and skip
|
|
903
|
+
if (showdown and skip) battle.last_damage = 0;
|
|
814
904
|
|
|
815
905
|
miss = if (showdown or skip)
|
|
816
906
|
miss
|
|
@@ -823,9 +913,9 @@ fn doMove(
|
|
|
823
913
|
|
|
824
914
|
if (!showdown or !miss) {
|
|
825
915
|
if (move.effect == .MirrorMove) {
|
|
826
|
-
return mirrorMove(battle, player, mslot, auto, residual, log);
|
|
916
|
+
return mirrorMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
827
917
|
} else if (move.effect == .Metronome) {
|
|
828
|
-
return metronome(battle, player, mslot, auto, residual, log);
|
|
918
|
+
return metronome(battle, player, mslot, rewrap, auto, residual, log);
|
|
829
919
|
} else if (move.effect.onEnd()) {
|
|
830
920
|
try onEnd(battle, player, move, log);
|
|
831
921
|
return null;
|
|
@@ -840,7 +930,7 @@ fn doMove(
|
|
|
840
930
|
move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
|
|
841
931
|
if (ohko) {
|
|
842
932
|
try log.immune(foe_ident, .OHKO);
|
|
843
|
-
} else if (immune and !invulnerable) {
|
|
933
|
+
} else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
|
|
844
934
|
try log.immune(foe_ident, .None);
|
|
845
935
|
} else if (mist) {
|
|
846
936
|
if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
|
|
@@ -857,15 +947,9 @@ fn doMove(
|
|
|
857
947
|
if (showdown and side.stored().hp == 0) residual.* = false;
|
|
858
948
|
} else if (move.effect == .Explode) {
|
|
859
949
|
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
|
-
}
|
|
950
|
+
try buildRage(battle, player.foe(), log);
|
|
951
|
+
} else if (showdown and move.effect == .Disable) {
|
|
952
|
+
try buildRage(battle, player.foe(), log);
|
|
869
953
|
}
|
|
870
954
|
return null;
|
|
871
955
|
}
|
|
@@ -878,6 +962,9 @@ fn doMove(
|
|
|
878
962
|
hits = side.active.volatiles.attacks;
|
|
879
963
|
}
|
|
880
964
|
|
|
965
|
+
// Pokémon Showdown only builds Rage for Disable/Explosion (hit/miss) when attacking into a sub
|
|
966
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
967
|
+
|
|
881
968
|
var nullified = false;
|
|
882
969
|
var hit: u4 = 0;
|
|
883
970
|
while (hit < hits) {
|
|
@@ -893,9 +980,7 @@ fn doMove(
|
|
|
893
980
|
if (hit == 0 and ohko) try log.ohko();
|
|
894
981
|
hit += 1;
|
|
895
982
|
if (foe.stored().hp == 0) break;
|
|
896
|
-
if (
|
|
897
|
-
try Effects.boost(battle, player.foe(), Move.get(.Rage), log);
|
|
898
|
-
}
|
|
983
|
+
if (!late and (!sub or move.effect == .Explode)) try buildRage(battle, player.foe(), log);
|
|
899
984
|
// If the substitute breaks during a multi-hit attack, the attack ends
|
|
900
985
|
if (nullified) break;
|
|
901
986
|
}
|
|
@@ -911,11 +996,17 @@ fn doMove(
|
|
|
911
996
|
} else if (showdown) {
|
|
912
997
|
// This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
|
|
913
998
|
if (move.effect == .Binding) {
|
|
914
|
-
Effects.binding(battle, player);
|
|
999
|
+
Effects.binding(battle, player, rewrap);
|
|
915
1000
|
if (immune) {
|
|
916
1001
|
battle.last_damage = 0;
|
|
1002
|
+
assert(foe.stored().hp > 0);
|
|
917
1003
|
// Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
|
|
918
|
-
|
|
1004
|
+
if (sub) {
|
|
1005
|
+
try log.activate(battle.active(player.foe()), .Substitute);
|
|
1006
|
+
} else {
|
|
1007
|
+
try log.damage(battle.active(player.foe()), foe.stored(), .None);
|
|
1008
|
+
try buildRage(battle, player.foe(), log);
|
|
1009
|
+
}
|
|
919
1010
|
return null;
|
|
920
1011
|
}
|
|
921
1012
|
}
|
|
@@ -932,6 +1023,9 @@ fn doMove(
|
|
|
932
1023
|
|
|
933
1024
|
if (foe.stored().hp == 0) return null;
|
|
934
1025
|
|
|
1026
|
+
// Pokémon Showdown builds Rage at the wrong time for non-MultiHit move
|
|
1027
|
+
if (late and !sub and move.effect != .Disable) try buildRage(battle, player.foe(), log);
|
|
1028
|
+
|
|
935
1029
|
if (!move.effect.isSpecial()) {
|
|
936
1030
|
// On the cartridge Rage is not considered to be "special" and thus gets executed for a
|
|
937
1031
|
// second time here (after being executed in the "always happens" block above) but that
|
|
@@ -943,7 +1037,9 @@ fn doMove(
|
|
|
943
1037
|
if (move.effect == .Twineedle) {
|
|
944
1038
|
if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), log);
|
|
945
1039
|
} else if (move.effect == .Disable) {
|
|
946
|
-
|
|
1040
|
+
const result = try Effects.disable(battle, player, move, log);
|
|
1041
|
+
if (showdown) try buildRage(battle, player.foe(), log);
|
|
1042
|
+
return result;
|
|
947
1043
|
} else {
|
|
948
1044
|
try moveEffect(battle, player, move, log);
|
|
949
1045
|
}
|
|
@@ -961,8 +1057,7 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
|
|
|
961
1057
|
chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
|
|
962
1058
|
chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
|
|
963
1059
|
|
|
964
|
-
|
|
965
|
-
return std.math.rotl(u8, battle.rng.next(), 3) < chance;
|
|
1060
|
+
return Rolls.criticalHit(battle, chance);
|
|
966
1061
|
}
|
|
967
1062
|
|
|
968
1063
|
fn calcDamage(
|
|
@@ -1066,49 +1161,37 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
|
1066
1161
|
|
|
1067
1162
|
fn randomizeDamage(battle: anytype) void {
|
|
1068
1163
|
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
|
-
|
|
1164
|
+
const random = Rolls.damage(battle);
|
|
1079
1165
|
battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
|
|
1080
1166
|
}
|
|
1081
1167
|
|
|
1082
1168
|
fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
|
|
1083
1169
|
const side = battle.side(player);
|
|
1170
|
+
const foe = battle.foe(player);
|
|
1084
1171
|
|
|
1085
1172
|
if (!try checkHit(battle, player, move, log)) return null;
|
|
1086
1173
|
|
|
1087
1174
|
battle.last_damage = switch (side.last_selected_move) {
|
|
1088
|
-
.SuperFang => @max(
|
|
1175
|
+
.SuperFang => @max(foe.stored().hp / 2, 1),
|
|
1089
1176
|
.SeismicToss, .NightShade => side.stored().level,
|
|
1090
1177
|
.SonicBoom => 20,
|
|
1091
1178
|
.DragonRage => 40,
|
|
1092
1179
|
// GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
|
|
1093
1180
|
.Psywave => power: {
|
|
1094
1181
|
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
|
-
}
|
|
1182
|
+
// GLITCH: Psywave infinite glitch loop
|
|
1183
|
+
if (!showdown and max <= 1) return Result.Error;
|
|
1184
|
+
break :power Rolls.psywave(battle, max);
|
|
1105
1185
|
},
|
|
1106
1186
|
else => unreachable,
|
|
1107
1187
|
};
|
|
1108
1188
|
|
|
1109
1189
|
if (battle.last_damage == 0) return if (showdown) null else Result.Error;
|
|
1110
1190
|
|
|
1191
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1111
1192
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
1193
|
+
if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
|
|
1194
|
+
|
|
1112
1195
|
return null;
|
|
1113
1196
|
}
|
|
1114
1197
|
|
|
@@ -1120,19 +1203,17 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
|
|
|
1120
1203
|
return null;
|
|
1121
1204
|
}
|
|
1122
1205
|
|
|
1123
|
-
// Pretend
|
|
1124
|
-
const foe_last_used_move =
|
|
1125
|
-
Move.get(if (foe.last_used_move == .None) .Counter else foe.last_used_move);
|
|
1206
|
+
// Pretend Splash was used as a stand-in when no move has been used to fail below with 0 BP
|
|
1126
1207
|
const foe_last_selected_move =
|
|
1127
1208
|
Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
|
|
1128
|
-
.
|
|
1209
|
+
.Splash
|
|
1129
1210
|
else
|
|
1130
1211
|
foe.last_selected_move);
|
|
1131
1212
|
|
|
1132
|
-
const used =
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1213
|
+
const used = (if (player.foe() == .P1)
|
|
1214
|
+
battle.last_moves.p1_counterable
|
|
1215
|
+
else
|
|
1216
|
+
battle.last_moves.p2_counterable) != 0;
|
|
1136
1217
|
|
|
1137
1218
|
const selected = foe_last_selected_move.bp > 0 and
|
|
1138
1219
|
foe.last_selected_move != .Counter and
|
|
@@ -1151,12 +1232,14 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
|
|
|
1151
1232
|
return null;
|
|
1152
1233
|
}
|
|
1153
1234
|
|
|
1154
|
-
battle.last_damage
|
|
1235
|
+
battle.last_damage *|= 2;
|
|
1155
1236
|
|
|
1156
1237
|
// Pokémon Showdown calls checkHit before Counter
|
|
1157
1238
|
if (!showdown and !try checkHit(battle, player, move, log)) return null;
|
|
1158
1239
|
|
|
1240
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1159
1241
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
1242
|
+
if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
|
|
1160
1243
|
return null;
|
|
1161
1244
|
}
|
|
1162
1245
|
|
|
@@ -1200,6 +1283,7 @@ fn mirrorMove(
|
|
|
1200
1283
|
battle: anytype,
|
|
1201
1284
|
player: Player,
|
|
1202
1285
|
mslot: u4,
|
|
1286
|
+
rewrap: bool,
|
|
1203
1287
|
auto: bool,
|
|
1204
1288
|
residual: *bool,
|
|
1205
1289
|
log: anytype,
|
|
@@ -1212,41 +1296,30 @@ fn mirrorMove(
|
|
|
1212
1296
|
if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
|
|
1213
1297
|
try log.fail(battle.active(player), .None);
|
|
1214
1298
|
return null;
|
|
1299
|
+
} else if (!showdown or foe.last_used_move != .Struggle) {
|
|
1300
|
+
incrementPP(side, mslot);
|
|
1215
1301
|
}
|
|
1216
1302
|
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, log)) return null;
|
|
1220
|
-
return doMove(battle, player, mslot, auto, residual, log);
|
|
1303
|
+
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
|
|
1304
|
+
return doMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
1221
1305
|
}
|
|
1222
1306
|
|
|
1223
1307
|
fn metronome(
|
|
1224
1308
|
battle: anytype,
|
|
1225
1309
|
player: Player,
|
|
1226
1310
|
mslot: u4,
|
|
1311
|
+
rewrap: bool,
|
|
1227
1312
|
auto: bool,
|
|
1228
1313
|
residual: *bool,
|
|
1229
1314
|
log: anytype,
|
|
1230
1315
|
) !?Result {
|
|
1231
1316
|
var side = battle.side(player);
|
|
1232
1317
|
|
|
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
|
-
|
|
1318
|
+
side.last_selected_move = Rolls.metronome(battle);
|
|
1246
1319
|
incrementPP(side, mslot);
|
|
1247
1320
|
|
|
1248
|
-
if (!try canMove(battle, player, mslot, auto, false, .Metronome, log)) return null;
|
|
1249
|
-
return doMove(battle, player, mslot, auto, residual, log);
|
|
1321
|
+
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
|
|
1322
|
+
return doMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
1250
1323
|
}
|
|
1251
1324
|
|
|
1252
1325
|
fn checkHit(battle: anytype, player: Player, move: Move.Data, log: anytype) !bool {
|
|
@@ -1300,23 +1373,24 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
|
|
|
1300
1373
|
}
|
|
1301
1374
|
|
|
1302
1375
|
// GLITCH: Thrash / Petal Dance / Rage get their accuracy overwritten on subsequent hits
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1376
|
+
const state = side.active.volatiles.state;
|
|
1377
|
+
var overwrite = move.effect == .Rage or move.effect == .Thrashing;
|
|
1378
|
+
const overwritten = overwrite and state > 0;
|
|
1379
|
+
assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
|
|
1380
|
+
|
|
1381
|
+
var accuracy = if (overwritten) state else @as(u16, Gen12.percent(move.accuracy));
|
|
1307
1382
|
var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
|
|
1308
1383
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1309
1384
|
boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
|
|
1310
1385
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1311
1386
|
accuracy = @min(255, @max(1, accuracy));
|
|
1312
1387
|
|
|
1313
|
-
|
|
1388
|
+
// Pokémon Showdown only overwrites if the volatile is present
|
|
1389
|
+
if (showdown) overwrite = side.active.volatiles.Rage or side.active.volatiles.Thrashing;
|
|
1390
|
+
if (overwrite) side.active.volatiles.state = accuracy;
|
|
1314
1391
|
|
|
1315
1392
|
// 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;
|
|
1393
|
+
break :miss !Rolls.hit(battle, accuracy);
|
|
1320
1394
|
};
|
|
1321
1395
|
|
|
1322
1396
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1431,6 +1505,15 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
|
|
|
1431
1505
|
}
|
|
1432
1506
|
|
|
1433
1507
|
if (volatiles.LeechSeed) {
|
|
1508
|
+
var foe = battle.foe(player);
|
|
1509
|
+
var foe_stored = foe.stored();
|
|
1510
|
+
const foe_ident = battle.active(player.foe());
|
|
1511
|
+
|
|
1512
|
+
if (foe_stored.hp == 0) {
|
|
1513
|
+
assert(showdown);
|
|
1514
|
+
return;
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1434
1517
|
var damage = @max(stored.stats.hp / 16, 1);
|
|
1435
1518
|
|
|
1436
1519
|
// GLITCH: Leech Seed + Toxic glitch
|
|
@@ -1442,10 +1525,6 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
|
|
|
1442
1525
|
const amount = @min(damage, stored.hp);
|
|
1443
1526
|
stored.hp -= amount;
|
|
1444
1527
|
|
|
1445
|
-
var foe = battle.foe(player);
|
|
1446
|
-
var foe_stored = foe.stored();
|
|
1447
|
-
const foe_ident = battle.active(player.foe());
|
|
1448
|
-
|
|
1449
1528
|
// As above, Pokémon Showdown uses damageOf but its not relevant
|
|
1450
1529
|
if (amount > 0) try log.damage(ident, stored, .LeechSeed);
|
|
1451
1530
|
|
|
@@ -1461,13 +1540,13 @@ fn endTurn(battle: anytype, log: anytype) @TypeOf(log).Error!Result {
|
|
|
1461
1540
|
assert(!battle.side(.P1).active.volatiles.MultiHit);
|
|
1462
1541
|
assert(!battle.side(.P2).active.volatiles.MultiHit);
|
|
1463
1542
|
|
|
1543
|
+
battle.turn += 1;
|
|
1544
|
+
|
|
1464
1545
|
if (showdown and options.ebc and checkEBC(battle)) {
|
|
1465
1546
|
try log.tie();
|
|
1466
1547
|
return Result.Tie;
|
|
1467
1548
|
}
|
|
1468
1549
|
|
|
1469
|
-
battle.turn += 1;
|
|
1470
|
-
|
|
1471
1550
|
if (showdown and battle.turn >= 1000) {
|
|
1472
1551
|
try log.tie();
|
|
1473
1552
|
return Result.Tie;
|
|
@@ -1534,7 +1613,50 @@ fn checkEBC(battle: anytype) bool {
|
|
|
1534
1613
|
return false;
|
|
1535
1614
|
}
|
|
1536
1615
|
|
|
1537
|
-
|
|
1616
|
+
fn disabled(side: *Side, ident: ID, log: anytype) !bool {
|
|
1617
|
+
if (side.active.volatiles.disabled_move != 0) {
|
|
1618
|
+
// A Pokémon that transforms after being disabled may end up with less move slots
|
|
1619
|
+
const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
|
|
1620
|
+
// side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
|
|
1621
|
+
const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
|
|
1622
|
+
m
|
|
1623
|
+
else
|
|
1624
|
+
side.last_selected_move;
|
|
1625
|
+
if (m != .None and m == last) {
|
|
1626
|
+
side.active.volatiles.Charging = false;
|
|
1627
|
+
try log.disabled(ident, last);
|
|
1628
|
+
return true;
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
return false;
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
inline fn buildRage(battle: anytype, who: Player, log: anytype) !void {
|
|
1635
|
+
const side = battle.side(who);
|
|
1636
|
+
if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
|
|
1637
|
+
try Effects.boost(battle, who, Move.get(.Rage), log);
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
|
|
1642
|
+
var volatiles = &active.volatiles;
|
|
1643
|
+
assert(volatiles.Thrashing);
|
|
1644
|
+
if (volatiles.attacks > 0) return false;
|
|
1645
|
+
|
|
1646
|
+
volatiles.Thrashing = false;
|
|
1647
|
+
volatiles.Confusion = true;
|
|
1648
|
+
volatiles.confusion = Rolls.confusionDuration(battle);
|
|
1649
|
+
return true;
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
inline fn onBegin(
|
|
1653
|
+
battle: anytype,
|
|
1654
|
+
player: Player,
|
|
1655
|
+
move: Move.Data,
|
|
1656
|
+
mslot: u8,
|
|
1657
|
+
residual: *bool,
|
|
1658
|
+
log: anytype,
|
|
1659
|
+
) !void {
|
|
1538
1660
|
assert(move.effect.onBegin());
|
|
1539
1661
|
return switch (move.effect) {
|
|
1540
1662
|
.Confusion => Effects.confusion(battle, player, move, log),
|
|
@@ -1550,7 +1672,7 @@ inline fn onBegin(battle: anytype, player: Player, move: Move.Data, mslot: u8, l
|
|
|
1550
1672
|
.Poison => Effects.poison(battle, player, move, log),
|
|
1551
1673
|
.Reflect => Effects.reflect(battle, player, log),
|
|
1552
1674
|
.Splash => Effects.splash(battle, player, log),
|
|
1553
|
-
.Substitute => Effects.substitute(battle, player, log),
|
|
1675
|
+
.Substitute => Effects.substitute(battle, player, residual, log),
|
|
1554
1676
|
.SwitchAndTeleport => Effects.switchAndTeleport(battle, player, move, log),
|
|
1555
1677
|
.Transform => Effects.transform(battle, player, log),
|
|
1556
1678
|
else => unreachable,
|
|
@@ -1617,10 +1739,7 @@ pub const Effects = struct {
|
|
|
1617
1739
|
side.active.volatiles.Bide = true;
|
|
1618
1740
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1619
1741
|
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);
|
|
1742
|
+
side.active.volatiles.attacks = Rolls.bideThrashDuration(battle);
|
|
1624
1743
|
|
|
1625
1744
|
try log.start(battle.active(player), .Bide);
|
|
1626
1745
|
}
|
|
@@ -1642,14 +1761,8 @@ pub const Effects = struct {
|
|
|
1642
1761
|
return;
|
|
1643
1762
|
}
|
|
1644
1763
|
|
|
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;
|
|
1764
|
+
if (foe.active.types.includes(move.type)) return;
|
|
1765
|
+
if (!Rolls.secondaryChance(battle, move.effect == .BurnChance1)) return;
|
|
1653
1766
|
|
|
1654
1767
|
foe_stored.status = Status.init(.BRN);
|
|
1655
1768
|
foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
|
|
@@ -1673,11 +1786,7 @@ pub const Effects = struct {
|
|
|
1673
1786
|
const sub = foe.active.volatiles.Substitute;
|
|
1674
1787
|
|
|
1675
1788
|
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;
|
|
1789
|
+
if (!Rolls.confusionChance(battle)) return;
|
|
1681
1790
|
} else {
|
|
1682
1791
|
if (showdown) {
|
|
1683
1792
|
if (!try checkHit(battle, player, move, log)) {
|
|
@@ -1696,10 +1805,7 @@ pub const Effects = struct {
|
|
|
1696
1805
|
|
|
1697
1806
|
if (foe.active.volatiles.Confusion) return;
|
|
1698
1807
|
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);
|
|
1808
|
+
foe.active.volatiles.confusion = Rolls.confusionDuration(battle);
|
|
1703
1809
|
|
|
1704
1810
|
try log.start(battle.active(player.foe()), .Confusion);
|
|
1705
1811
|
}
|
|
@@ -1749,11 +1855,8 @@ pub const Effects = struct {
|
|
|
1749
1855
|
return Result.Error;
|
|
1750
1856
|
}
|
|
1751
1857
|
|
|
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);
|
|
1858
|
+
volatiles.disabled_move = @intCast(u3, Rolls.moveSlot(battle, &foe.active.moves, n));
|
|
1859
|
+
volatiles.disabled_duration = Rolls.disableDuration(battle);
|
|
1757
1860
|
|
|
1758
1861
|
try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
|
|
1759
1862
|
return null;
|
|
@@ -1765,7 +1868,7 @@ pub const Effects = struct {
|
|
|
1765
1868
|
|
|
1766
1869
|
if (battle.last_damage == 0) {
|
|
1767
1870
|
assert(showdown);
|
|
1768
|
-
return;
|
|
1871
|
+
if (!battle.foe(player).active.volatiles.Substitute) return;
|
|
1769
1872
|
}
|
|
1770
1873
|
|
|
1771
1874
|
const drain = @max(battle.last_damage / 2, 1);
|
|
@@ -1792,15 +1895,7 @@ pub const Effects = struct {
|
|
|
1792
1895
|
var volatiles = &battle.foe(player).active.volatiles;
|
|
1793
1896
|
|
|
1794
1897
|
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;
|
|
1898
|
+
if (!Rolls.secondaryChance(battle, move.effect == .FlinchChance1)) return;
|
|
1804
1899
|
|
|
1805
1900
|
volatiles.Flinch = true;
|
|
1806
1901
|
volatiles.Recharging = false;
|
|
@@ -1826,12 +1921,8 @@ pub const Effects = struct {
|
|
|
1826
1921
|
return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
|
|
1827
1922
|
}
|
|
1828
1923
|
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
else
|
|
1832
|
-
battle.rng.next() < 1 + Gen12.percent(10);
|
|
1833
|
-
if (!chance) return;
|
|
1834
|
-
|
|
1924
|
+
if (foe.active.types.includes(move.type)) return;
|
|
1925
|
+
if (!Rolls.secondaryChance(battle, true)) return;
|
|
1835
1926
|
// Freeze Clause Mod
|
|
1836
1927
|
if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
1837
1928
|
|
|
@@ -1968,15 +2059,15 @@ pub const Effects = struct {
|
|
|
1968
2059
|
}
|
|
1969
2060
|
break :has_mimic false;
|
|
1970
2061
|
};
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
2062
|
+
if (!has_mimic) {
|
|
2063
|
+
// Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
|
|
2064
|
+
if (!try checkHit(battle, player, move, log)) return;
|
|
1974
2065
|
return try log.fail(battle.active(player.foe()), .None);
|
|
1975
2066
|
}
|
|
1976
2067
|
}
|
|
1977
2068
|
if (!try checkHit(battle, player, move, log)) return;
|
|
1978
2069
|
|
|
1979
|
-
const rslot =
|
|
2070
|
+
const rslot = Rolls.moveSlot(battle, &foe.active.moves, 0);
|
|
1980
2071
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
1981
2072
|
|
|
1982
2073
|
try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
|
|
@@ -1997,7 +2088,8 @@ pub const Effects = struct {
|
|
|
1997
2088
|
assert(!side.active.volatiles.MultiHit);
|
|
1998
2089
|
side.active.volatiles.MultiHit = true;
|
|
1999
2090
|
|
|
2000
|
-
side.active.volatiles.attacks =
|
|
2091
|
+
side.active.volatiles.attacks =
|
|
2092
|
+
if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
|
|
2001
2093
|
}
|
|
2002
2094
|
|
|
2003
2095
|
fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2041,14 +2133,8 @@ pub const Effects = struct {
|
|
|
2041
2133
|
}
|
|
2042
2134
|
|
|
2043
2135
|
// 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;
|
|
2136
|
+
if (foe.active.types.includes(move.type)) return;
|
|
2137
|
+
if (!Rolls.secondaryChance(battle, move.effect == .ParalyzeChance1)) return;
|
|
2052
2138
|
|
|
2053
2139
|
foe_stored.status = Status.init(.PAR);
|
|
2054
2140
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
@@ -2091,16 +2177,7 @@ pub const Effects = struct {
|
|
|
2091
2177
|
|
|
2092
2178
|
if (move.effect == .Poison) {
|
|
2093
2179
|
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
|
-
}
|
|
2180
|
+
} else if (!Rolls.poisonChance(battle, move.effect == .PoisonChance1)) return;
|
|
2104
2181
|
|
|
2105
2182
|
foe_stored.status = Status.init(.PSN);
|
|
2106
2183
|
if (toxic) {
|
|
@@ -2114,16 +2191,17 @@ pub const Effects = struct {
|
|
|
2114
2191
|
|
|
2115
2192
|
fn rage(battle: anytype, player: Player) !void {
|
|
2116
2193
|
var volatiles = &battle.side(player).active.volatiles;
|
|
2117
|
-
if (volatiles.Rage) return;
|
|
2118
|
-
volatiles.Rage = true;
|
|
2119
2194
|
assert(!volatiles.Bide);
|
|
2120
|
-
volatiles.
|
|
2195
|
+
volatiles.Rage = true;
|
|
2121
2196
|
}
|
|
2122
2197
|
|
|
2123
2198
|
fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
|
|
2124
2199
|
var side = battle.side(player);
|
|
2125
2200
|
var stored = side.stored();
|
|
2126
2201
|
|
|
2202
|
+
assert(showdown or battle.last_damage > 0);
|
|
2203
|
+
if (showdown and battle.last_damage == 0) return;
|
|
2204
|
+
|
|
2127
2205
|
const damage = @intCast(i16, @max(battle.last_damage /
|
|
2128
2206
|
@as(u8, if (side.last_selected_move == .Struggle) 2 else 4), 1));
|
|
2129
2207
|
stored.hp = @intCast(u16, @max(@intCast(i16, stored.hp) - damage, 0));
|
|
@@ -2169,16 +2247,7 @@ pub const Effects = struct {
|
|
|
2169
2247
|
}
|
|
2170
2248
|
foe.active.volatiles.Recharging = false;
|
|
2171
2249
|
|
|
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);
|
|
2250
|
+
foe_stored.status = Status.slp(Rolls.sleepDuration(battle));
|
|
2182
2251
|
try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
|
|
2183
2252
|
}
|
|
2184
2253
|
|
|
@@ -2186,7 +2255,7 @@ pub const Effects = struct {
|
|
|
2186
2255
|
try log.activate(battle.active(player), .Splash);
|
|
2187
2256
|
}
|
|
2188
2257
|
|
|
2189
|
-
fn substitute(battle: anytype, player: Player, log: anytype) !void {
|
|
2258
|
+
fn substitute(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
|
|
2190
2259
|
var side = battle.side(player);
|
|
2191
2260
|
if (side.active.volatiles.Substitute) {
|
|
2192
2261
|
try log.fail(battle.active(player), .Substitute);
|
|
@@ -2211,14 +2280,18 @@ pub const Effects = struct {
|
|
|
2211
2280
|
side.active.volatiles.substitute = hp + 1;
|
|
2212
2281
|
side.active.volatiles.Substitute = true;
|
|
2213
2282
|
try log.start(battle.active(player), .Substitute);
|
|
2214
|
-
if (hp > 0)
|
|
2283
|
+
if (hp > 0) {
|
|
2284
|
+
try log.damage(battle.active(player), side.stored(), .None);
|
|
2285
|
+
if (showdown and side.stored().hp == 0) residual.* = false;
|
|
2286
|
+
}
|
|
2215
2287
|
}
|
|
2216
2288
|
|
|
2217
2289
|
fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
2218
2290
|
if (!showdown or battle.side(player).last_selected_move == .Teleport) return;
|
|
2219
2291
|
|
|
2220
|
-
// Whirlwind/Roar should not roll to hit but Pokémon Showdown does anyway
|
|
2292
|
+
// Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
|
|
2221
2293
|
_ = try checkHit(battle, player, move, log);
|
|
2294
|
+
battle.last_damage = 0;
|
|
2222
2295
|
}
|
|
2223
2296
|
|
|
2224
2297
|
fn thrashing(battle: anytype, player: Player) void {
|
|
@@ -2227,11 +2300,7 @@ pub const Effects = struct {
|
|
|
2227
2300
|
assert(!volatiles.Bide);
|
|
2228
2301
|
|
|
2229
2302
|
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);
|
|
2303
|
+
volatiles.attacks = Rolls.bideThrashDuration(battle);
|
|
2235
2304
|
}
|
|
2236
2305
|
|
|
2237
2306
|
fn transform(battle: anytype, player: Player, log: anytype) !void {
|
|
@@ -2263,7 +2332,7 @@ pub const Effects = struct {
|
|
|
2263
2332
|
try log.transform(battle.active(player), foe_ident);
|
|
2264
2333
|
}
|
|
2265
2334
|
|
|
2266
|
-
fn binding(battle: anytype, player: Player) void {
|
|
2335
|
+
fn binding(battle: anytype, player: Player, rewrap: bool) void {
|
|
2267
2336
|
var side = battle.side(player);
|
|
2268
2337
|
var foe = battle.foe(player);
|
|
2269
2338
|
|
|
@@ -2271,9 +2340,11 @@ pub const Effects = struct {
|
|
|
2271
2340
|
side.active.volatiles.Binding = true;
|
|
2272
2341
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2273
2342
|
// (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
|
|
2274
|
-
if (!showdown)
|
|
2343
|
+
if (!showdown) {
|
|
2344
|
+
foe.active.volatiles.Recharging = false;
|
|
2345
|
+
} else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
|
|
2275
2346
|
|
|
2276
|
-
side.active.volatiles.attacks = distribution(battle) - 1;
|
|
2347
|
+
side.active.volatiles.attacks = Rolls.distribution(battle) - 1;
|
|
2277
2348
|
}
|
|
2278
2349
|
|
|
2279
2350
|
fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2300,9 +2371,11 @@ pub const Effects = struct {
|
|
|
2300
2371
|
return try log.fail(ident, .None);
|
|
2301
2372
|
}
|
|
2302
2373
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2303
|
-
const stat = unmodifiedStats(battle,
|
|
2374
|
+
const stat = unmodifiedStats(battle, side).atk;
|
|
2304
2375
|
stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2305
2376
|
try log.boost(ident, reason, n);
|
|
2377
|
+
// Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
|
|
2378
|
+
if (showdown and move.effect == .Rage) return;
|
|
2306
2379
|
},
|
|
2307
2380
|
.DefenseUp1, .DefenseUp2 => {
|
|
2308
2381
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
@@ -2318,7 +2391,7 @@ pub const Effects = struct {
|
|
|
2318
2391
|
return try log.fail(ident, .None);
|
|
2319
2392
|
}
|
|
2320
2393
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2321
|
-
const stat = unmodifiedStats(battle,
|
|
2394
|
+
const stat = unmodifiedStats(battle, side).def;
|
|
2322
2395
|
stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2323
2396
|
try log.boost(ident, .Defense, n);
|
|
2324
2397
|
},
|
|
@@ -2335,7 +2408,7 @@ pub const Effects = struct {
|
|
|
2335
2408
|
return try log.fail(ident, .None);
|
|
2336
2409
|
}
|
|
2337
2410
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2338
|
-
const stat = unmodifiedStats(battle,
|
|
2411
|
+
const stat = unmodifiedStats(battle, side).spe;
|
|
2339
2412
|
stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2340
2413
|
try log.boost(ident, .Speed, 2);
|
|
2341
2414
|
},
|
|
@@ -2355,7 +2428,7 @@ pub const Effects = struct {
|
|
|
2355
2428
|
return try log.fail(ident, .None);
|
|
2356
2429
|
}
|
|
2357
2430
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2358
|
-
const stat = unmodifiedStats(battle,
|
|
2431
|
+
const stat = unmodifiedStats(battle, side).spc;
|
|
2359
2432
|
stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2360
2433
|
try log.boost(ident, .SpecialAttack, n);
|
|
2361
2434
|
try log.boost(ident, .SpecialDefense, n);
|
|
@@ -2376,28 +2449,24 @@ pub const Effects = struct {
|
|
|
2376
2449
|
fn unboost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
2377
2450
|
var foe = battle.foe(player);
|
|
2378
2451
|
const foe_ident = battle.active(player.foe());
|
|
2452
|
+
const secondary = move.effect.isStatDownChance();
|
|
2379
2453
|
|
|
2380
|
-
if (foe.active.volatiles.Substitute)
|
|
2381
|
-
return if (!move.effect.isStatDownChance()) log.fail(foe_ident, .None);
|
|
2382
|
-
}
|
|
2454
|
+
if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(foe_ident, .None);
|
|
2383
2455
|
|
|
2384
|
-
if (
|
|
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;
|
|
2456
|
+
if (secondary) {
|
|
2457
|
+
if (!Rolls.unboost(battle) or foe.active.volatiles.Invulnerable) return;
|
|
2390
2458
|
} else if (!showdown and !try checkHit(battle, player, move, log)) {
|
|
2391
2459
|
return; // checkHit already checks for Invulnerable
|
|
2392
2460
|
}
|
|
2393
2461
|
|
|
2394
2462
|
var stats = &foe.active.stats;
|
|
2395
2463
|
var boosts = &foe.active.boosts;
|
|
2464
|
+
const fail = showdown or !secondary;
|
|
2396
2465
|
|
|
2397
2466
|
switch (move.effect) {
|
|
2398
2467
|
.AttackDown1, .AttackDownChance => {
|
|
2399
2468
|
assert(boosts.atk >= -6 and boosts.atk <= 6);
|
|
2400
|
-
if (boosts.atk == -6) return try log.fail(foe_ident, .None);
|
|
2469
|
+
if (boosts.atk == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2401
2470
|
boosts.atk = @intCast(i4, @max(-6, @as(i8, boosts.atk) - 1));
|
|
2402
2471
|
if (stats.atk == 1) {
|
|
2403
2472
|
boosts.atk += 1;
|
|
@@ -2408,13 +2477,13 @@ pub const Effects = struct {
|
|
|
2408
2477
|
return try log.fail(foe_ident, .None);
|
|
2409
2478
|
}
|
|
2410
2479
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2411
|
-
const stat = unmodifiedStats(battle,
|
|
2480
|
+
const stat = unmodifiedStats(battle, foe).atk;
|
|
2412
2481
|
stats.atk = @max(1, stat * mod[0] / mod[1]);
|
|
2413
2482
|
try log.boost(foe_ident, .Attack, -1);
|
|
2414
2483
|
},
|
|
2415
2484
|
.DefenseDown1, .DefenseDown2, .DefenseDownChance => {
|
|
2416
2485
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
2417
|
-
if (boosts.def == -6) return try log.fail(foe_ident, .None);
|
|
2486
|
+
if (boosts.def == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2418
2487
|
const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
|
|
2419
2488
|
boosts.def = @intCast(i4, @max(-6, @as(i8, boosts.def) - n));
|
|
2420
2489
|
if (stats.def == 1) {
|
|
@@ -2426,13 +2495,13 @@ pub const Effects = struct {
|
|
|
2426
2495
|
return try log.fail(foe_ident, .None);
|
|
2427
2496
|
}
|
|
2428
2497
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2429
|
-
const stat = unmodifiedStats(battle,
|
|
2498
|
+
const stat = unmodifiedStats(battle, foe).def;
|
|
2430
2499
|
stats.def = @max(1, stat * mod[0] / mod[1]);
|
|
2431
2500
|
try log.boost(foe_ident, .Defense, -@as(i8, n));
|
|
2432
2501
|
},
|
|
2433
2502
|
.SpeedDown1, .SpeedDownChance => {
|
|
2434
2503
|
assert(boosts.spe >= -6 and boosts.spe <= 6);
|
|
2435
|
-
if (boosts.spe == -6) return try log.fail(foe_ident, .None);
|
|
2504
|
+
if (boosts.spe == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2436
2505
|
boosts.spe = @intCast(i4, @max(-6, @as(i8, boosts.spe) - 1));
|
|
2437
2506
|
if (stats.spe == 1) {
|
|
2438
2507
|
boosts.spe += 1;
|
|
@@ -2443,14 +2512,14 @@ pub const Effects = struct {
|
|
|
2443
2512
|
return try log.fail(foe_ident, .None);
|
|
2444
2513
|
}
|
|
2445
2514
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2446
|
-
const stat = unmodifiedStats(battle,
|
|
2515
|
+
const stat = unmodifiedStats(battle, foe).spe;
|
|
2447
2516
|
stats.spe = @max(1, stat * mod[0] / mod[1]);
|
|
2448
2517
|
try log.boost(foe_ident, .Speed, -1);
|
|
2449
2518
|
assert(boosts.spe >= -6);
|
|
2450
2519
|
},
|
|
2451
2520
|
.SpecialDownChance => {
|
|
2452
2521
|
assert(boosts.spc >= -6 and boosts.spc <= 6);
|
|
2453
|
-
if (boosts.spc == -6) return try log.fail(foe_ident, .None);
|
|
2522
|
+
if (boosts.spc == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2454
2523
|
boosts.spc = @intCast(i4, @max(-6, @as(i8, boosts.spc) - 1));
|
|
2455
2524
|
if (stats.spc == 1) {
|
|
2456
2525
|
boosts.spc += 1;
|
|
@@ -2463,14 +2532,14 @@ pub const Effects = struct {
|
|
|
2463
2532
|
return try log.fail(foe_ident, .None);
|
|
2464
2533
|
}
|
|
2465
2534
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2466
|
-
const stat = unmodifiedStats(battle,
|
|
2535
|
+
const stat = unmodifiedStats(battle, foe).spc;
|
|
2467
2536
|
stats.spc = @max(1, stat * mod[0] / mod[1]);
|
|
2468
2537
|
try log.boost(foe_ident, .SpecialAttack, -1);
|
|
2469
2538
|
try log.boost(foe_ident, .SpecialDefense, -1);
|
|
2470
2539
|
},
|
|
2471
2540
|
.AccuracyDown1 => {
|
|
2472
2541
|
assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
|
|
2473
|
-
if (boosts.accuracy == -6) return try log.fail(foe_ident, .None);
|
|
2542
|
+
if (boosts.accuracy == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2474
2543
|
boosts.accuracy = @intCast(i4, @max(-6, @as(i8, boosts.accuracy) - 1));
|
|
2475
2544
|
try log.boost(foe_ident, .Accuracy, -1);
|
|
2476
2545
|
},
|
|
@@ -2482,8 +2551,7 @@ pub const Effects = struct {
|
|
|
2482
2551
|
}
|
|
2483
2552
|
};
|
|
2484
2553
|
|
|
2485
|
-
fn unmodifiedStats(battle: anytype,
|
|
2486
|
-
const side = battle.side(who);
|
|
2554
|
+
fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
|
|
2487
2555
|
if (!side.active.volatiles.Transform) return &side.stored().stats;
|
|
2488
2556
|
const id = ID.from(side.active.volatiles.transform);
|
|
2489
2557
|
return &battle.side(id.player).pokemon[id.id - 1].stats;
|
|
@@ -2497,7 +2565,7 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
|
|
|
2497
2565
|
}
|
|
2498
2566
|
}
|
|
2499
2567
|
|
|
2500
|
-
fn isForced(active: ActivePokemon) bool {
|
|
2568
|
+
inline fn isForced(active: ActivePokemon) bool {
|
|
2501
2569
|
return active.volatiles.Recharging or active.volatiles.Rage or
|
|
2502
2570
|
active.volatiles.Thrashing or active.volatiles.Charging;
|
|
2503
2571
|
}
|
|
@@ -2549,68 +2617,140 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
|
|
|
2549
2617
|
}
|
|
2550
2618
|
}
|
|
2551
2619
|
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2620
|
+
pub const Rolls = struct {
|
|
2621
|
+
inline fn speedTie(battle: anytype) bool {
|
|
2622
|
+
if (showdown) return battle.rng.range(u8, 0, 2) == 0;
|
|
2623
|
+
return battle.rng.next() < Gen12.percent(50) + 1;
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2626
|
+
inline fn criticalHit(battle: anytype, chance: u16) bool {
|
|
2627
|
+
if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
|
|
2628
|
+
return std.math.rotl(u8, battle.rng.next(), 3) < chance;
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2631
|
+
inline fn damage(battle: anytype) u8 {
|
|
2632
|
+
if (showdown) return battle.rng.range(u8, 217, 256);
|
|
2633
|
+
while (true) {
|
|
2634
|
+
const r = std.math.rotr(u8, battle.rng.next(), 1);
|
|
2635
|
+
if (r >= 217) return r;
|
|
2560
2636
|
}
|
|
2561
2637
|
}
|
|
2562
|
-
return false;
|
|
2563
|
-
}
|
|
2564
2638
|
|
|
2565
|
-
fn
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2639
|
+
inline fn hit(battle: anytype, accuracy: u16) bool {
|
|
2640
|
+
if (showdown) return battle.rng.chance(u8, @intCast(u8, accuracy), 256);
|
|
2641
|
+
return battle.rng.next() < accuracy;
|
|
2642
|
+
}
|
|
2569
2643
|
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
else
|
|
2575
|
-
(battle.rng.next() & 3) + 2);
|
|
2644
|
+
inline fn confusionDuration(battle: anytype) u3 {
|
|
2645
|
+
if (showdown) return @intCast(u3, battle.rng.range(u8, 2, 6));
|
|
2646
|
+
return @intCast(u3, (battle.rng.next() & 3) + 2);
|
|
2647
|
+
}
|
|
2576
2648
|
|
|
2577
|
-
|
|
2578
|
-
|
|
2649
|
+
inline fn confused(battle: anytype) bool {
|
|
2650
|
+
if (showdown) return !battle.rng.chance(u8, 128, 256);
|
|
2651
|
+
return battle.rng.next() >= Gen12.percent(50) + 1;
|
|
2652
|
+
}
|
|
2579
2653
|
|
|
2580
|
-
|
|
2654
|
+
inline fn paralyzed(battle: anytype) bool {
|
|
2655
|
+
if (showdown) return battle.rng.chance(u8, 63, 256);
|
|
2656
|
+
return battle.rng.next() < Gen12.percent(25);
|
|
2657
|
+
}
|
|
2581
2658
|
|
|
2582
|
-
fn
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
}
|
|
2659
|
+
inline fn confusionChance(battle: anytype) bool {
|
|
2660
|
+
if (showdown) return battle.rng.chance(u8, 25, 256);
|
|
2661
|
+
return battle.rng.next() < Gen12.percent(10);
|
|
2662
|
+
}
|
|
2587
2663
|
|
|
2588
|
-
fn
|
|
2589
|
-
|
|
2590
|
-
if (
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2664
|
+
inline fn secondaryChance(battle: anytype, low: bool) bool {
|
|
2665
|
+
if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 26 else 77), 256);
|
|
2666
|
+
return battle.rng.next() < 1 + (if (low) Gen12.percent(10) else Gen12.percent(30));
|
|
2667
|
+
}
|
|
2668
|
+
|
|
2669
|
+
inline fn poisonChance(battle: anytype, low: bool) bool {
|
|
2670
|
+
if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 52 else 103), 256);
|
|
2671
|
+
return battle.rng.next() < 1 + (if (low) Gen12.percent(20) else Gen12.percent(40));
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
inline fn sleepDuration(battle: anytype) u3 {
|
|
2675
|
+
if (showdown) return @intCast(u3, battle.rng.range(u8, 1, 8));
|
|
2676
|
+
while (true) {
|
|
2677
|
+
const r = battle.rng.next() & 7;
|
|
2678
|
+
if (r != 0) return @intCast(u3, r);
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
|
|
2682
|
+
inline fn bideThrashDuration(battle: anytype) u3 {
|
|
2683
|
+
if (showdown) return @intCast(u3, battle.rng.range(u4, 2, 4));
|
|
2684
|
+
return @intCast(u3, (battle.rng.next() & 1) + 2);
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
fn moveSlot(battle: anytype, moves: []MoveSlot, check_pp: u4) u4 {
|
|
2688
|
+
if (showdown) {
|
|
2689
|
+
if (check_pp == 0) {
|
|
2690
|
+
var i: usize = moves.len;
|
|
2691
|
+
while (i > 0) {
|
|
2692
|
+
i -= 1;
|
|
2693
|
+
if (moves[i].id != .None) {
|
|
2694
|
+
return battle.rng.range(u4, 0, @intCast(u4, i + 1)) + 1;
|
|
2695
|
+
}
|
|
2603
2696
|
}
|
|
2697
|
+
} else {
|
|
2698
|
+
var r = battle.rng.range(u4, 0, @intCast(u4, check_pp)) + 1;
|
|
2699
|
+
var i: usize = 0;
|
|
2700
|
+
while (i < moves.len and r > 0) : (i += 1) {
|
|
2701
|
+
if (moves[i].pp > 0) {
|
|
2702
|
+
r -= 1;
|
|
2703
|
+
if (r == 0) break;
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
return @intCast(u4, i + 1);
|
|
2604
2707
|
}
|
|
2605
|
-
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2710
|
+
while (true) {
|
|
2711
|
+
const r = @intCast(u4, battle.rng.next() & 3);
|
|
2712
|
+
if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
|
|
2606
2713
|
}
|
|
2607
2714
|
}
|
|
2608
2715
|
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2716
|
+
inline fn disableDuration(battle: anytype) u4 {
|
|
2717
|
+
if (showdown) return @intCast(u4, battle.rng.range(u8, 1, 9));
|
|
2718
|
+
return @intCast(u4, (battle.rng.next() & 7) + 1);
|
|
2612
2719
|
}
|
|
2613
|
-
|
|
2720
|
+
|
|
2721
|
+
inline fn metronome(battle: anytype) Move {
|
|
2722
|
+
if (showdown) {
|
|
2723
|
+
const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
|
|
2724
|
+
const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
|
|
2725
|
+
return @intToEnum(Move, r + mod);
|
|
2726
|
+
}
|
|
2727
|
+
while (true) {
|
|
2728
|
+
const r = battle.rng.next();
|
|
2729
|
+
if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
|
|
2730
|
+
if (r >= @enumToInt(Move.Struggle)) continue;
|
|
2731
|
+
return @intToEnum(Move, r);
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
|
|
2735
|
+
inline fn psywave(battle: anytype, max: u8) u8 {
|
|
2736
|
+
if (showdown) return battle.rng.range(u8, 0, max);
|
|
2737
|
+
while (true) {
|
|
2738
|
+
const r = battle.rng.next();
|
|
2739
|
+
if (r < max) return r;
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
|
|
2743
|
+
inline fn distribution(battle: anytype) u3 {
|
|
2744
|
+
if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
|
|
2745
|
+
const r = (battle.rng.next() & 3);
|
|
2746
|
+
return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
|
|
2747
|
+
}
|
|
2748
|
+
|
|
2749
|
+
inline fn unboost(battle: anytype) bool {
|
|
2750
|
+
if (showdown) return battle.rng.chance(u8, 85, 256);
|
|
2751
|
+
return battle.rng.next() < Gen12.percent(33) + 1;
|
|
2752
|
+
}
|
|
2753
|
+
};
|
|
2614
2754
|
|
|
2615
2755
|
test "RNG agreement" {
|
|
2616
2756
|
if (!showdown) return;
|
|
@@ -2710,9 +2850,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
2710
2850
|
while (slot <= 4) : (slot += 1) {
|
|
2711
2851
|
const m = active.moves[slot - 1];
|
|
2712
2852
|
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
|
|
2853
|
+
if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
|
|
2854
|
+
// Pokémon Showdown displays Struggle if limited to Bide but unable to pick
|
|
2855
|
+
const struggle =
|
|
2856
|
+
m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);
|
|
2857
|
+
const s = if (struggle) 0 else slot;
|
|
2716
2858
|
out[n] = .{ .type = .Move, .data = s };
|
|
2717
2859
|
n += 1;
|
|
2718
2860
|
return n;
|