@pkmn/engine 0.1.0-dev.84cc1a02 → 0.1.0-dev.8b83af6b
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 +15 -12
- package/build/pkg/index.js.map +1 -1
- package/build/pkg/protocol.d.ts +4 -2
- package/build/pkg/protocol.js +4 -2
- 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 +30 -39
- package/package.json +45 -34
- package/src/bin/install-pkmn-engine +47 -28
- package/src/bin/pkmn-debug +62 -0
- package/src/lib/binding/node.zig +20 -23
- package/src/lib/binding/wasm.zig +4 -4
- package/src/lib/common/data.zig +11 -11
- package/src/lib/common/debug.zig +1 -0
- package/src/lib/common/options.zig +3 -3
- package/src/lib/common/protocol.zig +194 -178
- package/src/lib/common/rng.zig +5 -5
- package/src/lib/gen1/README.md +106 -70
- package/src/lib/gen1/data/moves.zig +10 -10
- package/src/lib/gen1/data/species.zig +2 -2
- package/src/lib/gen1/data/types.zig +3 -3
- package/src/lib/gen1/data.zig +36 -31
- package/src/lib/gen1/helpers.zig +11 -22
- package/src/lib/gen1/mechanics.zig +460 -365
- package/src/lib/gen2/data/items.zig +3 -3
- package/src/lib/gen2/data/moves.zig +1 -1
- package/src/lib/gen2/data/species.zig +1 -1
- package/src/lib/gen2/data/types.zig +3 -3
- package/src/lib/gen2/data.zig +16 -15
- package/src/lib/gen2/helpers.zig +6 -6
- 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;
|
|
@@ -121,20 +126,29 @@ fn selectMove(
|
|
|
121
126
|
const stored = side.stored();
|
|
122
127
|
|
|
123
128
|
assert(!isForced(side.active) or
|
|
124
|
-
(choice.type == .Move and choice.data == @
|
|
129
|
+
(choice.type == .Move and choice.data == @intFromBool(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,10 +180,10 @@ 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.
|
|
169
|
-
const last = side.active.move(
|
|
185
|
+
battle.last_moves.p2_index;
|
|
186
|
+
const last = side.active.move(slot);
|
|
170
187
|
if (last.id == .Metronome) side.last_selected_move = last.id;
|
|
171
188
|
if (last.id == .MirrorMove) return Result.Error;
|
|
172
189
|
}
|
|
@@ -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(c.data);
|
|
222
241
|
} else {
|
|
223
|
-
battle.
|
|
242
|
+
battle.last_moves.p2_index = @intCast(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;
|
|
@@ -349,7 +379,16 @@ fn doTurn(
|
|
|
349
379
|
|
|
350
380
|
residual = true;
|
|
351
381
|
replace = battle.side(foe_player).stored().hp == 0;
|
|
352
|
-
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;
|
|
353
392
|
if (!replace) {
|
|
354
393
|
if (foe_choice.type != .Switch) {
|
|
355
394
|
if (try checkFaint(battle, player, log)) |r| return r;
|
|
@@ -372,6 +411,8 @@ fn executeMove(
|
|
|
372
411
|
battle: anytype,
|
|
373
412
|
player: Player,
|
|
374
413
|
choice: Choice,
|
|
414
|
+
rewrap: bool,
|
|
415
|
+
skip: bool,
|
|
375
416
|
from: ?Move,
|
|
376
417
|
residual: *bool,
|
|
377
418
|
log: anytype,
|
|
@@ -379,13 +420,14 @@ fn executeMove(
|
|
|
379
420
|
var side = battle.side(player);
|
|
380
421
|
|
|
381
422
|
if (choice.type == .Switch) {
|
|
382
|
-
try switchIn(battle, player,
|
|
423
|
+
try switchIn(battle, player, choice.data, false, log);
|
|
383
424
|
return null;
|
|
384
425
|
}
|
|
385
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
|
|
386
430
|
if (side.last_selected_move == .SKIP_TURN) {
|
|
387
|
-
// Pokémon Showdown overwrites the SKIP_TURN sentinel with its botched move select,
|
|
388
|
-
// Binding instead gets handled in beforeMove after sleep and freeze
|
|
389
431
|
assert(!showdown);
|
|
390
432
|
if (battle.foe(player).active.volatiles.Binding) {
|
|
391
433
|
try log.cant(battle.active(player), .Bound);
|
|
@@ -394,7 +436,7 @@ fn executeMove(
|
|
|
394
436
|
}
|
|
395
437
|
|
|
396
438
|
assert(choice.type == .Move);
|
|
397
|
-
var mslot = @intCast(
|
|
439
|
+
var mslot: u4 = @intCast(choice.data);
|
|
398
440
|
// Sadly, we can't even check `Move.get(side.last_selected_move).effect == .Binding` here
|
|
399
441
|
// because Pokémon Showdown's Mirror Move implementation clobbers side.last_selected_move
|
|
400
442
|
var auto = showdown and side.last_selected_move != .None;
|
|
@@ -403,10 +445,10 @@ fn executeMove(
|
|
|
403
445
|
if (mslot == 0 and side.last_selected_move != .None and side.last_selected_move != .Struggle) {
|
|
404
446
|
// choice.data == 0 only happens with Struggle on Pokémon Showdown
|
|
405
447
|
assert(!showdown);
|
|
406
|
-
mslot = @intCast(
|
|
407
|
-
battle.
|
|
448
|
+
mslot = @intCast(if (player == .P1)
|
|
449
|
+
battle.last_moves.p1_index
|
|
408
450
|
else
|
|
409
|
-
battle.
|
|
451
|
+
battle.last_moves.p2_index);
|
|
410
452
|
const stored = side.stored();
|
|
411
453
|
// GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
|
|
412
454
|
auto = side.last_selected_move == .HyperBeam or side.active.volatiles.Bide or
|
|
@@ -427,15 +469,15 @@ fn executeMove(
|
|
|
427
469
|
} else if (showdown and side.active.volatiles.Charging) {
|
|
428
470
|
// Incorrect mslot with Pokémon Showdown choice semantics so we need to recover from index
|
|
429
471
|
assert(mslot == 1);
|
|
430
|
-
mslot = @intCast(
|
|
431
|
-
battle.
|
|
472
|
+
mslot = @intCast(if (player == .P1)
|
|
473
|
+
battle.last_moves.p1_index
|
|
432
474
|
else
|
|
433
|
-
battle.
|
|
475
|
+
battle.last_moves.p2_index);
|
|
434
476
|
}
|
|
435
477
|
|
|
436
478
|
var skip_can = false;
|
|
437
479
|
var skip_pp = false;
|
|
438
|
-
switch (try beforeMove(battle, player, from, residual, log)) {
|
|
480
|
+
switch (try beforeMove(battle, player, skip, from, residual, log)) {
|
|
439
481
|
.done => return null,
|
|
440
482
|
.skip_can => skip_can = true,
|
|
441
483
|
.skip_pp => skip_pp = true,
|
|
@@ -447,14 +489,15 @@ fn executeMove(
|
|
|
447
489
|
return null;
|
|
448
490
|
}
|
|
449
491
|
|
|
450
|
-
return doMove(battle, player, mslot, auto, residual, log);
|
|
492
|
+
return doMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
451
493
|
}
|
|
452
494
|
|
|
453
|
-
const BeforeMove =
|
|
495
|
+
const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
|
|
454
496
|
|
|
455
497
|
fn beforeMove(
|
|
456
498
|
battle: anytype,
|
|
457
499
|
player: Player,
|
|
500
|
+
skip: bool,
|
|
458
501
|
from: ?Move,
|
|
459
502
|
residual: *bool,
|
|
460
503
|
log: anytype,
|
|
@@ -486,13 +529,15 @@ fn beforeMove(
|
|
|
486
529
|
return .done;
|
|
487
530
|
}
|
|
488
531
|
|
|
489
|
-
if (foe.active.volatiles.Binding) {
|
|
532
|
+
if (skip or foe.active.volatiles.Binding) {
|
|
490
533
|
try log.cant(ident, .Bound);
|
|
491
534
|
return .done;
|
|
492
535
|
}
|
|
493
536
|
|
|
494
|
-
if (
|
|
495
|
-
|
|
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;
|
|
496
541
|
try log.cant(ident, .Flinch);
|
|
497
542
|
return .done;
|
|
498
543
|
}
|
|
@@ -513,13 +558,6 @@ fn beforeMove(
|
|
|
513
558
|
// Pokémon Showdown's disable condition has a single onBeforeMove handler
|
|
514
559
|
if (showdown and try disabled(side, ident, log)) return .done;
|
|
515
560
|
|
|
516
|
-
// Pokémon Showdown checks for Flinch *after* instead of before
|
|
517
|
-
if (showdown and volatiles.Flinch) {
|
|
518
|
-
volatiles.Flinch = false;
|
|
519
|
-
try log.cant(ident, .Flinch);
|
|
520
|
-
return .done;
|
|
521
|
-
}
|
|
522
|
-
|
|
523
561
|
// This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
|
|
524
562
|
// before the side had a selected a move - we simply need to assume this leads to a desync
|
|
525
563
|
if (side.last_selected_move == .None) {
|
|
@@ -537,12 +575,7 @@ fn beforeMove(
|
|
|
537
575
|
} else {
|
|
538
576
|
try log.activate(ident, .Confusion);
|
|
539
577
|
|
|
540
|
-
|
|
541
|
-
!battle.rng.chance(u8, 128, 256)
|
|
542
|
-
else
|
|
543
|
-
battle.rng.next() >= Gen12.percent(50) + 1;
|
|
544
|
-
|
|
545
|
-
if (confused) {
|
|
578
|
+
if (Rolls.confused(battle)) {
|
|
546
579
|
assert(!volatiles.MultiHit);
|
|
547
580
|
if (!volatiles.Rage) volatiles.state = 0;
|
|
548
581
|
volatiles.Bide = false;
|
|
@@ -568,8 +601,12 @@ fn beforeMove(
|
|
|
568
601
|
player
|
|
569
602
|
else
|
|
570
603
|
player.foe();
|
|
604
|
+
|
|
605
|
+
const uncapped = battle.last_damage;
|
|
571
606
|
// Skipping adjustDamage / randomizeDamage / checkHit
|
|
572
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;
|
|
573
610
|
|
|
574
611
|
return .done;
|
|
575
612
|
}
|
|
@@ -578,28 +615,28 @@ fn beforeMove(
|
|
|
578
615
|
|
|
579
616
|
if (!showdown and try disabled(side, ident, log)) return .done;
|
|
580
617
|
|
|
581
|
-
if (Status.is(stored.status, .PAR)) {
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
volatiles.Thrashing = false;
|
|
591
|
-
volatiles.Charging = false;
|
|
592
|
-
volatiles.Binding = false;
|
|
593
|
-
// GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
|
|
594
|
-
try log.cant(ident, .Paralysis);
|
|
595
|
-
return .done;
|
|
596
|
-
}
|
|
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;
|
|
597
627
|
}
|
|
598
628
|
|
|
599
629
|
if (volatiles.Bide) {
|
|
600
630
|
assert(!volatiles.Thrashing and !volatiles.Rage);
|
|
601
631
|
|
|
602
|
-
|
|
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
|
+
}
|
|
603
640
|
|
|
604
641
|
assert(volatiles.attacks > 0);
|
|
605
642
|
|
|
@@ -620,8 +657,9 @@ fn beforeMove(
|
|
|
620
657
|
return .done;
|
|
621
658
|
}
|
|
622
659
|
|
|
660
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
623
661
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
624
|
-
if (foe.stored().hp > 0) try buildRage(battle, player.foe(), log);
|
|
662
|
+
if (foe.stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
|
|
625
663
|
|
|
626
664
|
// For reasons passing understanding, Pokémon Showdown still inflicts residual damage to
|
|
627
665
|
// Bide's user even if the above damage has caused the foe to faint. It's simpler to always
|
|
@@ -642,8 +680,13 @@ fn beforeMove(
|
|
|
642
680
|
try log.start(battle.active(player), .ConfusionSilent);
|
|
643
681
|
}
|
|
644
682
|
try log.move(ident, side.last_selected_move, battle.active(player.foe()), from);
|
|
645
|
-
if (showdown
|
|
646
|
-
|
|
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
|
+
}
|
|
647
690
|
}
|
|
648
691
|
return .skip_can;
|
|
649
692
|
}
|
|
@@ -658,7 +701,11 @@ fn beforeMove(
|
|
|
658
701
|
side.last_selected_move,
|
|
659
702
|
);
|
|
660
703
|
if (showdown or battle.last_damage != 0) {
|
|
704
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
661
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
|
+
}
|
|
662
709
|
}
|
|
663
710
|
return .done;
|
|
664
711
|
}
|
|
@@ -690,6 +737,7 @@ fn canMove(
|
|
|
690
737
|
side.active.volatiles.Invulnerable = false;
|
|
691
738
|
} else if (move.effect == .Charge) {
|
|
692
739
|
try log.move(player_ident, side.last_selected_move, .{}, from);
|
|
740
|
+
setCounterable(battle, player, side, move);
|
|
693
741
|
try Effects.charge(battle, player, log);
|
|
694
742
|
return false;
|
|
695
743
|
}
|
|
@@ -706,6 +754,7 @@ fn canMove(
|
|
|
706
754
|
// regrettable, but this information is more "nice to have" than required
|
|
707
755
|
const f = if (from != null and from.? == .None) null else from;
|
|
708
756
|
try log.move(player_ident, side.last_selected_move, battle.active(target), f);
|
|
757
|
+
setCounterable(battle, player, side, move);
|
|
709
758
|
|
|
710
759
|
if (move.effect.onBegin()) {
|
|
711
760
|
try onBegin(battle, player, move, mslot, residual, log);
|
|
@@ -716,12 +765,27 @@ fn canMove(
|
|
|
716
765
|
Effects.thrashing(battle, player);
|
|
717
766
|
} else if (!showdown and move.effect == .Binding) {
|
|
718
767
|
// Pokémon Showdown handles this after hit/miss checks and damage calculation
|
|
719
|
-
Effects.binding(battle, player);
|
|
768
|
+
Effects.binding(battle, player, false);
|
|
720
769
|
}
|
|
721
770
|
|
|
722
771
|
return true;
|
|
723
772
|
}
|
|
724
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 = @intFromBool(counterable);
|
|
784
|
+
} else {
|
|
785
|
+
battle.last_moves.p2_counterable = @intFromBool(counterable);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
725
789
|
fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
|
|
726
790
|
if (side.last_selected_move == .Struggle) return;
|
|
727
791
|
|
|
@@ -732,11 +796,11 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
|
|
|
732
796
|
if (volatiles.Bide) return;
|
|
733
797
|
|
|
734
798
|
assert(active.move(mslot).pp > 0 or auto);
|
|
735
|
-
active.move(mslot).pp = @
|
|
799
|
+
active.move(mslot).pp = @as(u6, @intCast(active.move(mslot).pp)) -% 1;
|
|
736
800
|
if (volatiles.Transform) return;
|
|
737
801
|
|
|
738
802
|
assert(side.stored().move(mslot).pp > 0 or auto);
|
|
739
|
-
side.stored().move(mslot).pp = @
|
|
803
|
+
side.stored().move(mslot).pp = @as(u6, @intCast(side.stored().move(mslot).pp)) -% 1;
|
|
740
804
|
assert(active.move(mslot).pp == side.stored().move(mslot).pp);
|
|
741
805
|
}
|
|
742
806
|
|
|
@@ -744,12 +808,12 @@ fn incrementPP(side: *Side, mslot: u4) void {
|
|
|
744
808
|
var active = &side.active;
|
|
745
809
|
const volatiles = &active.volatiles;
|
|
746
810
|
|
|
747
|
-
active.move(mslot).pp = @
|
|
811
|
+
active.move(mslot).pp = @as(u6, @intCast(active.move(mslot).pp)) +% 1;
|
|
748
812
|
// GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
|
|
749
813
|
if (showdown and volatiles.Transform) return;
|
|
750
814
|
|
|
751
815
|
assert(mslot > 0 and mslot <= 4);
|
|
752
|
-
side.stored().moves[mslot - 1].pp = @
|
|
816
|
+
side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% 1;
|
|
753
817
|
}
|
|
754
818
|
|
|
755
819
|
// Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
|
|
@@ -757,6 +821,7 @@ fn doMove(
|
|
|
757
821
|
battle: anytype,
|
|
758
822
|
player: Player,
|
|
759
823
|
mslot: u4,
|
|
824
|
+
rewrap: bool,
|
|
760
825
|
auto: bool,
|
|
761
826
|
residual: *bool,
|
|
762
827
|
log: anytype,
|
|
@@ -784,15 +849,18 @@ fn doMove(
|
|
|
784
849
|
// calling moveHit early covers most of that but we also need to check type immunity first
|
|
785
850
|
var miss = showdown and miss: {
|
|
786
851
|
immune = move.target != .Self and !status and !counter and
|
|
787
|
-
(@
|
|
788
|
-
@
|
|
852
|
+
(@intFromEnum(move.type.effectiveness(foe.active.types.type1)) == 0 or
|
|
853
|
+
@intFromEnum(move.type.effectiveness(foe.active.types.type2)) == 0);
|
|
789
854
|
if (immune and move.effect != .Binding) break :miss true;
|
|
790
|
-
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
|
+
}
|
|
791
859
|
break :miss move.target != .Self and !moveHit(battle, player, move, &immune, &mist);
|
|
792
860
|
};
|
|
793
861
|
assert(!immune or miss or (showdown and move.effect == .Binding));
|
|
794
862
|
|
|
795
|
-
var late = showdown;
|
|
863
|
+
var late = showdown and move.effect != .Explode;
|
|
796
864
|
const skip = status or immune;
|
|
797
865
|
if ((!showdown or (!skip or counter)) and !miss) blk: {
|
|
798
866
|
if (showdown and move.effect.isMulti()) {
|
|
@@ -845,9 +913,9 @@ fn doMove(
|
|
|
845
913
|
|
|
846
914
|
if (!showdown or !miss) {
|
|
847
915
|
if (move.effect == .MirrorMove) {
|
|
848
|
-
return mirrorMove(battle, player, mslot, auto, residual, log);
|
|
916
|
+
return mirrorMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
849
917
|
} else if (move.effect == .Metronome) {
|
|
850
|
-
return metronome(battle, player, mslot, auto, residual, log);
|
|
918
|
+
return metronome(battle, player, mslot, rewrap, auto, residual, log);
|
|
851
919
|
} else if (move.effect.onEnd()) {
|
|
852
920
|
try onEnd(battle, player, move, log);
|
|
853
921
|
return null;
|
|
@@ -862,7 +930,7 @@ fn doMove(
|
|
|
862
930
|
move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
|
|
863
931
|
if (ohko) {
|
|
864
932
|
try log.immune(foe_ident, .OHKO);
|
|
865
|
-
} else if (immune and !invulnerable) {
|
|
933
|
+
} else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
|
|
866
934
|
try log.immune(foe_ident, .None);
|
|
867
935
|
} else if (mist) {
|
|
868
936
|
if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
|
|
@@ -873,7 +941,7 @@ fn doMove(
|
|
|
873
941
|
}
|
|
874
942
|
if (move.effect == .JumpKick) {
|
|
875
943
|
// Recoil is supposed to be damage/8 but damage will always be 0 here
|
|
876
|
-
assert(battle.last_damage == 0);
|
|
944
|
+
assert(showdown or battle.last_damage == 0);
|
|
877
945
|
battle.last_damage = 1;
|
|
878
946
|
_ = try applyDamage(battle, player, player.foe(), .None, log);
|
|
879
947
|
if (showdown and side.stored().hp == 0) residual.* = false;
|
|
@@ -894,6 +962,9 @@ fn doMove(
|
|
|
894
962
|
hits = side.active.volatiles.attacks;
|
|
895
963
|
}
|
|
896
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
|
+
|
|
897
968
|
var nullified = false;
|
|
898
969
|
var hit: u4 = 0;
|
|
899
970
|
while (hit < hits) {
|
|
@@ -909,7 +980,7 @@ fn doMove(
|
|
|
909
980
|
if (hit == 0 and ohko) try log.ohko();
|
|
910
981
|
hit += 1;
|
|
911
982
|
if (foe.stored().hp == 0) break;
|
|
912
|
-
if (!late) try buildRage(battle, player.foe(), log);
|
|
983
|
+
if (!late and (!sub or move.effect == .Explode)) try buildRage(battle, player.foe(), log);
|
|
913
984
|
// If the substitute breaks during a multi-hit attack, the attack ends
|
|
914
985
|
if (nullified) break;
|
|
915
986
|
}
|
|
@@ -925,11 +996,17 @@ fn doMove(
|
|
|
925
996
|
} else if (showdown) {
|
|
926
997
|
// This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
|
|
927
998
|
if (move.effect == .Binding) {
|
|
928
|
-
Effects.binding(battle, player);
|
|
999
|
+
Effects.binding(battle, player, rewrap);
|
|
929
1000
|
if (immune) {
|
|
930
1001
|
battle.last_damage = 0;
|
|
1002
|
+
assert(foe.stored().hp > 0);
|
|
931
1003
|
// Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
|
|
932
|
-
|
|
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
|
+
}
|
|
933
1010
|
return null;
|
|
934
1011
|
}
|
|
935
1012
|
}
|
|
@@ -946,8 +1023,8 @@ fn doMove(
|
|
|
946
1023
|
|
|
947
1024
|
if (foe.stored().hp == 0) return null;
|
|
948
1025
|
|
|
949
|
-
// Pokémon Showdown builds Rage at the wrong time for non-MultiHit
|
|
950
|
-
if (late and move.effect != .Disable) try buildRage(battle, player.foe(), log);
|
|
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);
|
|
951
1028
|
|
|
952
1029
|
if (!move.effect.isSpecial()) {
|
|
953
1030
|
// On the cartridge Rage is not considered to be "special" and thus gets executed for a
|
|
@@ -980,8 +1057,7 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
|
|
|
980
1057
|
chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
|
|
981
1058
|
chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
|
|
982
1059
|
|
|
983
|
-
|
|
984
|
-
return std.math.rotl(u8, battle.rng.next(), 3) < chance;
|
|
1060
|
+
return Rolls.criticalHit(battle, chance);
|
|
985
1061
|
}
|
|
986
1062
|
|
|
987
1063
|
fn calcDamage(
|
|
@@ -1047,7 +1123,7 @@ fn calcDamage(
|
|
|
1047
1123
|
d = @min(997, d);
|
|
1048
1124
|
d += 2;
|
|
1049
1125
|
|
|
1050
|
-
battle.last_damage = @intCast(
|
|
1126
|
+
battle.last_damage = @intCast(d);
|
|
1051
1127
|
|
|
1052
1128
|
return true;
|
|
1053
1129
|
}
|
|
@@ -1061,9 +1137,9 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
|
1061
1137
|
var d = battle.last_damage;
|
|
1062
1138
|
if (side.active.types.includes(move.type)) d +%= d / 2;
|
|
1063
1139
|
|
|
1064
|
-
const neutral = @
|
|
1065
|
-
const eff1: u16 = @
|
|
1066
|
-
const eff2: u16 = @
|
|
1140
|
+
const neutral = @intFromEnum(Effectiveness.Neutral);
|
|
1141
|
+
const eff1: u16 = @intFromEnum(move.type.effectiveness(types.type1));
|
|
1142
|
+
const eff2: u16 = @intFromEnum(move.type.effectiveness(types.type2));
|
|
1067
1143
|
|
|
1068
1144
|
// Type effectiveness matchup precedence only matters with (NVE, SE)
|
|
1069
1145
|
if (!showdown and (eff1 + eff2) == Effectiveness.mismatch and
|
|
@@ -1085,50 +1161,36 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
|
1085
1161
|
|
|
1086
1162
|
fn randomizeDamage(battle: anytype) void {
|
|
1087
1163
|
if (battle.last_damage <= 1) return;
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
battle.rng.range(u8, 217, 256)
|
|
1091
|
-
else loop: {
|
|
1092
|
-
while (true) {
|
|
1093
|
-
const r = std.math.rotr(u8, battle.rng.next(), 1);
|
|
1094
|
-
if (r >= 217) break :loop r;
|
|
1095
|
-
}
|
|
1096
|
-
};
|
|
1097
|
-
|
|
1098
|
-
battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
|
|
1164
|
+
const random = Rolls.damage(battle);
|
|
1165
|
+
battle.last_damage = @intCast(@as(u32, battle.last_damage) *% random / 255);
|
|
1099
1166
|
}
|
|
1100
1167
|
|
|
1101
1168
|
fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
|
|
1102
1169
|
const side = battle.side(player);
|
|
1170
|
+
const foe = battle.foe(player);
|
|
1103
1171
|
|
|
1104
1172
|
if (!try checkHit(battle, player, move, log)) return null;
|
|
1105
1173
|
|
|
1106
1174
|
battle.last_damage = switch (side.last_selected_move) {
|
|
1107
|
-
.SuperFang => @max(
|
|
1175
|
+
.SuperFang => @max(foe.stored().hp / 2, 1),
|
|
1108
1176
|
.SeismicToss, .NightShade => side.stored().level,
|
|
1109
1177
|
.SonicBoom => 20,
|
|
1110
1178
|
.DragonRage => 40,
|
|
1111
1179
|
// GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
|
|
1112
1180
|
.Psywave => power: {
|
|
1113
|
-
const max = @intCast(
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
// GLITCH: Psywave infinite glitch loop
|
|
1118
|
-
if (max <= 1) return Result.Error;
|
|
1119
|
-
while (true) {
|
|
1120
|
-
const r = battle.rng.next();
|
|
1121
|
-
if (r < max) break :power r;
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1181
|
+
const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
|
|
1182
|
+
// GLITCH: Psywave infinite glitch loop
|
|
1183
|
+
if (!showdown and max <= 1) return Result.Error;
|
|
1184
|
+
break :power Rolls.psywave(battle, max);
|
|
1124
1185
|
},
|
|
1125
1186
|
else => unreachable,
|
|
1126
1187
|
};
|
|
1127
1188
|
|
|
1128
1189
|
if (battle.last_damage == 0) return if (showdown) null else Result.Error;
|
|
1129
1190
|
|
|
1191
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1130
1192
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
1131
|
-
if (battle.foe(player).stored().hp > 0) try buildRage(battle, player.foe(), log);
|
|
1193
|
+
if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
|
|
1132
1194
|
|
|
1133
1195
|
return null;
|
|
1134
1196
|
}
|
|
@@ -1141,19 +1203,17 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
|
|
|
1141
1203
|
return null;
|
|
1142
1204
|
}
|
|
1143
1205
|
|
|
1144
|
-
// Pretend
|
|
1145
|
-
const foe_last_used_move =
|
|
1146
|
-
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
|
|
1147
1207
|
const foe_last_selected_move =
|
|
1148
1208
|
Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
|
|
1149
|
-
.
|
|
1209
|
+
.Splash
|
|
1150
1210
|
else
|
|
1151
1211
|
foe.last_selected_move);
|
|
1152
1212
|
|
|
1153
|
-
const used =
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1213
|
+
const used = (if (player.foe() == .P1)
|
|
1214
|
+
battle.last_moves.p1_counterable
|
|
1215
|
+
else
|
|
1216
|
+
battle.last_moves.p2_counterable) != 0;
|
|
1157
1217
|
|
|
1158
1218
|
const selected = foe_last_selected_move.bp > 0 and
|
|
1159
1219
|
foe.last_selected_move != .Counter and
|
|
@@ -1172,12 +1232,14 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
|
|
|
1172
1232
|
return null;
|
|
1173
1233
|
}
|
|
1174
1234
|
|
|
1175
|
-
battle.last_damage
|
|
1235
|
+
battle.last_damage *|= 2;
|
|
1176
1236
|
|
|
1177
1237
|
// Pokémon Showdown calls checkHit before Counter
|
|
1178
1238
|
if (!showdown and !try checkHit(battle, player, move, log)) return null;
|
|
1179
1239
|
|
|
1240
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1180
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);
|
|
1181
1243
|
return null;
|
|
1182
1244
|
}
|
|
1183
1245
|
|
|
@@ -1205,7 +1267,7 @@ fn applyDamage(
|
|
|
1205
1267
|
return true;
|
|
1206
1268
|
} else {
|
|
1207
1269
|
// Safe to truncate since less than subbed.volatiles.substitute which is a u8
|
|
1208
|
-
subbed.active.volatiles.substitute -= @intCast(
|
|
1270
|
+
subbed.active.volatiles.substitute -= @intCast(battle.last_damage);
|
|
1209
1271
|
try log.activate(battle.active(sub_player), .Substitute);
|
|
1210
1272
|
return false;
|
|
1211
1273
|
}
|
|
@@ -1221,6 +1283,7 @@ fn mirrorMove(
|
|
|
1221
1283
|
battle: anytype,
|
|
1222
1284
|
player: Player,
|
|
1223
1285
|
mslot: u4,
|
|
1286
|
+
rewrap: bool,
|
|
1224
1287
|
auto: bool,
|
|
1225
1288
|
residual: *bool,
|
|
1226
1289
|
log: anytype,
|
|
@@ -1233,41 +1296,30 @@ fn mirrorMove(
|
|
|
1233
1296
|
if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
|
|
1234
1297
|
try log.fail(battle.active(player), .None);
|
|
1235
1298
|
return null;
|
|
1299
|
+
} else if (!showdown or foe.last_used_move != .Struggle) {
|
|
1300
|
+
incrementPP(side, mslot);
|
|
1236
1301
|
}
|
|
1237
1302
|
|
|
1238
|
-
incrementPP(side, mslot);
|
|
1239
|
-
|
|
1240
1303
|
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
|
|
1241
|
-
return doMove(battle, player, mslot, auto, residual, log);
|
|
1304
|
+
return doMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
1242
1305
|
}
|
|
1243
1306
|
|
|
1244
1307
|
fn metronome(
|
|
1245
1308
|
battle: anytype,
|
|
1246
1309
|
player: Player,
|
|
1247
1310
|
mslot: u4,
|
|
1311
|
+
rewrap: bool,
|
|
1248
1312
|
auto: bool,
|
|
1249
1313
|
residual: *bool,
|
|
1250
1314
|
log: anytype,
|
|
1251
1315
|
) !?Result {
|
|
1252
1316
|
var side = battle.side(player);
|
|
1253
1317
|
|
|
1254
|
-
side.last_selected_move =
|
|
1255
|
-
const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
|
|
1256
|
-
const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
|
|
1257
|
-
break :blk @intToEnum(Move, r + mod);
|
|
1258
|
-
} else loop: {
|
|
1259
|
-
while (true) {
|
|
1260
|
-
const r = battle.rng.next();
|
|
1261
|
-
if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
|
|
1262
|
-
if (r >= @enumToInt(Move.Struggle)) continue;
|
|
1263
|
-
break :loop @intToEnum(Move, r);
|
|
1264
|
-
}
|
|
1265
|
-
};
|
|
1266
|
-
|
|
1318
|
+
side.last_selected_move = Rolls.metronome(battle);
|
|
1267
1319
|
incrementPP(side, mslot);
|
|
1268
1320
|
|
|
1269
1321
|
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
|
|
1270
|
-
return doMove(battle, player, mslot, auto, residual, log);
|
|
1322
|
+
return doMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
1271
1323
|
}
|
|
1272
1324
|
|
|
1273
1325
|
fn checkHit(battle: anytype, player: Player, move: Move.Data, log: anytype) !bool {
|
|
@@ -1321,23 +1373,24 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
|
|
|
1321
1373
|
}
|
|
1322
1374
|
|
|
1323
1375
|
// GLITCH: Thrash / Petal Dance / Rage get their accuracy overwritten on subsequent hits
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
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));
|
|
1382
|
+
var boost = BOOSTS[@intCast(@as(i8, side.active.boosts.accuracy) + 6)];
|
|
1329
1383
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1330
|
-
boost = BOOSTS[@intCast(
|
|
1384
|
+
boost = BOOSTS[@intCast(@as(i8, -foe.active.boosts.evasion) + 6)];
|
|
1331
1385
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1332
1386
|
accuracy = @min(255, @max(1, accuracy));
|
|
1333
1387
|
|
|
1334
|
-
|
|
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;
|
|
1335
1391
|
|
|
1336
1392
|
// GLITCH: max accuracy is 255 so 1/256 chance of miss
|
|
1337
|
-
break :miss
|
|
1338
|
-
!battle.rng.chance(u8, @intCast(u8, accuracy), 256)
|
|
1339
|
-
else
|
|
1340
|
-
battle.rng.next() >= accuracy;
|
|
1393
|
+
break :miss !Rolls.hit(battle, accuracy);
|
|
1341
1394
|
};
|
|
1342
1395
|
|
|
1343
1396
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1347,7 +1400,7 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
|
|
|
1347
1400
|
}
|
|
1348
1401
|
|
|
1349
1402
|
if (!miss) return true;
|
|
1350
|
-
battle.last_damage = 0;
|
|
1403
|
+
if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
|
|
1351
1404
|
side.active.volatiles.Binding = false;
|
|
1352
1405
|
return false;
|
|
1353
1406
|
}
|
|
@@ -1469,9 +1522,6 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
|
|
|
1469
1522
|
damage *= volatiles.toxic;
|
|
1470
1523
|
}
|
|
1471
1524
|
|
|
1472
|
-
// Pokémon Showdown erroneously updates last damage with uncapped Leech Seed damage
|
|
1473
|
-
if (showdown) battle.last_damage = damage;
|
|
1474
|
-
|
|
1475
1525
|
const amount = @min(damage, stored.hp);
|
|
1476
1526
|
stored.hp -= amount;
|
|
1477
1527
|
|
|
@@ -1490,13 +1540,13 @@ fn endTurn(battle: anytype, log: anytype) @TypeOf(log).Error!Result {
|
|
|
1490
1540
|
assert(!battle.side(.P1).active.volatiles.MultiHit);
|
|
1491
1541
|
assert(!battle.side(.P2).active.volatiles.MultiHit);
|
|
1492
1542
|
|
|
1543
|
+
battle.turn += 1;
|
|
1544
|
+
|
|
1493
1545
|
if (showdown and options.ebc and checkEBC(battle)) {
|
|
1494
1546
|
try log.tie();
|
|
1495
1547
|
return Result.Tie;
|
|
1496
1548
|
}
|
|
1497
1549
|
|
|
1498
|
-
battle.turn += 1;
|
|
1499
|
-
|
|
1500
1550
|
if (showdown and battle.turn >= 1000) {
|
|
1501
1551
|
try log.tie();
|
|
1502
1552
|
return Result.Tie;
|
|
@@ -1511,7 +1561,7 @@ fn endTurn(battle: anytype, log: anytype) @TypeOf(log).Error!Result {
|
|
|
1511
1561
|
|
|
1512
1562
|
fn checkEBC(battle: anytype) bool {
|
|
1513
1563
|
for (battle.sides, 0..) |side, i| {
|
|
1514
|
-
const foe = battle.sides[~@
|
|
1564
|
+
const foe = battle.sides[~@as(u1, @intCast(i))];
|
|
1515
1565
|
|
|
1516
1566
|
var foe_all_ghosts = true;
|
|
1517
1567
|
var foe_all_transform = true;
|
|
@@ -1563,6 +1613,42 @@ fn checkEBC(battle: anytype) bool {
|
|
|
1563
1613
|
return false;
|
|
1564
1614
|
}
|
|
1565
1615
|
|
|
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
|
+
|
|
1566
1652
|
inline fn onBegin(
|
|
1567
1653
|
battle: anytype,
|
|
1568
1654
|
player: Player,
|
|
@@ -1653,10 +1739,7 @@ pub const Effects = struct {
|
|
|
1653
1739
|
side.active.volatiles.Bide = true;
|
|
1654
1740
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1655
1741
|
side.active.volatiles.state = 0;
|
|
1656
|
-
side.active.volatiles.attacks =
|
|
1657
|
-
battle.rng.range(u4, 2, 4)
|
|
1658
|
-
else
|
|
1659
|
-
(battle.rng.next() & 1) + 2);
|
|
1742
|
+
side.active.volatiles.attacks = Rolls.bideThrashDuration(battle);
|
|
1660
1743
|
|
|
1661
1744
|
try log.start(battle.active(player), .Bide);
|
|
1662
1745
|
}
|
|
@@ -1678,14 +1761,8 @@ pub const Effects = struct {
|
|
|
1678
1761
|
return;
|
|
1679
1762
|
}
|
|
1680
1763
|
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
else
|
|
1684
|
-
battle.rng.next() < 1 + (if (move.effect == .BurnChance1)
|
|
1685
|
-
Gen12.percent(10)
|
|
1686
|
-
else
|
|
1687
|
-
Gen12.percent(30));
|
|
1688
|
-
if (!chance) return;
|
|
1764
|
+
if (foe.active.types.includes(move.type)) return;
|
|
1765
|
+
if (!Rolls.secondaryChance(battle, move.effect == .BurnChance1)) return;
|
|
1689
1766
|
|
|
1690
1767
|
foe_stored.status = Status.init(.BRN);
|
|
1691
1768
|
foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
|
|
@@ -1709,11 +1786,7 @@ pub const Effects = struct {
|
|
|
1709
1786
|
const sub = foe.active.volatiles.Substitute;
|
|
1710
1787
|
|
|
1711
1788
|
if (move.effect == .ConfusionChance) {
|
|
1712
|
-
|
|
1713
|
-
battle.rng.chance(u8, 25, 256)
|
|
1714
|
-
else
|
|
1715
|
-
battle.rng.next() < Gen12.percent(10);
|
|
1716
|
-
if (!chance) return;
|
|
1789
|
+
if (!Rolls.confusionChance(battle)) return;
|
|
1717
1790
|
} else {
|
|
1718
1791
|
if (showdown) {
|
|
1719
1792
|
if (!try checkHit(battle, player, move, log)) {
|
|
@@ -1732,10 +1805,7 @@ pub const Effects = struct {
|
|
|
1732
1805
|
|
|
1733
1806
|
if (foe.active.volatiles.Confusion) return;
|
|
1734
1807
|
foe.active.volatiles.Confusion = true;
|
|
1735
|
-
foe.active.volatiles.confusion =
|
|
1736
|
-
battle.rng.range(u8, 2, 6)
|
|
1737
|
-
else
|
|
1738
|
-
(battle.rng.next() & 3) + 2);
|
|
1808
|
+
foe.active.volatiles.confusion = Rolls.confusionDuration(battle);
|
|
1739
1809
|
|
|
1740
1810
|
try log.start(battle.active(player.foe()), .Confusion);
|
|
1741
1811
|
}
|
|
@@ -1785,11 +1855,8 @@ pub const Effects = struct {
|
|
|
1785
1855
|
return Result.Error;
|
|
1786
1856
|
}
|
|
1787
1857
|
|
|
1788
|
-
volatiles.disabled_move = @intCast(
|
|
1789
|
-
volatiles.disabled_duration =
|
|
1790
|
-
battle.rng.range(u8, 1, 9)
|
|
1791
|
-
else
|
|
1792
|
-
(battle.rng.next() & 7) + 1);
|
|
1858
|
+
volatiles.disabled_move = @intCast(Rolls.moveSlot(battle, &foe.active.moves, n));
|
|
1859
|
+
volatiles.disabled_duration = Rolls.disableDuration(battle);
|
|
1793
1860
|
|
|
1794
1861
|
try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
|
|
1795
1862
|
return null;
|
|
@@ -1801,7 +1868,7 @@ pub const Effects = struct {
|
|
|
1801
1868
|
|
|
1802
1869
|
if (battle.last_damage == 0) {
|
|
1803
1870
|
assert(showdown);
|
|
1804
|
-
return;
|
|
1871
|
+
if (!battle.foe(player).active.volatiles.Substitute) return;
|
|
1805
1872
|
}
|
|
1806
1873
|
|
|
1807
1874
|
const drain = @max(battle.last_damage / 2, 1);
|
|
@@ -1828,15 +1895,7 @@ pub const Effects = struct {
|
|
|
1828
1895
|
var volatiles = &battle.foe(player).active.volatiles;
|
|
1829
1896
|
|
|
1830
1897
|
if (volatiles.Substitute) return;
|
|
1831
|
-
|
|
1832
|
-
const chance = if (showdown)
|
|
1833
|
-
battle.rng.chance(u8, @as(u8, if (move.effect == .FlinchChance1) 26 else 77), 256)
|
|
1834
|
-
else
|
|
1835
|
-
battle.rng.next() < 1 + (if (move.effect == .FlinchChance1)
|
|
1836
|
-
Gen12.percent(10)
|
|
1837
|
-
else
|
|
1838
|
-
Gen12.percent(30));
|
|
1839
|
-
if (!chance) return;
|
|
1898
|
+
if (!Rolls.secondaryChance(battle, move.effect == .FlinchChance1)) return;
|
|
1840
1899
|
|
|
1841
1900
|
volatiles.Flinch = true;
|
|
1842
1901
|
volatiles.Recharging = false;
|
|
@@ -1862,12 +1921,8 @@ pub const Effects = struct {
|
|
|
1862
1921
|
return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
|
|
1863
1922
|
}
|
|
1864
1923
|
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
else
|
|
1868
|
-
battle.rng.next() < 1 + Gen12.percent(10);
|
|
1869
|
-
if (!chance) return;
|
|
1870
|
-
|
|
1924
|
+
if (foe.active.types.includes(move.type)) return;
|
|
1925
|
+
if (!Rolls.secondaryChance(battle, true)) return;
|
|
1871
1926
|
// Freeze Clause Mod
|
|
1872
1927
|
if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
1873
1928
|
|
|
@@ -1899,7 +1954,7 @@ pub const Effects = struct {
|
|
|
1899
1954
|
// Pokémon Showdown clears P1 then P2 instead of status -> side -> foe
|
|
1900
1955
|
if (showdown) {
|
|
1901
1956
|
for (&battle.sides, 0..) |*s, i| {
|
|
1902
|
-
const p = @
|
|
1957
|
+
const p: Player = @enumFromInt(i);
|
|
1903
1958
|
// Pokémon Showdown incorrectly does not prevent sleep/freeze from moving
|
|
1904
1959
|
if (p != player and Status.any(s.stored().status)) {
|
|
1905
1960
|
try log.curestatus(foe_ident, foe_stored.status, .Silent);
|
|
@@ -1930,7 +1985,9 @@ pub const Effects = struct {
|
|
|
1930
1985
|
|
|
1931
1986
|
// GLITCH: HP recovery move failure glitches
|
|
1932
1987
|
const delta = stored.stats.hp - stored.hp;
|
|
1933
|
-
if (delta == 0 or delta & 255 == 255
|
|
1988
|
+
if (delta == 0 or (delta & 255 == 255 and stored.hp % 256 != 0)) {
|
|
1989
|
+
return try log.fail(ident, .None);
|
|
1990
|
+
}
|
|
1934
1991
|
|
|
1935
1992
|
const rest = side.last_selected_move == .Rest;
|
|
1936
1993
|
if (rest) {
|
|
@@ -1998,21 +2055,21 @@ pub const Effects = struct {
|
|
|
1998
2055
|
const has_mimic = has_mimic: {
|
|
1999
2056
|
for (side.active.moves, 0..) |m, i| {
|
|
2000
2057
|
if (m.id == .Mimic) {
|
|
2001
|
-
oslot = @intCast(
|
|
2058
|
+
oslot = @intCast(i + 1);
|
|
2002
2059
|
break :has_mimic true;
|
|
2003
2060
|
}
|
|
2004
2061
|
}
|
|
2005
2062
|
break :has_mimic false;
|
|
2006
2063
|
};
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2064
|
+
if (!has_mimic) {
|
|
2065
|
+
// Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
|
|
2066
|
+
if (!try checkHit(battle, player, move, log)) return;
|
|
2010
2067
|
return try log.fail(battle.active(player.foe()), .None);
|
|
2011
2068
|
}
|
|
2012
2069
|
}
|
|
2013
2070
|
if (!try checkHit(battle, player, move, log)) return;
|
|
2014
2071
|
|
|
2015
|
-
const rslot =
|
|
2072
|
+
const rslot = Rolls.moveSlot(battle, &foe.active.moves, 0);
|
|
2016
2073
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
2017
2074
|
|
|
2018
2075
|
try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
|
|
@@ -2033,7 +2090,8 @@ pub const Effects = struct {
|
|
|
2033
2090
|
assert(!side.active.volatiles.MultiHit);
|
|
2034
2091
|
side.active.volatiles.MultiHit = true;
|
|
2035
2092
|
|
|
2036
|
-
side.active.volatiles.attacks =
|
|
2093
|
+
side.active.volatiles.attacks =
|
|
2094
|
+
if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
|
|
2037
2095
|
}
|
|
2038
2096
|
|
|
2039
2097
|
fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2077,14 +2135,8 @@ pub const Effects = struct {
|
|
|
2077
2135
|
}
|
|
2078
2136
|
|
|
2079
2137
|
// Body Slam can't paralyze a Normal type Pokémon
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
else
|
|
2083
|
-
battle.rng.next() < 1 + (if (move.effect == .ParalyzeChance1)
|
|
2084
|
-
Gen12.percent(10)
|
|
2085
|
-
else
|
|
2086
|
-
Gen12.percent(30));
|
|
2087
|
-
if (!chance) return;
|
|
2138
|
+
if (foe.active.types.includes(move.type)) return;
|
|
2139
|
+
if (!Rolls.secondaryChance(battle, move.effect == .ParalyzeChance1)) return;
|
|
2088
2140
|
|
|
2089
2141
|
foe_stored.status = Status.init(.PAR);
|
|
2090
2142
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
@@ -2127,16 +2179,7 @@ pub const Effects = struct {
|
|
|
2127
2179
|
|
|
2128
2180
|
if (move.effect == .Poison) {
|
|
2129
2181
|
if (!showdown and !try checkHit(battle, player, move, log)) return;
|
|
2130
|
-
} else
|
|
2131
|
-
const chance = if (showdown)
|
|
2132
|
-
battle.rng.chance(u8, @as(u8, if (move.effect == .PoisonChance1) 52 else 103), 256)
|
|
2133
|
-
else
|
|
2134
|
-
battle.rng.next() < 1 + (if (move.effect == .PoisonChance1)
|
|
2135
|
-
Gen12.percent(20)
|
|
2136
|
-
else
|
|
2137
|
-
Gen12.percent(40));
|
|
2138
|
-
if (!chance) return;
|
|
2139
|
-
}
|
|
2182
|
+
} else if (!Rolls.poisonChance(battle, move.effect == .PoisonChance1)) return;
|
|
2140
2183
|
|
|
2141
2184
|
foe_stored.status = Status.init(.PSN);
|
|
2142
2185
|
if (toxic) {
|
|
@@ -2150,19 +2193,20 @@ pub const Effects = struct {
|
|
|
2150
2193
|
|
|
2151
2194
|
fn rage(battle: anytype, player: Player) !void {
|
|
2152
2195
|
var volatiles = &battle.side(player).active.volatiles;
|
|
2153
|
-
if (volatiles.Rage) return;
|
|
2154
|
-
volatiles.Rage = true;
|
|
2155
2196
|
assert(!volatiles.Bide);
|
|
2156
|
-
volatiles.
|
|
2197
|
+
volatiles.Rage = true;
|
|
2157
2198
|
}
|
|
2158
2199
|
|
|
2159
2200
|
fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
|
|
2160
2201
|
var side = battle.side(player);
|
|
2161
2202
|
var stored = side.stored();
|
|
2162
2203
|
|
|
2163
|
-
|
|
2204
|
+
assert(showdown or battle.last_damage > 0);
|
|
2205
|
+
if (showdown and battle.last_damage == 0) return;
|
|
2206
|
+
|
|
2207
|
+
const damage: i16 = @intCast(@max(battle.last_damage /
|
|
2164
2208
|
@as(u8, if (side.last_selected_move == .Struggle) 2 else 4), 1));
|
|
2165
|
-
stored.hp = @intCast(
|
|
2209
|
+
stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
|
|
2166
2210
|
|
|
2167
2211
|
try log.damageOf(battle.active(player), stored, .RecoilOf, battle.active(player.foe()));
|
|
2168
2212
|
if (showdown and stored.hp == 0) residual.* = false;
|
|
@@ -2205,16 +2249,7 @@ pub const Effects = struct {
|
|
|
2205
2249
|
}
|
|
2206
2250
|
foe.active.volatiles.Recharging = false;
|
|
2207
2251
|
|
|
2208
|
-
|
|
2209
|
-
battle.rng.range(u8, 1, 8)
|
|
2210
|
-
else loop: {
|
|
2211
|
-
while (true) {
|
|
2212
|
-
const r = battle.rng.next() & 7;
|
|
2213
|
-
if (r != 0) break :loop r;
|
|
2214
|
-
}
|
|
2215
|
-
});
|
|
2216
|
-
|
|
2217
|
-
foe_stored.status = Status.slp(duration);
|
|
2252
|
+
foe_stored.status = Status.slp(Rolls.sleepDuration(battle));
|
|
2218
2253
|
try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
|
|
2219
2254
|
}
|
|
2220
2255
|
|
|
@@ -2231,7 +2266,7 @@ pub const Effects = struct {
|
|
|
2231
2266
|
|
|
2232
2267
|
assert(side.stored().stats.hp <= 1023);
|
|
2233
2268
|
// Will be 0 if HP is <= 3 meaning that the user gets a 1 HP Substitute for "free"
|
|
2234
|
-
const hp = @intCast(
|
|
2269
|
+
const hp: u8 = @intCast(side.stored().stats.hp / 4);
|
|
2235
2270
|
// Pokénon Showdown incorrectly checks for 1/4 HP based on `target.maxhp / 4` which returns
|
|
2236
2271
|
// a floating point value and thus only correctly implements the Substitute 1/4 glitch when
|
|
2237
2272
|
// the target's HP is exactly divisible by 4 (here we're using an inlined divCeil routine to
|
|
@@ -2267,11 +2302,7 @@ pub const Effects = struct {
|
|
|
2267
2302
|
assert(!volatiles.Bide);
|
|
2268
2303
|
|
|
2269
2304
|
volatiles.Thrashing = true;
|
|
2270
|
-
volatiles.
|
|
2271
|
-
volatiles.attacks = @intCast(u3, if (showdown)
|
|
2272
|
-
battle.rng.range(u8, 2, 4)
|
|
2273
|
-
else
|
|
2274
|
-
(battle.rng.next() & 1) + 2);
|
|
2305
|
+
volatiles.attacks = Rolls.bideThrashDuration(battle);
|
|
2275
2306
|
}
|
|
2276
2307
|
|
|
2277
2308
|
fn transform(battle: anytype, player: Player, log: anytype) !void {
|
|
@@ -2303,7 +2334,7 @@ pub const Effects = struct {
|
|
|
2303
2334
|
try log.transform(battle.active(player), foe_ident);
|
|
2304
2335
|
}
|
|
2305
2336
|
|
|
2306
|
-
fn binding(battle: anytype, player: Player) void {
|
|
2337
|
+
fn binding(battle: anytype, player: Player, rewrap: bool) void {
|
|
2307
2338
|
var side = battle.side(player);
|
|
2308
2339
|
var foe = battle.foe(player);
|
|
2309
2340
|
|
|
@@ -2311,9 +2342,11 @@ pub const Effects = struct {
|
|
|
2311
2342
|
side.active.volatiles.Binding = true;
|
|
2312
2343
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2313
2344
|
// (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
|
|
2314
|
-
if (!showdown)
|
|
2345
|
+
if (!showdown) {
|
|
2346
|
+
foe.active.volatiles.Recharging = false;
|
|
2347
|
+
} else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
|
|
2315
2348
|
|
|
2316
|
-
side.active.volatiles.attacks = distribution(battle) - 1;
|
|
2349
|
+
side.active.volatiles.attacks = Rolls.distribution(battle) - 1;
|
|
2317
2350
|
}
|
|
2318
2351
|
|
|
2319
2352
|
fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2328,7 +2361,7 @@ pub const Effects = struct {
|
|
|
2328
2361
|
assert(boosts.atk >= -6 and boosts.atk <= 6);
|
|
2329
2362
|
if (boosts.atk == 6) return try log.fail(ident, .None);
|
|
2330
2363
|
const n: u2 = if (move.effect == .AttackUp2) 2 else 1;
|
|
2331
|
-
boosts.atk = @intCast(
|
|
2364
|
+
boosts.atk = @intCast(@min(6, @as(i8, boosts.atk) + n));
|
|
2332
2365
|
const reason = if (move.effect == .Rage) Boost.Rage else Boost.Attack;
|
|
2333
2366
|
if (stats.atk == MAX_STAT_VALUE) {
|
|
2334
2367
|
boosts.atk -= 1;
|
|
@@ -2339,16 +2372,18 @@ pub const Effects = struct {
|
|
|
2339
2372
|
}
|
|
2340
2373
|
return try log.fail(ident, .None);
|
|
2341
2374
|
}
|
|
2342
|
-
var mod = BOOSTS[@intCast(
|
|
2343
|
-
const stat = unmodifiedStats(battle,
|
|
2375
|
+
var mod = BOOSTS[@intCast(@as(i8, boosts.atk) + 6)];
|
|
2376
|
+
const stat = unmodifiedStats(battle, side).atk;
|
|
2344
2377
|
stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2345
2378
|
try log.boost(ident, reason, n);
|
|
2379
|
+
// Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
|
|
2380
|
+
if (showdown and move.effect == .Rage) return;
|
|
2346
2381
|
},
|
|
2347
2382
|
.DefenseUp1, .DefenseUp2 => {
|
|
2348
2383
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
2349
2384
|
if (boosts.def == 6) return try log.fail(ident, .None);
|
|
2350
2385
|
const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
|
|
2351
|
-
boosts.def = @intCast(
|
|
2386
|
+
boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
|
|
2352
2387
|
if (stats.def == MAX_STAT_VALUE) {
|
|
2353
2388
|
boosts.def -= 1;
|
|
2354
2389
|
if (showdown) {
|
|
@@ -2357,15 +2392,15 @@ pub const Effects = struct {
|
|
|
2357
2392
|
}
|
|
2358
2393
|
return try log.fail(ident, .None);
|
|
2359
2394
|
}
|
|
2360
|
-
var mod = BOOSTS[@intCast(
|
|
2361
|
-
const stat = unmodifiedStats(battle,
|
|
2395
|
+
var mod = BOOSTS[@intCast(@as(i8, boosts.def) + 6)];
|
|
2396
|
+
const stat = unmodifiedStats(battle, side).def;
|
|
2362
2397
|
stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2363
2398
|
try log.boost(ident, .Defense, n);
|
|
2364
2399
|
},
|
|
2365
2400
|
.SpeedUp2 => {
|
|
2366
2401
|
assert(boosts.spe >= -6 and boosts.spe <= 6);
|
|
2367
2402
|
if (boosts.spe == 6) return try log.fail(ident, .None);
|
|
2368
|
-
boosts.spe = @intCast(
|
|
2403
|
+
boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
|
|
2369
2404
|
if (stats.spe == MAX_STAT_VALUE) {
|
|
2370
2405
|
boosts.spe -= 1;
|
|
2371
2406
|
if (showdown) {
|
|
@@ -2374,8 +2409,8 @@ pub const Effects = struct {
|
|
|
2374
2409
|
}
|
|
2375
2410
|
return try log.fail(ident, .None);
|
|
2376
2411
|
}
|
|
2377
|
-
var mod = BOOSTS[@intCast(
|
|
2378
|
-
const stat = unmodifiedStats(battle,
|
|
2412
|
+
var mod = BOOSTS[@intCast(@as(i8, boosts.spe) + 6)];
|
|
2413
|
+
const stat = unmodifiedStats(battle, side).spe;
|
|
2379
2414
|
stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2380
2415
|
try log.boost(ident, .Speed, 2);
|
|
2381
2416
|
},
|
|
@@ -2383,7 +2418,7 @@ pub const Effects = struct {
|
|
|
2383
2418
|
assert(boosts.spc >= -6 and boosts.spc <= 6);
|
|
2384
2419
|
if (boosts.spc == 6) return try log.fail(ident, .None);
|
|
2385
2420
|
const n: u2 = if (move.effect == .SpecialUp2) 2 else 1;
|
|
2386
|
-
boosts.spc = @intCast(
|
|
2421
|
+
boosts.spc = @intCast(@min(6, @as(i8, boosts.spc) + n));
|
|
2387
2422
|
if (stats.spc == MAX_STAT_VALUE) {
|
|
2388
2423
|
boosts.spc -= 1;
|
|
2389
2424
|
if (showdown) {
|
|
@@ -2394,8 +2429,8 @@ pub const Effects = struct {
|
|
|
2394
2429
|
}
|
|
2395
2430
|
return try log.fail(ident, .None);
|
|
2396
2431
|
}
|
|
2397
|
-
var mod = BOOSTS[@intCast(
|
|
2398
|
-
const stat = unmodifiedStats(battle,
|
|
2432
|
+
var mod = BOOSTS[@intCast(@as(i8, boosts.spc) + 6)];
|
|
2433
|
+
const stat = unmodifiedStats(battle, side).spc;
|
|
2399
2434
|
stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2400
2435
|
try log.boost(ident, .SpecialAttack, n);
|
|
2401
2436
|
try log.boost(ident, .SpecialDefense, n);
|
|
@@ -2403,7 +2438,7 @@ pub const Effects = struct {
|
|
|
2403
2438
|
.EvasionUp1 => {
|
|
2404
2439
|
assert(boosts.evasion >= -6 and boosts.evasion <= 6);
|
|
2405
2440
|
if (boosts.evasion == 6) return try log.fail(ident, .None);
|
|
2406
|
-
boosts.evasion = @intCast(
|
|
2441
|
+
boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
|
|
2407
2442
|
try log.boost(ident, .Evasion, 1);
|
|
2408
2443
|
},
|
|
2409
2444
|
else => unreachable,
|
|
@@ -2416,29 +2451,25 @@ pub const Effects = struct {
|
|
|
2416
2451
|
fn unboost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
2417
2452
|
var foe = battle.foe(player);
|
|
2418
2453
|
const foe_ident = battle.active(player.foe());
|
|
2454
|
+
const secondary = move.effect.isStatDownChance();
|
|
2419
2455
|
|
|
2420
|
-
if (foe.active.volatiles.Substitute)
|
|
2421
|
-
return if (!move.effect.isStatDownChance()) log.fail(foe_ident, .None);
|
|
2422
|
-
}
|
|
2456
|
+
if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(foe_ident, .None);
|
|
2423
2457
|
|
|
2424
|
-
if (
|
|
2425
|
-
|
|
2426
|
-
battle.rng.chance(u8, 85, 256)
|
|
2427
|
-
else
|
|
2428
|
-
battle.rng.next() < Gen12.percent(33) + 1;
|
|
2429
|
-
if (!chance or foe.active.volatiles.Invulnerable) return;
|
|
2458
|
+
if (secondary) {
|
|
2459
|
+
if (!Rolls.unboost(battle) or foe.active.volatiles.Invulnerable) return;
|
|
2430
2460
|
} else if (!showdown and !try checkHit(battle, player, move, log)) {
|
|
2431
2461
|
return; // checkHit already checks for Invulnerable
|
|
2432
2462
|
}
|
|
2433
2463
|
|
|
2434
2464
|
var stats = &foe.active.stats;
|
|
2435
2465
|
var boosts = &foe.active.boosts;
|
|
2466
|
+
const fail = showdown or !secondary;
|
|
2436
2467
|
|
|
2437
2468
|
switch (move.effect) {
|
|
2438
2469
|
.AttackDown1, .AttackDownChance => {
|
|
2439
2470
|
assert(boosts.atk >= -6 and boosts.atk <= 6);
|
|
2440
|
-
if (boosts.atk == -6) return try log.fail(foe_ident, .None);
|
|
2441
|
-
boosts.atk = @intCast(
|
|
2471
|
+
if (boosts.atk == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2472
|
+
boosts.atk = @intCast(@max(-6, @as(i8, boosts.atk) - 1));
|
|
2442
2473
|
if (stats.atk == 1) {
|
|
2443
2474
|
boosts.atk += 1;
|
|
2444
2475
|
if (showdown) {
|
|
@@ -2447,16 +2478,16 @@ pub const Effects = struct {
|
|
|
2447
2478
|
}
|
|
2448
2479
|
return try log.fail(foe_ident, .None);
|
|
2449
2480
|
}
|
|
2450
|
-
var mod = BOOSTS[@intCast(
|
|
2451
|
-
const stat = unmodifiedStats(battle,
|
|
2481
|
+
var mod = BOOSTS[@intCast(@as(i8, boosts.atk) + 6)];
|
|
2482
|
+
const stat = unmodifiedStats(battle, foe).atk;
|
|
2452
2483
|
stats.atk = @max(1, stat * mod[0] / mod[1]);
|
|
2453
2484
|
try log.boost(foe_ident, .Attack, -1);
|
|
2454
2485
|
},
|
|
2455
2486
|
.DefenseDown1, .DefenseDown2, .DefenseDownChance => {
|
|
2456
2487
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
2457
|
-
if (boosts.def == -6) return try log.fail(foe_ident, .None);
|
|
2488
|
+
if (boosts.def == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2458
2489
|
const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
|
|
2459
|
-
boosts.def = @intCast(
|
|
2490
|
+
boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
|
|
2460
2491
|
if (stats.def == 1) {
|
|
2461
2492
|
boosts.def += 1;
|
|
2462
2493
|
if (showdown) {
|
|
@@ -2465,15 +2496,15 @@ pub const Effects = struct {
|
|
|
2465
2496
|
}
|
|
2466
2497
|
return try log.fail(foe_ident, .None);
|
|
2467
2498
|
}
|
|
2468
|
-
var mod = BOOSTS[@intCast(
|
|
2469
|
-
const stat = unmodifiedStats(battle,
|
|
2499
|
+
var mod = BOOSTS[@intCast(@as(i8, boosts.def) + 6)];
|
|
2500
|
+
const stat = unmodifiedStats(battle, foe).def;
|
|
2470
2501
|
stats.def = @max(1, stat * mod[0] / mod[1]);
|
|
2471
2502
|
try log.boost(foe_ident, .Defense, -@as(i8, n));
|
|
2472
2503
|
},
|
|
2473
2504
|
.SpeedDown1, .SpeedDownChance => {
|
|
2474
2505
|
assert(boosts.spe >= -6 and boosts.spe <= 6);
|
|
2475
|
-
if (boosts.spe == -6) return try log.fail(foe_ident, .None);
|
|
2476
|
-
boosts.spe = @intCast(
|
|
2506
|
+
if (boosts.spe == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2507
|
+
boosts.spe = @intCast(@max(-6, @as(i8, boosts.spe) - 1));
|
|
2477
2508
|
if (stats.spe == 1) {
|
|
2478
2509
|
boosts.spe += 1;
|
|
2479
2510
|
if (showdown) {
|
|
@@ -2482,16 +2513,16 @@ pub const Effects = struct {
|
|
|
2482
2513
|
}
|
|
2483
2514
|
return try log.fail(foe_ident, .None);
|
|
2484
2515
|
}
|
|
2485
|
-
var mod = BOOSTS[@intCast(
|
|
2486
|
-
const stat = unmodifiedStats(battle,
|
|
2516
|
+
var mod = BOOSTS[@intCast(@as(i8, boosts.spe) + 6)];
|
|
2517
|
+
const stat = unmodifiedStats(battle, foe).spe;
|
|
2487
2518
|
stats.spe = @max(1, stat * mod[0] / mod[1]);
|
|
2488
2519
|
try log.boost(foe_ident, .Speed, -1);
|
|
2489
2520
|
assert(boosts.spe >= -6);
|
|
2490
2521
|
},
|
|
2491
2522
|
.SpecialDownChance => {
|
|
2492
2523
|
assert(boosts.spc >= -6 and boosts.spc <= 6);
|
|
2493
|
-
if (boosts.spc == -6) return try log.fail(foe_ident, .None);
|
|
2494
|
-
boosts.spc = @intCast(
|
|
2524
|
+
if (boosts.spc == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2525
|
+
boosts.spc = @intCast(@max(-6, @as(i8, boosts.spc) - 1));
|
|
2495
2526
|
if (stats.spc == 1) {
|
|
2496
2527
|
boosts.spc += 1;
|
|
2497
2528
|
if (showdown) {
|
|
@@ -2502,16 +2533,16 @@ pub const Effects = struct {
|
|
|
2502
2533
|
}
|
|
2503
2534
|
return try log.fail(foe_ident, .None);
|
|
2504
2535
|
}
|
|
2505
|
-
var mod = BOOSTS[@intCast(
|
|
2506
|
-
const stat = unmodifiedStats(battle,
|
|
2536
|
+
var mod = BOOSTS[@intCast(@as(i8, boosts.spc) + 6)];
|
|
2537
|
+
const stat = unmodifiedStats(battle, foe).spc;
|
|
2507
2538
|
stats.spc = @max(1, stat * mod[0] / mod[1]);
|
|
2508
2539
|
try log.boost(foe_ident, .SpecialAttack, -1);
|
|
2509
2540
|
try log.boost(foe_ident, .SpecialDefense, -1);
|
|
2510
2541
|
},
|
|
2511
2542
|
.AccuracyDown1 => {
|
|
2512
2543
|
assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
|
|
2513
|
-
if (boosts.accuracy == -6) return try log.fail(foe_ident, .None);
|
|
2514
|
-
boosts.accuracy = @intCast(
|
|
2544
|
+
if (boosts.accuracy == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2545
|
+
boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
|
|
2515
2546
|
try log.boost(foe_ident, .Accuracy, -1);
|
|
2516
2547
|
},
|
|
2517
2548
|
else => unreachable,
|
|
@@ -2522,8 +2553,7 @@ pub const Effects = struct {
|
|
|
2522
2553
|
}
|
|
2523
2554
|
};
|
|
2524
2555
|
|
|
2525
|
-
fn unmodifiedStats(battle: anytype,
|
|
2526
|
-
const side = battle.side(who);
|
|
2556
|
+
fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
|
|
2527
2557
|
if (!side.active.volatiles.Transform) return &side.stored().stats;
|
|
2528
2558
|
const id = ID.from(side.active.volatiles.transform);
|
|
2529
2559
|
return &battle.side(id.player).pokemon[id.id - 1].stats;
|
|
@@ -2589,81 +2619,146 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
|
|
|
2589
2619
|
}
|
|
2590
2620
|
}
|
|
2591
2621
|
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2622
|
+
pub const Rolls = struct {
|
|
2623
|
+
inline fn speedTie(battle: anytype) bool {
|
|
2624
|
+
if (showdown) return battle.rng.range(u8, 0, 2) == 0;
|
|
2625
|
+
return battle.rng.next() < Gen12.percent(50) + 1;
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2628
|
+
inline fn criticalHit(battle: anytype, chance: u16) bool {
|
|
2629
|
+
if (showdown) return battle.rng.chance(u8, @as(u8, @intCast(chance)), 256);
|
|
2630
|
+
return std.math.rotl(u8, battle.rng.next(), 3) < chance;
|
|
2631
|
+
}
|
|
2632
|
+
|
|
2633
|
+
inline fn damage(battle: anytype) u8 {
|
|
2634
|
+
if (showdown) return battle.rng.range(u8, 217, 256);
|
|
2635
|
+
while (true) {
|
|
2636
|
+
const r = std.math.rotr(u8, battle.rng.next(), 1);
|
|
2637
|
+
if (r >= 217) return r;
|
|
2600
2638
|
}
|
|
2601
2639
|
}
|
|
2602
|
-
return false;
|
|
2603
|
-
}
|
|
2604
2640
|
|
|
2605
|
-
inline fn
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
try Effects.boost(battle, who, Move.get(.Rage), log);
|
|
2641
|
+
inline fn hit(battle: anytype, accuracy: u16) bool {
|
|
2642
|
+
if (showdown) return battle.rng.chance(u8, @as(u8, @intCast(accuracy)), 256);
|
|
2643
|
+
return battle.rng.next() < accuracy;
|
|
2609
2644
|
}
|
|
2610
|
-
}
|
|
2611
2645
|
|
|
2612
|
-
fn
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2646
|
+
inline fn confusionDuration(battle: anytype) u3 {
|
|
2647
|
+
if (showdown) return @intCast(battle.rng.range(u8, 2, 6));
|
|
2648
|
+
return @intCast((battle.rng.next() & 3) + 2);
|
|
2649
|
+
}
|
|
2616
2650
|
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
else
|
|
2622
|
-
(battle.rng.next() & 3) + 2);
|
|
2651
|
+
inline fn confused(battle: anytype) bool {
|
|
2652
|
+
if (showdown) return !battle.rng.chance(u8, 128, 256);
|
|
2653
|
+
return battle.rng.next() >= Gen12.percent(50) + 1;
|
|
2654
|
+
}
|
|
2623
2655
|
|
|
2624
|
-
|
|
2625
|
-
|
|
2656
|
+
inline fn paralyzed(battle: anytype) bool {
|
|
2657
|
+
if (showdown) return battle.rng.chance(u8, 63, 256);
|
|
2658
|
+
return battle.rng.next() < Gen12.percent(25);
|
|
2659
|
+
}
|
|
2626
2660
|
|
|
2627
|
-
|
|
2661
|
+
inline fn confusionChance(battle: anytype) bool {
|
|
2662
|
+
if (showdown) return battle.rng.chance(u8, 25, 256);
|
|
2663
|
+
return battle.rng.next() < Gen12.percent(10);
|
|
2664
|
+
}
|
|
2628
2665
|
|
|
2629
|
-
fn
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
}
|
|
2666
|
+
inline fn secondaryChance(battle: anytype, low: bool) bool {
|
|
2667
|
+
if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 26 else 77), 256);
|
|
2668
|
+
return battle.rng.next() < 1 + (if (low) Gen12.percent(10) else Gen12.percent(30));
|
|
2669
|
+
}
|
|
2634
2670
|
|
|
2635
|
-
fn
|
|
2636
|
-
|
|
2637
|
-
if (
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2671
|
+
inline fn poisonChance(battle: anytype, low: bool) bool {
|
|
2672
|
+
if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 52 else 103), 256);
|
|
2673
|
+
return battle.rng.next() < 1 + (if (low) Gen12.percent(20) else Gen12.percent(40));
|
|
2674
|
+
}
|
|
2675
|
+
|
|
2676
|
+
inline fn sleepDuration(battle: anytype) u3 {
|
|
2677
|
+
if (showdown) return @intCast(battle.rng.range(u8, 1, 8));
|
|
2678
|
+
while (true) {
|
|
2679
|
+
const r = battle.rng.next() & 7;
|
|
2680
|
+
if (r != 0) return @intCast(r);
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
inline fn bideThrashDuration(battle: anytype) u3 {
|
|
2685
|
+
if (showdown) return @intCast(battle.rng.range(u4, 2, 4));
|
|
2686
|
+
return @intCast((battle.rng.next() & 1) + 2);
|
|
2687
|
+
}
|
|
2688
|
+
|
|
2689
|
+
fn moveSlot(battle: anytype, moves: []MoveSlot, check_pp: u4) u4 {
|
|
2690
|
+
if (showdown) {
|
|
2691
|
+
if (check_pp == 0) {
|
|
2692
|
+
var i: usize = moves.len;
|
|
2693
|
+
while (i > 0) {
|
|
2694
|
+
i -= 1;
|
|
2695
|
+
if (moves[i].id != .None) {
|
|
2696
|
+
return battle.rng.range(u4, 0, @as(u4, @intCast(i + 1))) + 1;
|
|
2697
|
+
}
|
|
2650
2698
|
}
|
|
2699
|
+
} else {
|
|
2700
|
+
var r = battle.rng.range(u4, 0, check_pp) + 1;
|
|
2701
|
+
var i: usize = 0;
|
|
2702
|
+
while (i < moves.len and r > 0) : (i += 1) {
|
|
2703
|
+
if (moves[i].pp > 0) {
|
|
2704
|
+
r -= 1;
|
|
2705
|
+
if (r == 0) break;
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
return @intCast(i + 1);
|
|
2651
2709
|
}
|
|
2652
|
-
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
while (true) {
|
|
2713
|
+
const r: u4 = @intCast(battle.rng.next() & 3);
|
|
2714
|
+
if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
|
|
2653
2715
|
}
|
|
2654
2716
|
}
|
|
2655
2717
|
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2718
|
+
inline fn disableDuration(battle: anytype) u4 {
|
|
2719
|
+
if (showdown) return @intCast(battle.rng.range(u8, 1, 9));
|
|
2720
|
+
return @intCast((battle.rng.next() & 7) + 1);
|
|
2659
2721
|
}
|
|
2660
|
-
|
|
2722
|
+
|
|
2723
|
+
inline fn metronome(battle: anytype) Move {
|
|
2724
|
+
if (showdown) {
|
|
2725
|
+
const r = battle.rng.range(u8, 0, @intFromEnum(Move.Struggle) - 2);
|
|
2726
|
+
const mod = @as(u2, (if (r < @intFromEnum(Move.Metronome) - 1) 1 else 2));
|
|
2727
|
+
return @enumFromInt(r + mod);
|
|
2728
|
+
}
|
|
2729
|
+
while (true) {
|
|
2730
|
+
const r = battle.rng.next();
|
|
2731
|
+
if (r == 0 or r == @intFromEnum(Move.Metronome)) continue;
|
|
2732
|
+
if (r >= @intFromEnum(Move.Struggle)) continue;
|
|
2733
|
+
return @enumFromInt(r);
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
|
|
2737
|
+
inline fn psywave(battle: anytype, max: u8) u8 {
|
|
2738
|
+
if (showdown) return battle.rng.range(u8, 0, max);
|
|
2739
|
+
while (true) {
|
|
2740
|
+
const r = battle.rng.next();
|
|
2741
|
+
if (r < max) return r;
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
|
|
2745
|
+
inline fn distribution(battle: anytype) u3 {
|
|
2746
|
+
if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
|
|
2747
|
+
const r = (battle.rng.next() & 3);
|
|
2748
|
+
return @intCast((if (r < 2) r else battle.rng.next() & 3) + 2);
|
|
2749
|
+
}
|
|
2750
|
+
|
|
2751
|
+
inline fn unboost(battle: anytype) bool {
|
|
2752
|
+
if (showdown) return battle.rng.chance(u8, 85, 256);
|
|
2753
|
+
return battle.rng.next() < Gen12.percent(33) + 1;
|
|
2754
|
+
}
|
|
2755
|
+
};
|
|
2661
2756
|
|
|
2662
2757
|
test "RNG agreement" {
|
|
2663
2758
|
if (!showdown) return;
|
|
2664
2759
|
var expected: [256]u32 = undefined;
|
|
2665
2760
|
for (0..expected.len) |i| {
|
|
2666
|
-
expected[i] = @intCast(
|
|
2761
|
+
expected[i] = @intCast(i * 0x1000000);
|
|
2667
2762
|
}
|
|
2668
2763
|
|
|
2669
2764
|
var spe = rng.FixedRNG(1, expected.len){ .rolls = expected };
|
|
@@ -2720,7 +2815,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
2720
2815
|
// strictly correct as the player would not have been presented the option to move or
|
|
2721
2816
|
// switch at all.
|
|
2722
2817
|
if (isForced(active)) {
|
|
2723
|
-
out[n] = .{ .type = .Move, .data = @
|
|
2818
|
+
out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
|
|
2724
2819
|
n += 1;
|
|
2725
2820
|
return n;
|
|
2726
2821
|
}
|
|
@@ -2757,7 +2852,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
2757
2852
|
while (slot <= 4) : (slot += 1) {
|
|
2758
2853
|
const m = active.moves[slot - 1];
|
|
2759
2854
|
if (m.id == .None) break;
|
|
2760
|
-
if (m.id == side.last_selected_move) {
|
|
2855
|
+
if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
|
|
2761
2856
|
// Pokémon Showdown displays Struggle if limited to Bide but unable to pick
|
|
2762
2857
|
const struggle =
|
|
2763
2858
|
m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);
|