@pkmn/engine 0.1.0-dev.207fa86a → 0.1.0-dev.24867658
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 +43 -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 +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 +24 -34
- package/package.json +43 -31
- 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 +65 -0
- package/src/lib/common/options.zig +3 -3
- package/src/lib/common/protocol.zig +38 -38
- package/src/lib/gen1/README.md +117 -83
- package/src/lib/gen1/data.zig +20 -18
- package/src/lib/gen1/helpers.zig +3 -18
- package/src/lib/gen1/mechanics.zig +495 -339
- 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 {
|
|
@@ -1278,7 +1351,9 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
|
|
|
1278
1351
|
if (move.effect == .Swift) return true;
|
|
1279
1352
|
if (foe.active.volatiles.Invulnerable) break :miss true;
|
|
1280
1353
|
}
|
|
1281
|
-
if (move.effect == .DreamEater and !Status.is(foe.stored().status, .SLP)
|
|
1354
|
+
if (move.effect == .DreamEater and (!Status.is(foe.stored().status, .SLP) or
|
|
1355
|
+
(showdown and foe.active.volatiles.Substitute)))
|
|
1356
|
+
{
|
|
1282
1357
|
immune.* = true;
|
|
1283
1358
|
if (showdown) return false;
|
|
1284
1359
|
break :miss true;
|
|
@@ -1298,23 +1373,24 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
|
|
|
1298
1373
|
}
|
|
1299
1374
|
|
|
1300
1375
|
// GLITCH: Thrash / Petal Dance / Rage get their accuracy overwritten on subsequent hits
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
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));
|
|
1305
1382
|
var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
|
|
1306
1383
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1307
1384
|
boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
|
|
1308
1385
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1309
1386
|
accuracy = @min(255, @max(1, accuracy));
|
|
1310
1387
|
|
|
1311
|
-
|
|
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;
|
|
1312
1391
|
|
|
1313
1392
|
// 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;
|
|
1393
|
+
break :miss !Rolls.hit(battle, accuracy);
|
|
1318
1394
|
};
|
|
1319
1395
|
|
|
1320
1396
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1429,6 +1505,15 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
|
|
|
1429
1505
|
}
|
|
1430
1506
|
|
|
1431
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
|
+
|
|
1432
1517
|
var damage = @max(stored.stats.hp / 16, 1);
|
|
1433
1518
|
|
|
1434
1519
|
// GLITCH: Leech Seed + Toxic glitch
|
|
@@ -1440,10 +1525,6 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
|
|
|
1440
1525
|
const amount = @min(damage, stored.hp);
|
|
1441
1526
|
stored.hp -= amount;
|
|
1442
1527
|
|
|
1443
|
-
var foe = battle.foe(player);
|
|
1444
|
-
var foe_stored = foe.stored();
|
|
1445
|
-
const foe_ident = battle.active(player.foe());
|
|
1446
|
-
|
|
1447
1528
|
// As above, Pokémon Showdown uses damageOf but its not relevant
|
|
1448
1529
|
if (amount > 0) try log.damage(ident, stored, .LeechSeed);
|
|
1449
1530
|
|
|
@@ -1459,13 +1540,13 @@ fn endTurn(battle: anytype, log: anytype) @TypeOf(log).Error!Result {
|
|
|
1459
1540
|
assert(!battle.side(.P1).active.volatiles.MultiHit);
|
|
1460
1541
|
assert(!battle.side(.P2).active.volatiles.MultiHit);
|
|
1461
1542
|
|
|
1543
|
+
battle.turn += 1;
|
|
1544
|
+
|
|
1462
1545
|
if (showdown and options.ebc and checkEBC(battle)) {
|
|
1463
1546
|
try log.tie();
|
|
1464
1547
|
return Result.Tie;
|
|
1465
1548
|
}
|
|
1466
1549
|
|
|
1467
|
-
battle.turn += 1;
|
|
1468
|
-
|
|
1469
1550
|
if (showdown and battle.turn >= 1000) {
|
|
1470
1551
|
try log.tie();
|
|
1471
1552
|
return Result.Tie;
|
|
@@ -1480,16 +1561,20 @@ fn endTurn(battle: anytype, log: anytype) @TypeOf(log).Error!Result {
|
|
|
1480
1561
|
|
|
1481
1562
|
fn checkEBC(battle: anytype) bool {
|
|
1482
1563
|
for (battle.sides, 0..) |side, i| {
|
|
1564
|
+
const foe = battle.sides[~@intCast(u1, i)];
|
|
1565
|
+
|
|
1483
1566
|
var foe_all_ghosts = true;
|
|
1484
1567
|
var foe_all_transform = true;
|
|
1568
|
+
for (foe.order, 0..) |id, j| {
|
|
1569
|
+
if (id == 0) break;
|
|
1570
|
+
const active = j == 0;
|
|
1571
|
+
const pokemon = foe.pokemon[id - 1];
|
|
1485
1572
|
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
const ghost = pokemon.hp == 0 or pokemon.types.includes(.Ghost);
|
|
1573
|
+
const ghost = pokemon.hp == 0 or
|
|
1574
|
+
(if (active) foe.active.types else pokemon.types).includes(.Ghost);
|
|
1490
1575
|
foe_all_ghosts = foe_all_ghosts and ghost;
|
|
1491
1576
|
foe_all_transform = foe_all_transform and pokemon.hp == 0 or transform: {
|
|
1492
|
-
for (pokemon.moves) |m| {
|
|
1577
|
+
for (if (active) foe.active.moves else pokemon.moves) |m| {
|
|
1493
1578
|
if (m.id == .None) break :transform true;
|
|
1494
1579
|
if (m.id != .Transform) break :transform false;
|
|
1495
1580
|
}
|
|
@@ -1497,10 +1582,14 @@ fn checkEBC(battle: anytype) bool {
|
|
|
1497
1582
|
};
|
|
1498
1583
|
}
|
|
1499
1584
|
|
|
1500
|
-
for (side.
|
|
1585
|
+
for (side.order, 0..) |id, j| {
|
|
1586
|
+
if (id == 0) break;
|
|
1587
|
+
const active = j == 0;
|
|
1588
|
+
const pokemon = side.pokemon[id - 1];
|
|
1589
|
+
|
|
1501
1590
|
if (pokemon.hp == 0 or Status.is(pokemon.status, .FRZ)) continue;
|
|
1502
1591
|
const transform = foe_all_transform and transform: {
|
|
1503
|
-
for (pokemon.moves) |m| {
|
|
1592
|
+
for (if (active) side.active.moves else pokemon.moves) |m| {
|
|
1504
1593
|
if (m.id == .None) break :transform true;
|
|
1505
1594
|
if (m.id != .Transform) break :transform false;
|
|
1506
1595
|
}
|
|
@@ -1508,7 +1597,7 @@ fn checkEBC(battle: anytype) bool {
|
|
|
1508
1597
|
};
|
|
1509
1598
|
if (transform) continue;
|
|
1510
1599
|
const no_pp = foe_all_ghosts and no_pp: {
|
|
1511
|
-
for (pokemon.moves) |m| {
|
|
1600
|
+
for (if (active) side.active.moves else pokemon.moves) |m| {
|
|
1512
1601
|
if (m.pp != 0) break :no_pp false;
|
|
1513
1602
|
}
|
|
1514
1603
|
break :no_pp true;
|
|
@@ -1524,7 +1613,50 @@ fn checkEBC(battle: anytype) bool {
|
|
|
1524
1613
|
return false;
|
|
1525
1614
|
}
|
|
1526
1615
|
|
|
1527
|
-
|
|
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 {
|
|
1528
1660
|
assert(move.effect.onBegin());
|
|
1529
1661
|
return switch (move.effect) {
|
|
1530
1662
|
.Confusion => Effects.confusion(battle, player, move, log),
|
|
@@ -1540,7 +1672,7 @@ inline fn onBegin(battle: anytype, player: Player, move: Move.Data, mslot: u8, l
|
|
|
1540
1672
|
.Poison => Effects.poison(battle, player, move, log),
|
|
1541
1673
|
.Reflect => Effects.reflect(battle, player, log),
|
|
1542
1674
|
.Splash => Effects.splash(battle, player, log),
|
|
1543
|
-
.Substitute => Effects.substitute(battle, player, log),
|
|
1675
|
+
.Substitute => Effects.substitute(battle, player, residual, log),
|
|
1544
1676
|
.SwitchAndTeleport => Effects.switchAndTeleport(battle, player, move, log),
|
|
1545
1677
|
.Transform => Effects.transform(battle, player, log),
|
|
1546
1678
|
else => unreachable,
|
|
@@ -1607,10 +1739,7 @@ pub const Effects = struct {
|
|
|
1607
1739
|
side.active.volatiles.Bide = true;
|
|
1608
1740
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1609
1741
|
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);
|
|
1742
|
+
side.active.volatiles.attacks = Rolls.bideThrashDuration(battle);
|
|
1614
1743
|
|
|
1615
1744
|
try log.start(battle.active(player), .Bide);
|
|
1616
1745
|
}
|
|
@@ -1632,14 +1761,8 @@ pub const Effects = struct {
|
|
|
1632
1761
|
return;
|
|
1633
1762
|
}
|
|
1634
1763
|
|
|
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;
|
|
1764
|
+
if (foe.active.types.includes(move.type)) return;
|
|
1765
|
+
if (!Rolls.secondaryChance(battle, move.effect == .BurnChance1)) return;
|
|
1643
1766
|
|
|
1644
1767
|
foe_stored.status = Status.init(.BRN);
|
|
1645
1768
|
foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
|
|
@@ -1663,11 +1786,7 @@ pub const Effects = struct {
|
|
|
1663
1786
|
const sub = foe.active.volatiles.Substitute;
|
|
1664
1787
|
|
|
1665
1788
|
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;
|
|
1789
|
+
if (!Rolls.confusionChance(battle)) return;
|
|
1671
1790
|
} else {
|
|
1672
1791
|
if (showdown) {
|
|
1673
1792
|
if (!try checkHit(battle, player, move, log)) {
|
|
@@ -1686,10 +1805,7 @@ pub const Effects = struct {
|
|
|
1686
1805
|
|
|
1687
1806
|
if (foe.active.volatiles.Confusion) return;
|
|
1688
1807
|
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);
|
|
1808
|
+
foe.active.volatiles.confusion = Rolls.confusionDuration(battle);
|
|
1693
1809
|
|
|
1694
1810
|
try log.start(battle.active(player.foe()), .Confusion);
|
|
1695
1811
|
}
|
|
@@ -1739,11 +1855,8 @@ pub const Effects = struct {
|
|
|
1739
1855
|
return Result.Error;
|
|
1740
1856
|
}
|
|
1741
1857
|
|
|
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);
|
|
1858
|
+
volatiles.disabled_move = @intCast(u3, Rolls.moveSlot(battle, &foe.active.moves, n));
|
|
1859
|
+
volatiles.disabled_duration = Rolls.disableDuration(battle);
|
|
1747
1860
|
|
|
1748
1861
|
try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
|
|
1749
1862
|
return null;
|
|
@@ -1753,6 +1866,11 @@ pub const Effects = struct {
|
|
|
1753
1866
|
var side = battle.side(player);
|
|
1754
1867
|
var stored = side.stored();
|
|
1755
1868
|
|
|
1869
|
+
if (battle.last_damage == 0) {
|
|
1870
|
+
assert(showdown);
|
|
1871
|
+
if (!battle.foe(player).active.volatiles.Substitute) return;
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1756
1874
|
const drain = @max(battle.last_damage / 2, 1);
|
|
1757
1875
|
battle.last_damage = drain;
|
|
1758
1876
|
|
|
@@ -1777,15 +1895,7 @@ pub const Effects = struct {
|
|
|
1777
1895
|
var volatiles = &battle.foe(player).active.volatiles;
|
|
1778
1896
|
|
|
1779
1897
|
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;
|
|
1898
|
+
if (!Rolls.secondaryChance(battle, move.effect == .FlinchChance1)) return;
|
|
1789
1899
|
|
|
1790
1900
|
volatiles.Flinch = true;
|
|
1791
1901
|
volatiles.Recharging = false;
|
|
@@ -1811,12 +1921,8 @@ pub const Effects = struct {
|
|
|
1811
1921
|
return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
|
|
1812
1922
|
}
|
|
1813
1923
|
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
else
|
|
1817
|
-
battle.rng.next() < 1 + Gen12.percent(10);
|
|
1818
|
-
if (!chance) return;
|
|
1819
|
-
|
|
1924
|
+
if (foe.active.types.includes(move.type)) return;
|
|
1925
|
+
if (!Rolls.secondaryChance(battle, true)) return;
|
|
1820
1926
|
// Freeze Clause Mod
|
|
1821
1927
|
if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
1822
1928
|
|
|
@@ -1953,15 +2059,15 @@ pub const Effects = struct {
|
|
|
1953
2059
|
}
|
|
1954
2060
|
break :has_mimic false;
|
|
1955
2061
|
};
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
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;
|
|
1959
2065
|
return try log.fail(battle.active(player.foe()), .None);
|
|
1960
2066
|
}
|
|
1961
2067
|
}
|
|
1962
2068
|
if (!try checkHit(battle, player, move, log)) return;
|
|
1963
2069
|
|
|
1964
|
-
const rslot =
|
|
2070
|
+
const rslot = Rolls.moveSlot(battle, &foe.active.moves, 0);
|
|
1965
2071
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
1966
2072
|
|
|
1967
2073
|
try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
|
|
@@ -1982,7 +2088,8 @@ pub const Effects = struct {
|
|
|
1982
2088
|
assert(!side.active.volatiles.MultiHit);
|
|
1983
2089
|
side.active.volatiles.MultiHit = true;
|
|
1984
2090
|
|
|
1985
|
-
side.active.volatiles.attacks =
|
|
2091
|
+
side.active.volatiles.attacks =
|
|
2092
|
+
if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
|
|
1986
2093
|
}
|
|
1987
2094
|
|
|
1988
2095
|
fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2026,14 +2133,8 @@ pub const Effects = struct {
|
|
|
2026
2133
|
}
|
|
2027
2134
|
|
|
2028
2135
|
// 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;
|
|
2136
|
+
if (foe.active.types.includes(move.type)) return;
|
|
2137
|
+
if (!Rolls.secondaryChance(battle, move.effect == .ParalyzeChance1)) return;
|
|
2037
2138
|
|
|
2038
2139
|
foe_stored.status = Status.init(.PAR);
|
|
2039
2140
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
@@ -2063,7 +2164,7 @@ pub const Effects = struct {
|
|
|
2063
2164
|
return log.fail(foe_ident, if (Status.is(foe_stored.status, .PSN))
|
|
2064
2165
|
if (!showdown)
|
|
2065
2166
|
.Poison
|
|
2066
|
-
else if (toxic ==
|
|
2167
|
+
else if (toxic == (foe_stored.status == Status.TOX))
|
|
2067
2168
|
if (toxic) .Toxic else .Poison
|
|
2068
2169
|
else
|
|
2069
2170
|
.None
|
|
@@ -2076,16 +2177,7 @@ pub const Effects = struct {
|
|
|
2076
2177
|
|
|
2077
2178
|
if (move.effect == .Poison) {
|
|
2078
2179
|
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
|
-
}
|
|
2180
|
+
} else if (!Rolls.poisonChance(battle, move.effect == .PoisonChance1)) return;
|
|
2089
2181
|
|
|
2090
2182
|
foe_stored.status = Status.init(.PSN);
|
|
2091
2183
|
if (toxic) {
|
|
@@ -2099,10 +2191,8 @@ pub const Effects = struct {
|
|
|
2099
2191
|
|
|
2100
2192
|
fn rage(battle: anytype, player: Player) !void {
|
|
2101
2193
|
var volatiles = &battle.side(player).active.volatiles;
|
|
2102
|
-
if (volatiles.Rage) return;
|
|
2103
|
-
volatiles.Rage = true;
|
|
2104
2194
|
assert(!volatiles.Bide);
|
|
2105
|
-
volatiles.
|
|
2195
|
+
volatiles.Rage = true;
|
|
2106
2196
|
}
|
|
2107
2197
|
|
|
2108
2198
|
fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
|
|
@@ -2154,16 +2244,7 @@ pub const Effects = struct {
|
|
|
2154
2244
|
}
|
|
2155
2245
|
foe.active.volatiles.Recharging = false;
|
|
2156
2246
|
|
|
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);
|
|
2247
|
+
foe_stored.status = Status.slp(Rolls.sleepDuration(battle));
|
|
2167
2248
|
try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
|
|
2168
2249
|
}
|
|
2169
2250
|
|
|
@@ -2171,7 +2252,7 @@ pub const Effects = struct {
|
|
|
2171
2252
|
try log.activate(battle.active(player), .Splash);
|
|
2172
2253
|
}
|
|
2173
2254
|
|
|
2174
|
-
fn substitute(battle: anytype, player: Player, log: anytype) !void {
|
|
2255
|
+
fn substitute(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
|
|
2175
2256
|
var side = battle.side(player);
|
|
2176
2257
|
if (side.active.volatiles.Substitute) {
|
|
2177
2258
|
try log.fail(battle.active(player), .Substitute);
|
|
@@ -2196,14 +2277,18 @@ pub const Effects = struct {
|
|
|
2196
2277
|
side.active.volatiles.substitute = hp + 1;
|
|
2197
2278
|
side.active.volatiles.Substitute = true;
|
|
2198
2279
|
try log.start(battle.active(player), .Substitute);
|
|
2199
|
-
if (hp > 0)
|
|
2280
|
+
if (hp > 0) {
|
|
2281
|
+
try log.damage(battle.active(player), side.stored(), .None);
|
|
2282
|
+
if (showdown and side.stored().hp == 0) residual.* = false;
|
|
2283
|
+
}
|
|
2200
2284
|
}
|
|
2201
2285
|
|
|
2202
2286
|
fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
2203
2287
|
if (!showdown or battle.side(player).last_selected_move == .Teleport) return;
|
|
2204
2288
|
|
|
2205
|
-
// Whirlwind/Roar should not roll to hit but Pokémon Showdown does anyway
|
|
2289
|
+
// Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
|
|
2206
2290
|
_ = try checkHit(battle, player, move, log);
|
|
2291
|
+
battle.last_damage = 0;
|
|
2207
2292
|
}
|
|
2208
2293
|
|
|
2209
2294
|
fn thrashing(battle: anytype, player: Player) void {
|
|
@@ -2212,11 +2297,7 @@ pub const Effects = struct {
|
|
|
2212
2297
|
assert(!volatiles.Bide);
|
|
2213
2298
|
|
|
2214
2299
|
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);
|
|
2300
|
+
volatiles.attacks = Rolls.bideThrashDuration(battle);
|
|
2220
2301
|
}
|
|
2221
2302
|
|
|
2222
2303
|
fn transform(battle: anytype, player: Player, log: anytype) !void {
|
|
@@ -2248,7 +2329,7 @@ pub const Effects = struct {
|
|
|
2248
2329
|
try log.transform(battle.active(player), foe_ident);
|
|
2249
2330
|
}
|
|
2250
2331
|
|
|
2251
|
-
fn binding(battle: anytype, player: Player) void {
|
|
2332
|
+
fn binding(battle: anytype, player: Player, rewrap: bool) void {
|
|
2252
2333
|
var side = battle.side(player);
|
|
2253
2334
|
var foe = battle.foe(player);
|
|
2254
2335
|
|
|
@@ -2256,9 +2337,11 @@ pub const Effects = struct {
|
|
|
2256
2337
|
side.active.volatiles.Binding = true;
|
|
2257
2338
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2258
2339
|
// (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
|
|
2259
|
-
if (!showdown)
|
|
2340
|
+
if (!showdown) {
|
|
2341
|
+
foe.active.volatiles.Recharging = false;
|
|
2342
|
+
} else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
|
|
2260
2343
|
|
|
2261
|
-
side.active.volatiles.attacks = distribution(battle) - 1;
|
|
2344
|
+
side.active.volatiles.attacks = Rolls.distribution(battle) - 1;
|
|
2262
2345
|
}
|
|
2263
2346
|
|
|
2264
2347
|
fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2285,9 +2368,11 @@ pub const Effects = struct {
|
|
|
2285
2368
|
return try log.fail(ident, .None);
|
|
2286
2369
|
}
|
|
2287
2370
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2288
|
-
const stat = unmodifiedStats(battle,
|
|
2371
|
+
const stat = unmodifiedStats(battle, side).atk;
|
|
2289
2372
|
stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2290
2373
|
try log.boost(ident, reason, n);
|
|
2374
|
+
// Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
|
|
2375
|
+
if (showdown and move.effect == .Rage) return;
|
|
2291
2376
|
},
|
|
2292
2377
|
.DefenseUp1, .DefenseUp2 => {
|
|
2293
2378
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
@@ -2303,7 +2388,7 @@ pub const Effects = struct {
|
|
|
2303
2388
|
return try log.fail(ident, .None);
|
|
2304
2389
|
}
|
|
2305
2390
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2306
|
-
const stat = unmodifiedStats(battle,
|
|
2391
|
+
const stat = unmodifiedStats(battle, side).def;
|
|
2307
2392
|
stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2308
2393
|
try log.boost(ident, .Defense, n);
|
|
2309
2394
|
},
|
|
@@ -2320,7 +2405,7 @@ pub const Effects = struct {
|
|
|
2320
2405
|
return try log.fail(ident, .None);
|
|
2321
2406
|
}
|
|
2322
2407
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2323
|
-
const stat = unmodifiedStats(battle,
|
|
2408
|
+
const stat = unmodifiedStats(battle, side).spe;
|
|
2324
2409
|
stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2325
2410
|
try log.boost(ident, .Speed, 2);
|
|
2326
2411
|
},
|
|
@@ -2340,7 +2425,7 @@ pub const Effects = struct {
|
|
|
2340
2425
|
return try log.fail(ident, .None);
|
|
2341
2426
|
}
|
|
2342
2427
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2343
|
-
const stat = unmodifiedStats(battle,
|
|
2428
|
+
const stat = unmodifiedStats(battle, side).spc;
|
|
2344
2429
|
stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2345
2430
|
try log.boost(ident, .SpecialAttack, n);
|
|
2346
2431
|
try log.boost(ident, .SpecialDefense, n);
|
|
@@ -2367,11 +2452,7 @@ pub const Effects = struct {
|
|
|
2367
2452
|
}
|
|
2368
2453
|
|
|
2369
2454
|
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;
|
|
2455
|
+
if (!Rolls.unboost(battle) or foe.active.volatiles.Invulnerable) return;
|
|
2375
2456
|
} else if (!showdown and !try checkHit(battle, player, move, log)) {
|
|
2376
2457
|
return; // checkHit already checks for Invulnerable
|
|
2377
2458
|
}
|
|
@@ -2393,7 +2474,7 @@ pub const Effects = struct {
|
|
|
2393
2474
|
return try log.fail(foe_ident, .None);
|
|
2394
2475
|
}
|
|
2395
2476
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2396
|
-
const stat = unmodifiedStats(battle,
|
|
2477
|
+
const stat = unmodifiedStats(battle, foe).atk;
|
|
2397
2478
|
stats.atk = @max(1, stat * mod[0] / mod[1]);
|
|
2398
2479
|
try log.boost(foe_ident, .Attack, -1);
|
|
2399
2480
|
},
|
|
@@ -2411,7 +2492,7 @@ pub const Effects = struct {
|
|
|
2411
2492
|
return try log.fail(foe_ident, .None);
|
|
2412
2493
|
}
|
|
2413
2494
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2414
|
-
const stat = unmodifiedStats(battle,
|
|
2495
|
+
const stat = unmodifiedStats(battle, foe).def;
|
|
2415
2496
|
stats.def = @max(1, stat * mod[0] / mod[1]);
|
|
2416
2497
|
try log.boost(foe_ident, .Defense, -@as(i8, n));
|
|
2417
2498
|
},
|
|
@@ -2428,7 +2509,7 @@ pub const Effects = struct {
|
|
|
2428
2509
|
return try log.fail(foe_ident, .None);
|
|
2429
2510
|
}
|
|
2430
2511
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2431
|
-
const stat = unmodifiedStats(battle,
|
|
2512
|
+
const stat = unmodifiedStats(battle, foe).spe;
|
|
2432
2513
|
stats.spe = @max(1, stat * mod[0] / mod[1]);
|
|
2433
2514
|
try log.boost(foe_ident, .Speed, -1);
|
|
2434
2515
|
assert(boosts.spe >= -6);
|
|
@@ -2448,7 +2529,7 @@ pub const Effects = struct {
|
|
|
2448
2529
|
return try log.fail(foe_ident, .None);
|
|
2449
2530
|
}
|
|
2450
2531
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2451
|
-
const stat = unmodifiedStats(battle,
|
|
2532
|
+
const stat = unmodifiedStats(battle, foe).spc;
|
|
2452
2533
|
stats.spc = @max(1, stat * mod[0] / mod[1]);
|
|
2453
2534
|
try log.boost(foe_ident, .SpecialAttack, -1);
|
|
2454
2535
|
try log.boost(foe_ident, .SpecialDefense, -1);
|
|
@@ -2467,8 +2548,7 @@ pub const Effects = struct {
|
|
|
2467
2548
|
}
|
|
2468
2549
|
};
|
|
2469
2550
|
|
|
2470
|
-
fn unmodifiedStats(battle: anytype,
|
|
2471
|
-
const side = battle.side(who);
|
|
2551
|
+
fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
|
|
2472
2552
|
if (!side.active.volatiles.Transform) return &side.stored().stats;
|
|
2473
2553
|
const id = ID.from(side.active.volatiles.transform);
|
|
2474
2554
|
return &battle.side(id.player).pokemon[id.id - 1].stats;
|
|
@@ -2482,7 +2562,7 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
|
|
|
2482
2562
|
}
|
|
2483
2563
|
}
|
|
2484
2564
|
|
|
2485
|
-
fn isForced(active: ActivePokemon) bool {
|
|
2565
|
+
inline fn isForced(active: ActivePokemon) bool {
|
|
2486
2566
|
return active.volatiles.Recharging or active.volatiles.Rage or
|
|
2487
2567
|
active.volatiles.Thrashing or active.volatiles.Charging;
|
|
2488
2568
|
}
|
|
@@ -2514,13 +2594,9 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
|
|
|
2514
2594
|
volatiles.LeechSeed = false;
|
|
2515
2595
|
try log.end(ident, .LeechSeed);
|
|
2516
2596
|
}
|
|
2517
|
-
if (volatiles.Toxic) {
|
|
2597
|
+
if (!showdown and volatiles.Toxic) {
|
|
2518
2598
|
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
|
-
}
|
|
2599
|
+
// volatiles.toxic is left unchanged, except on Pokémon Showdown which clears it (below)
|
|
2524
2600
|
}
|
|
2525
2601
|
if (volatiles.LightScreen) {
|
|
2526
2602
|
volatiles.LightScreen = false;
|
|
@@ -2530,70 +2606,148 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
|
|
|
2530
2606
|
volatiles.Reflect = false;
|
|
2531
2607
|
try log.end(ident, .Reflect);
|
|
2532
2608
|
}
|
|
2609
|
+
if (showdown and volatiles.Toxic) {
|
|
2610
|
+
volatiles.Toxic = false;
|
|
2611
|
+
// Pokémon Showdown erroneously clears the toxic counter
|
|
2612
|
+
volatiles.toxic = 0;
|
|
2613
|
+
try log.end(ident, .Toxic);
|
|
2614
|
+
}
|
|
2533
2615
|
}
|
|
2534
2616
|
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2617
|
+
pub const Rolls = struct {
|
|
2618
|
+
inline fn speedTie(battle: anytype) bool {
|
|
2619
|
+
if (showdown) return battle.rng.range(u8, 0, 2) == 0;
|
|
2620
|
+
return battle.rng.next() < Gen12.percent(50) + 1;
|
|
2621
|
+
}
|
|
2622
|
+
|
|
2623
|
+
inline fn criticalHit(battle: anytype, chance: u16) bool {
|
|
2624
|
+
if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
|
|
2625
|
+
return std.math.rotl(u8, battle.rng.next(), 3) < chance;
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2628
|
+
inline fn damage(battle: anytype) u8 {
|
|
2629
|
+
if (showdown) return battle.rng.range(u8, 217, 256);
|
|
2630
|
+
while (true) {
|
|
2631
|
+
const r = std.math.rotr(u8, battle.rng.next(), 1);
|
|
2632
|
+
if (r >= 217) return r;
|
|
2543
2633
|
}
|
|
2544
2634
|
}
|
|
2545
|
-
return false;
|
|
2546
|
-
}
|
|
2547
2635
|
|
|
2548
|
-
fn
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2636
|
+
inline fn hit(battle: anytype, accuracy: u16) bool {
|
|
2637
|
+
if (showdown) return battle.rng.chance(u8, @intCast(u8, accuracy), 256);
|
|
2638
|
+
return battle.rng.next() < accuracy;
|
|
2639
|
+
}
|
|
2552
2640
|
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
else
|
|
2558
|
-
(battle.rng.next() & 3) + 2);
|
|
2641
|
+
inline fn confusionDuration(battle: anytype) u3 {
|
|
2642
|
+
if (showdown) return @intCast(u3, battle.rng.range(u8, 2, 6));
|
|
2643
|
+
return @intCast(u3, (battle.rng.next() & 3) + 2);
|
|
2644
|
+
}
|
|
2559
2645
|
|
|
2560
|
-
|
|
2561
|
-
|
|
2646
|
+
inline fn confused(battle: anytype) bool {
|
|
2647
|
+
if (showdown) return !battle.rng.chance(u8, 128, 256);
|
|
2648
|
+
return battle.rng.next() >= Gen12.percent(50) + 1;
|
|
2649
|
+
}
|
|
2562
2650
|
|
|
2563
|
-
|
|
2651
|
+
inline fn paralyzed(battle: anytype) bool {
|
|
2652
|
+
if (showdown) return battle.rng.chance(u8, 63, 256);
|
|
2653
|
+
return battle.rng.next() < Gen12.percent(25);
|
|
2654
|
+
}
|
|
2564
2655
|
|
|
2565
|
-
fn
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
}
|
|
2656
|
+
inline fn confusionChance(battle: anytype) bool {
|
|
2657
|
+
if (showdown) return battle.rng.chance(u8, 25, 256);
|
|
2658
|
+
return battle.rng.next() < Gen12.percent(10);
|
|
2659
|
+
}
|
|
2570
2660
|
|
|
2571
|
-
fn
|
|
2572
|
-
|
|
2573
|
-
if (
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2661
|
+
inline fn secondaryChance(battle: anytype, low: bool) bool {
|
|
2662
|
+
if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 26 else 77), 256);
|
|
2663
|
+
return battle.rng.next() < 1 + (if (low) Gen12.percent(10) else Gen12.percent(30));
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2666
|
+
inline fn poisonChance(battle: anytype, low: bool) bool {
|
|
2667
|
+
if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 52 else 103), 256);
|
|
2668
|
+
return battle.rng.next() < 1 + (if (low) Gen12.percent(20) else Gen12.percent(40));
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2671
|
+
inline fn sleepDuration(battle: anytype) u3 {
|
|
2672
|
+
if (showdown) return @intCast(u3, battle.rng.range(u8, 1, 8));
|
|
2673
|
+
while (true) {
|
|
2674
|
+
const r = battle.rng.next() & 7;
|
|
2675
|
+
if (r != 0) return @intCast(u3, r);
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
inline fn bideThrashDuration(battle: anytype) u3 {
|
|
2680
|
+
if (showdown) return @intCast(u3, battle.rng.range(u4, 2, 4));
|
|
2681
|
+
return @intCast(u3, (battle.rng.next() & 1) + 2);
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
fn moveSlot(battle: anytype, moves: []MoveSlot, check_pp: u4) u4 {
|
|
2685
|
+
if (showdown) {
|
|
2686
|
+
if (check_pp == 0) {
|
|
2687
|
+
var i: usize = moves.len;
|
|
2688
|
+
while (i > 0) {
|
|
2689
|
+
i -= 1;
|
|
2690
|
+
if (moves[i].id != .None) {
|
|
2691
|
+
return battle.rng.range(u4, 0, @intCast(u4, i + 1)) + 1;
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
} else {
|
|
2695
|
+
var r = battle.rng.range(u4, 0, @intCast(u4, check_pp)) + 1;
|
|
2696
|
+
var i: usize = 0;
|
|
2697
|
+
while (i < moves.len and r > 0) : (i += 1) {
|
|
2698
|
+
if (moves[i].pp > 0) {
|
|
2699
|
+
r -= 1;
|
|
2700
|
+
if (r == 0) break;
|
|
2701
|
+
}
|
|
2586
2702
|
}
|
|
2703
|
+
return @intCast(u4, i + 1);
|
|
2587
2704
|
}
|
|
2588
|
-
|
|
2705
|
+
}
|
|
2706
|
+
|
|
2707
|
+
while (true) {
|
|
2708
|
+
const r = @intCast(u4, battle.rng.next() & 3);
|
|
2709
|
+
if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
|
|
2589
2710
|
}
|
|
2590
2711
|
}
|
|
2591
2712
|
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2713
|
+
inline fn disableDuration(battle: anytype) u4 {
|
|
2714
|
+
if (showdown) return @intCast(u4, battle.rng.range(u8, 1, 9));
|
|
2715
|
+
return @intCast(u4, (battle.rng.next() & 7) + 1);
|
|
2595
2716
|
}
|
|
2596
|
-
|
|
2717
|
+
|
|
2718
|
+
inline fn metronome(battle: anytype) Move {
|
|
2719
|
+
if (showdown) {
|
|
2720
|
+
const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
|
|
2721
|
+
const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
|
|
2722
|
+
return @intToEnum(Move, r + mod);
|
|
2723
|
+
}
|
|
2724
|
+
while (true) {
|
|
2725
|
+
const r = battle.rng.next();
|
|
2726
|
+
if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
|
|
2727
|
+
if (r >= @enumToInt(Move.Struggle)) continue;
|
|
2728
|
+
return @intToEnum(Move, r);
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2732
|
+
inline fn psywave(battle: anytype, max: u8) u8 {
|
|
2733
|
+
if (showdown) return battle.rng.range(u8, 0, max);
|
|
2734
|
+
while (true) {
|
|
2735
|
+
const r = battle.rng.next();
|
|
2736
|
+
if (r < max) return r;
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
|
|
2740
|
+
inline fn distribution(battle: anytype) u3 {
|
|
2741
|
+
if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
|
|
2742
|
+
const r = (battle.rng.next() & 3);
|
|
2743
|
+
return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
|
|
2744
|
+
}
|
|
2745
|
+
|
|
2746
|
+
inline fn unboost(battle: anytype) bool {
|
|
2747
|
+
if (showdown) return battle.rng.chance(u8, 85, 256);
|
|
2748
|
+
return battle.rng.next() < Gen12.percent(33) + 1;
|
|
2749
|
+
}
|
|
2750
|
+
};
|
|
2597
2751
|
|
|
2598
2752
|
test "RNG agreement" {
|
|
2599
2753
|
if (!showdown) return;
|
|
@@ -2693,9 +2847,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
2693
2847
|
while (slot <= 4) : (slot += 1) {
|
|
2694
2848
|
const m = active.moves[slot - 1];
|
|
2695
2849
|
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
|
|
2850
|
+
if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
|
|
2851
|
+
// Pokémon Showdown displays Struggle if limited to Bide but unable to pick
|
|
2852
|
+
const struggle =
|
|
2853
|
+
m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);
|
|
2854
|
+
const s = if (struggle) 0 else slot;
|
|
2699
2855
|
out[n] = .{ .type = .Move, .data = s };
|
|
2700
2856
|
n += 1;
|
|
2701
2857
|
return n;
|