@pkmn/engine 0.1.0-dev.84cc1a02 → 0.1.0-dev.942dbfa4
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 +7 -7
- package/src/lib/binding/wasm.zig +3 -3
- package/src/lib/common/data.zig +1 -1
- package/src/lib/common/debug.zig +1 -0
- package/src/lib/common/options.zig +3 -3
- package/src/lib/common/protocol.zig +140 -133
- 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 +2 -2
- package/src/lib/gen1/data.zig +32 -28
- package/src/lib/gen1/helpers.zig +6 -17
- package/src/lib/gen1/mechanics.zig +418 -325
- 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 +12 -12
- package/src/lib/gen2/helpers.zig +3 -3
- 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,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;
|
|
@@ -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,
|
|
@@ -383,9 +424,10 @@ fn executeMove(
|
|
|
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);
|
|
@@ -404,9 +446,9 @@ fn executeMove(
|
|
|
404
446
|
// choice.data == 0 only happens with Struggle on Pokémon Showdown
|
|
405
447
|
assert(!showdown);
|
|
406
448
|
mslot = @intCast(u4, if (player == .P1)
|
|
407
|
-
battle.
|
|
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
|
|
@@ -428,14 +470,14 @@ fn executeMove(
|
|
|
428
470
|
// Incorrect mslot with Pokémon Showdown choice semantics so we need to recover from index
|
|
429
471
|
assert(mslot == 1);
|
|
430
472
|
mslot = @intCast(u4, if (player == .P1)
|
|
431
|
-
battle.
|
|
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
|
|
|
@@ -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(
|
|
@@ -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
|
-
const random = if (showdown)
|
|
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
|
-
|
|
1164
|
+
const random = Rolls.damage(battle);
|
|
1098
1165
|
battle.last_damage = @intCast(u16, @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
1181
|
const max = @intCast(u8, @as(u16, side.stored().level) * 3 / 2);
|
|
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
|
-
}
|
|
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
|
|
|
@@ -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
|
-
|
|
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));
|
|
1328
1382
|
var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
|
|
1329
1383
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1330
1384
|
boost = BOOSTS[@intCast(u4, @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;
|
|
@@ -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(u3,
|
|
1789
|
-
volatiles.disabled_duration =
|
|
1790
|
-
battle.rng.range(u8, 1, 9)
|
|
1791
|
-
else
|
|
1792
|
-
(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);
|
|
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 = @enumFromInt(Player, 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);
|
|
@@ -2004,15 +2059,15 @@ pub const Effects = struct {
|
|
|
2004
2059
|
}
|
|
2005
2060
|
break :has_mimic false;
|
|
2006
2061
|
};
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
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;
|
|
2010
2065
|
return try log.fail(battle.active(player.foe()), .None);
|
|
2011
2066
|
}
|
|
2012
2067
|
}
|
|
2013
2068
|
if (!try checkHit(battle, player, move, log)) return;
|
|
2014
2069
|
|
|
2015
|
-
const rslot =
|
|
2070
|
+
const rslot = Rolls.moveSlot(battle, &foe.active.moves, 0);
|
|
2016
2071
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
2017
2072
|
|
|
2018
2073
|
try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
|
|
@@ -2033,7 +2088,8 @@ pub const Effects = struct {
|
|
|
2033
2088
|
assert(!side.active.volatiles.MultiHit);
|
|
2034
2089
|
side.active.volatiles.MultiHit = true;
|
|
2035
2090
|
|
|
2036
|
-
side.active.volatiles.attacks =
|
|
2091
|
+
side.active.volatiles.attacks =
|
|
2092
|
+
if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
|
|
2037
2093
|
}
|
|
2038
2094
|
|
|
2039
2095
|
fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2077,14 +2133,8 @@ pub const Effects = struct {
|
|
|
2077
2133
|
}
|
|
2078
2134
|
|
|
2079
2135
|
// 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;
|
|
2136
|
+
if (foe.active.types.includes(move.type)) return;
|
|
2137
|
+
if (!Rolls.secondaryChance(battle, move.effect == .ParalyzeChance1)) return;
|
|
2088
2138
|
|
|
2089
2139
|
foe_stored.status = Status.init(.PAR);
|
|
2090
2140
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
@@ -2127,16 +2177,7 @@ pub const Effects = struct {
|
|
|
2127
2177
|
|
|
2128
2178
|
if (move.effect == .Poison) {
|
|
2129
2179
|
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
|
-
}
|
|
2180
|
+
} else if (!Rolls.poisonChance(battle, move.effect == .PoisonChance1)) return;
|
|
2140
2181
|
|
|
2141
2182
|
foe_stored.status = Status.init(.PSN);
|
|
2142
2183
|
if (toxic) {
|
|
@@ -2150,16 +2191,17 @@ pub const Effects = struct {
|
|
|
2150
2191
|
|
|
2151
2192
|
fn rage(battle: anytype, player: Player) !void {
|
|
2152
2193
|
var volatiles = &battle.side(player).active.volatiles;
|
|
2153
|
-
if (volatiles.Rage) return;
|
|
2154
|
-
volatiles.Rage = true;
|
|
2155
2194
|
assert(!volatiles.Bide);
|
|
2156
|
-
volatiles.
|
|
2195
|
+
volatiles.Rage = true;
|
|
2157
2196
|
}
|
|
2158
2197
|
|
|
2159
2198
|
fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
|
|
2160
2199
|
var side = battle.side(player);
|
|
2161
2200
|
var stored = side.stored();
|
|
2162
2201
|
|
|
2202
|
+
assert(showdown or battle.last_damage > 0);
|
|
2203
|
+
if (showdown and battle.last_damage == 0) return;
|
|
2204
|
+
|
|
2163
2205
|
const damage = @intCast(i16, @max(battle.last_damage /
|
|
2164
2206
|
@as(u8, if (side.last_selected_move == .Struggle) 2 else 4), 1));
|
|
2165
2207
|
stored.hp = @intCast(u16, @max(@intCast(i16, stored.hp) - damage, 0));
|
|
@@ -2205,16 +2247,7 @@ pub const Effects = struct {
|
|
|
2205
2247
|
}
|
|
2206
2248
|
foe.active.volatiles.Recharging = false;
|
|
2207
2249
|
|
|
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);
|
|
2250
|
+
foe_stored.status = Status.slp(Rolls.sleepDuration(battle));
|
|
2218
2251
|
try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
|
|
2219
2252
|
}
|
|
2220
2253
|
|
|
@@ -2267,11 +2300,7 @@ pub const Effects = struct {
|
|
|
2267
2300
|
assert(!volatiles.Bide);
|
|
2268
2301
|
|
|
2269
2302
|
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);
|
|
2303
|
+
volatiles.attacks = Rolls.bideThrashDuration(battle);
|
|
2275
2304
|
}
|
|
2276
2305
|
|
|
2277
2306
|
fn transform(battle: anytype, player: Player, log: anytype) !void {
|
|
@@ -2303,7 +2332,7 @@ pub const Effects = struct {
|
|
|
2303
2332
|
try log.transform(battle.active(player), foe_ident);
|
|
2304
2333
|
}
|
|
2305
2334
|
|
|
2306
|
-
fn binding(battle: anytype, player: Player) void {
|
|
2335
|
+
fn binding(battle: anytype, player: Player, rewrap: bool) void {
|
|
2307
2336
|
var side = battle.side(player);
|
|
2308
2337
|
var foe = battle.foe(player);
|
|
2309
2338
|
|
|
@@ -2311,9 +2340,11 @@ pub const Effects = struct {
|
|
|
2311
2340
|
side.active.volatiles.Binding = true;
|
|
2312
2341
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2313
2342
|
// (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
|
|
2314
|
-
if (!showdown)
|
|
2343
|
+
if (!showdown) {
|
|
2344
|
+
foe.active.volatiles.Recharging = false;
|
|
2345
|
+
} else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
|
|
2315
2346
|
|
|
2316
|
-
side.active.volatiles.attacks = distribution(battle) - 1;
|
|
2347
|
+
side.active.volatiles.attacks = Rolls.distribution(battle) - 1;
|
|
2317
2348
|
}
|
|
2318
2349
|
|
|
2319
2350
|
fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2340,9 +2371,11 @@ pub const Effects = struct {
|
|
|
2340
2371
|
return try log.fail(ident, .None);
|
|
2341
2372
|
}
|
|
2342
2373
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2343
|
-
const stat = unmodifiedStats(battle,
|
|
2374
|
+
const stat = unmodifiedStats(battle, side).atk;
|
|
2344
2375
|
stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2345
2376
|
try log.boost(ident, reason, n);
|
|
2377
|
+
// Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
|
|
2378
|
+
if (showdown and move.effect == .Rage) return;
|
|
2346
2379
|
},
|
|
2347
2380
|
.DefenseUp1, .DefenseUp2 => {
|
|
2348
2381
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
@@ -2358,7 +2391,7 @@ pub const Effects = struct {
|
|
|
2358
2391
|
return try log.fail(ident, .None);
|
|
2359
2392
|
}
|
|
2360
2393
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2361
|
-
const stat = unmodifiedStats(battle,
|
|
2394
|
+
const stat = unmodifiedStats(battle, side).def;
|
|
2362
2395
|
stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2363
2396
|
try log.boost(ident, .Defense, n);
|
|
2364
2397
|
},
|
|
@@ -2375,7 +2408,7 @@ pub const Effects = struct {
|
|
|
2375
2408
|
return try log.fail(ident, .None);
|
|
2376
2409
|
}
|
|
2377
2410
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2378
|
-
const stat = unmodifiedStats(battle,
|
|
2411
|
+
const stat = unmodifiedStats(battle, side).spe;
|
|
2379
2412
|
stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2380
2413
|
try log.boost(ident, .Speed, 2);
|
|
2381
2414
|
},
|
|
@@ -2395,7 +2428,7 @@ pub const Effects = struct {
|
|
|
2395
2428
|
return try log.fail(ident, .None);
|
|
2396
2429
|
}
|
|
2397
2430
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2398
|
-
const stat = unmodifiedStats(battle,
|
|
2431
|
+
const stat = unmodifiedStats(battle, side).spc;
|
|
2399
2432
|
stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2400
2433
|
try log.boost(ident, .SpecialAttack, n);
|
|
2401
2434
|
try log.boost(ident, .SpecialDefense, n);
|
|
@@ -2416,28 +2449,24 @@ pub const Effects = struct {
|
|
|
2416
2449
|
fn unboost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
2417
2450
|
var foe = battle.foe(player);
|
|
2418
2451
|
const foe_ident = battle.active(player.foe());
|
|
2452
|
+
const secondary = move.effect.isStatDownChance();
|
|
2419
2453
|
|
|
2420
|
-
if (foe.active.volatiles.Substitute)
|
|
2421
|
-
return if (!move.effect.isStatDownChance()) log.fail(foe_ident, .None);
|
|
2422
|
-
}
|
|
2454
|
+
if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(foe_ident, .None);
|
|
2423
2455
|
|
|
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;
|
|
2456
|
+
if (secondary) {
|
|
2457
|
+
if (!Rolls.unboost(battle) or foe.active.volatiles.Invulnerable) return;
|
|
2430
2458
|
} else if (!showdown and !try checkHit(battle, player, move, log)) {
|
|
2431
2459
|
return; // checkHit already checks for Invulnerable
|
|
2432
2460
|
}
|
|
2433
2461
|
|
|
2434
2462
|
var stats = &foe.active.stats;
|
|
2435
2463
|
var boosts = &foe.active.boosts;
|
|
2464
|
+
const fail = showdown or !secondary;
|
|
2436
2465
|
|
|
2437
2466
|
switch (move.effect) {
|
|
2438
2467
|
.AttackDown1, .AttackDownChance => {
|
|
2439
2468
|
assert(boosts.atk >= -6 and boosts.atk <= 6);
|
|
2440
|
-
if (boosts.atk == -6) return try log.fail(foe_ident, .None);
|
|
2469
|
+
if (boosts.atk == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2441
2470
|
boosts.atk = @intCast(i4, @max(-6, @as(i8, boosts.atk) - 1));
|
|
2442
2471
|
if (stats.atk == 1) {
|
|
2443
2472
|
boosts.atk += 1;
|
|
@@ -2448,13 +2477,13 @@ pub const Effects = struct {
|
|
|
2448
2477
|
return try log.fail(foe_ident, .None);
|
|
2449
2478
|
}
|
|
2450
2479
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2451
|
-
const stat = unmodifiedStats(battle,
|
|
2480
|
+
const stat = unmodifiedStats(battle, foe).atk;
|
|
2452
2481
|
stats.atk = @max(1, stat * mod[0] / mod[1]);
|
|
2453
2482
|
try log.boost(foe_ident, .Attack, -1);
|
|
2454
2483
|
},
|
|
2455
2484
|
.DefenseDown1, .DefenseDown2, .DefenseDownChance => {
|
|
2456
2485
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
2457
|
-
if (boosts.def == -6) return try log.fail(foe_ident, .None);
|
|
2486
|
+
if (boosts.def == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2458
2487
|
const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
|
|
2459
2488
|
boosts.def = @intCast(i4, @max(-6, @as(i8, boosts.def) - n));
|
|
2460
2489
|
if (stats.def == 1) {
|
|
@@ -2466,13 +2495,13 @@ pub const Effects = struct {
|
|
|
2466
2495
|
return try log.fail(foe_ident, .None);
|
|
2467
2496
|
}
|
|
2468
2497
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2469
|
-
const stat = unmodifiedStats(battle,
|
|
2498
|
+
const stat = unmodifiedStats(battle, foe).def;
|
|
2470
2499
|
stats.def = @max(1, stat * mod[0] / mod[1]);
|
|
2471
2500
|
try log.boost(foe_ident, .Defense, -@as(i8, n));
|
|
2472
2501
|
},
|
|
2473
2502
|
.SpeedDown1, .SpeedDownChance => {
|
|
2474
2503
|
assert(boosts.spe >= -6 and boosts.spe <= 6);
|
|
2475
|
-
if (boosts.spe == -6) return try log.fail(foe_ident, .None);
|
|
2504
|
+
if (boosts.spe == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2476
2505
|
boosts.spe = @intCast(i4, @max(-6, @as(i8, boosts.spe) - 1));
|
|
2477
2506
|
if (stats.spe == 1) {
|
|
2478
2507
|
boosts.spe += 1;
|
|
@@ -2483,14 +2512,14 @@ pub const Effects = struct {
|
|
|
2483
2512
|
return try log.fail(foe_ident, .None);
|
|
2484
2513
|
}
|
|
2485
2514
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2486
|
-
const stat = unmodifiedStats(battle,
|
|
2515
|
+
const stat = unmodifiedStats(battle, foe).spe;
|
|
2487
2516
|
stats.spe = @max(1, stat * mod[0] / mod[1]);
|
|
2488
2517
|
try log.boost(foe_ident, .Speed, -1);
|
|
2489
2518
|
assert(boosts.spe >= -6);
|
|
2490
2519
|
},
|
|
2491
2520
|
.SpecialDownChance => {
|
|
2492
2521
|
assert(boosts.spc >= -6 and boosts.spc <= 6);
|
|
2493
|
-
if (boosts.spc == -6) return try log.fail(foe_ident, .None);
|
|
2522
|
+
if (boosts.spc == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2494
2523
|
boosts.spc = @intCast(i4, @max(-6, @as(i8, boosts.spc) - 1));
|
|
2495
2524
|
if (stats.spc == 1) {
|
|
2496
2525
|
boosts.spc += 1;
|
|
@@ -2503,14 +2532,14 @@ pub const Effects = struct {
|
|
|
2503
2532
|
return try log.fail(foe_ident, .None);
|
|
2504
2533
|
}
|
|
2505
2534
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2506
|
-
const stat = unmodifiedStats(battle,
|
|
2535
|
+
const stat = unmodifiedStats(battle, foe).spc;
|
|
2507
2536
|
stats.spc = @max(1, stat * mod[0] / mod[1]);
|
|
2508
2537
|
try log.boost(foe_ident, .SpecialAttack, -1);
|
|
2509
2538
|
try log.boost(foe_ident, .SpecialDefense, -1);
|
|
2510
2539
|
},
|
|
2511
2540
|
.AccuracyDown1 => {
|
|
2512
2541
|
assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
|
|
2513
|
-
if (boosts.accuracy == -6) return try log.fail(foe_ident, .None);
|
|
2542
|
+
if (boosts.accuracy == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2514
2543
|
boosts.accuracy = @intCast(i4, @max(-6, @as(i8, boosts.accuracy) - 1));
|
|
2515
2544
|
try log.boost(foe_ident, .Accuracy, -1);
|
|
2516
2545
|
},
|
|
@@ -2522,8 +2551,7 @@ pub const Effects = struct {
|
|
|
2522
2551
|
}
|
|
2523
2552
|
};
|
|
2524
2553
|
|
|
2525
|
-
fn unmodifiedStats(battle: anytype,
|
|
2526
|
-
const side = battle.side(who);
|
|
2554
|
+
fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
|
|
2527
2555
|
if (!side.active.volatiles.Transform) return &side.stored().stats;
|
|
2528
2556
|
const id = ID.from(side.active.volatiles.transform);
|
|
2529
2557
|
return &battle.side(id.player).pokemon[id.id - 1].stats;
|
|
@@ -2589,75 +2617,140 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
|
|
|
2589
2617
|
}
|
|
2590
2618
|
}
|
|
2591
2619
|
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2620
|
+
pub const Rolls = struct {
|
|
2621
|
+
inline fn speedTie(battle: anytype) bool {
|
|
2622
|
+
if (showdown) return battle.rng.range(u8, 0, 2) == 0;
|
|
2623
|
+
return battle.rng.next() < Gen12.percent(50) + 1;
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2626
|
+
inline fn criticalHit(battle: anytype, chance: u16) bool {
|
|
2627
|
+
if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
|
|
2628
|
+
return std.math.rotl(u8, battle.rng.next(), 3) < chance;
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2631
|
+
inline fn damage(battle: anytype) u8 {
|
|
2632
|
+
if (showdown) return battle.rng.range(u8, 217, 256);
|
|
2633
|
+
while (true) {
|
|
2634
|
+
const r = std.math.rotr(u8, battle.rng.next(), 1);
|
|
2635
|
+
if (r >= 217) return r;
|
|
2600
2636
|
}
|
|
2601
2637
|
}
|
|
2602
|
-
return false;
|
|
2603
|
-
}
|
|
2604
2638
|
|
|
2605
|
-
inline fn
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
try Effects.boost(battle, who, Move.get(.Rage), log);
|
|
2639
|
+
inline fn hit(battle: anytype, accuracy: u16) bool {
|
|
2640
|
+
if (showdown) return battle.rng.chance(u8, @intCast(u8, accuracy), 256);
|
|
2641
|
+
return battle.rng.next() < accuracy;
|
|
2609
2642
|
}
|
|
2610
|
-
}
|
|
2611
2643
|
|
|
2612
|
-
fn
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2644
|
+
inline fn confusionDuration(battle: anytype) u3 {
|
|
2645
|
+
if (showdown) return @intCast(u3, battle.rng.range(u8, 2, 6));
|
|
2646
|
+
return @intCast(u3, (battle.rng.next() & 3) + 2);
|
|
2647
|
+
}
|
|
2616
2648
|
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
else
|
|
2622
|
-
(battle.rng.next() & 3) + 2);
|
|
2649
|
+
inline fn confused(battle: anytype) bool {
|
|
2650
|
+
if (showdown) return !battle.rng.chance(u8, 128, 256);
|
|
2651
|
+
return battle.rng.next() >= Gen12.percent(50) + 1;
|
|
2652
|
+
}
|
|
2623
2653
|
|
|
2624
|
-
|
|
2625
|
-
|
|
2654
|
+
inline fn paralyzed(battle: anytype) bool {
|
|
2655
|
+
if (showdown) return battle.rng.chance(u8, 63, 256);
|
|
2656
|
+
return battle.rng.next() < Gen12.percent(25);
|
|
2657
|
+
}
|
|
2626
2658
|
|
|
2627
|
-
|
|
2659
|
+
inline fn confusionChance(battle: anytype) bool {
|
|
2660
|
+
if (showdown) return battle.rng.chance(u8, 25, 256);
|
|
2661
|
+
return battle.rng.next() < Gen12.percent(10);
|
|
2662
|
+
}
|
|
2628
2663
|
|
|
2629
|
-
fn
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
}
|
|
2664
|
+
inline fn secondaryChance(battle: anytype, low: bool) bool {
|
|
2665
|
+
if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 26 else 77), 256);
|
|
2666
|
+
return battle.rng.next() < 1 + (if (low) Gen12.percent(10) else Gen12.percent(30));
|
|
2667
|
+
}
|
|
2634
2668
|
|
|
2635
|
-
fn
|
|
2636
|
-
|
|
2637
|
-
if (
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2669
|
+
inline fn poisonChance(battle: anytype, low: bool) bool {
|
|
2670
|
+
if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 52 else 103), 256);
|
|
2671
|
+
return battle.rng.next() < 1 + (if (low) Gen12.percent(20) else Gen12.percent(40));
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
inline fn sleepDuration(battle: anytype) u3 {
|
|
2675
|
+
if (showdown) return @intCast(u3, battle.rng.range(u8, 1, 8));
|
|
2676
|
+
while (true) {
|
|
2677
|
+
const r = battle.rng.next() & 7;
|
|
2678
|
+
if (r != 0) return @intCast(u3, r);
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
|
|
2682
|
+
inline fn bideThrashDuration(battle: anytype) u3 {
|
|
2683
|
+
if (showdown) return @intCast(u3, battle.rng.range(u4, 2, 4));
|
|
2684
|
+
return @intCast(u3, (battle.rng.next() & 1) + 2);
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
fn moveSlot(battle: anytype, moves: []MoveSlot, check_pp: u4) u4 {
|
|
2688
|
+
if (showdown) {
|
|
2689
|
+
if (check_pp == 0) {
|
|
2690
|
+
var i: usize = moves.len;
|
|
2691
|
+
while (i > 0) {
|
|
2692
|
+
i -= 1;
|
|
2693
|
+
if (moves[i].id != .None) {
|
|
2694
|
+
return battle.rng.range(u4, 0, @intCast(u4, i + 1)) + 1;
|
|
2695
|
+
}
|
|
2650
2696
|
}
|
|
2697
|
+
} else {
|
|
2698
|
+
var r = battle.rng.range(u4, 0, @intCast(u4, check_pp)) + 1;
|
|
2699
|
+
var i: usize = 0;
|
|
2700
|
+
while (i < moves.len and r > 0) : (i += 1) {
|
|
2701
|
+
if (moves[i].pp > 0) {
|
|
2702
|
+
r -= 1;
|
|
2703
|
+
if (r == 0) break;
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
return @intCast(u4, i + 1);
|
|
2651
2707
|
}
|
|
2652
|
-
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2710
|
+
while (true) {
|
|
2711
|
+
const r = @intCast(u4, battle.rng.next() & 3);
|
|
2712
|
+
if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
|
|
2653
2713
|
}
|
|
2654
2714
|
}
|
|
2655
2715
|
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2716
|
+
inline fn disableDuration(battle: anytype) u4 {
|
|
2717
|
+
if (showdown) return @intCast(u4, battle.rng.range(u8, 1, 9));
|
|
2718
|
+
return @intCast(u4, (battle.rng.next() & 7) + 1);
|
|
2659
2719
|
}
|
|
2660
|
-
|
|
2720
|
+
|
|
2721
|
+
inline fn metronome(battle: anytype) Move {
|
|
2722
|
+
if (showdown) {
|
|
2723
|
+
const r = battle.rng.range(u8, 0, @intFromEnum(Move.Struggle) - 2);
|
|
2724
|
+
const mod = @as(u2, (if (r < @intFromEnum(Move.Metronome) - 1) 1 else 2));
|
|
2725
|
+
return @enumFromInt(Move, r + mod);
|
|
2726
|
+
}
|
|
2727
|
+
while (true) {
|
|
2728
|
+
const r = battle.rng.next();
|
|
2729
|
+
if (r == 0 or r == @intFromEnum(Move.Metronome)) continue;
|
|
2730
|
+
if (r >= @intFromEnum(Move.Struggle)) continue;
|
|
2731
|
+
return @enumFromInt(Move, r);
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
|
|
2735
|
+
inline fn psywave(battle: anytype, max: u8) u8 {
|
|
2736
|
+
if (showdown) return battle.rng.range(u8, 0, max);
|
|
2737
|
+
while (true) {
|
|
2738
|
+
const r = battle.rng.next();
|
|
2739
|
+
if (r < max) return r;
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
|
|
2743
|
+
inline fn distribution(battle: anytype) u3 {
|
|
2744
|
+
if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
|
|
2745
|
+
const r = (battle.rng.next() & 3);
|
|
2746
|
+
return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
|
|
2747
|
+
}
|
|
2748
|
+
|
|
2749
|
+
inline fn unboost(battle: anytype) bool {
|
|
2750
|
+
if (showdown) return battle.rng.chance(u8, 85, 256);
|
|
2751
|
+
return battle.rng.next() < Gen12.percent(33) + 1;
|
|
2752
|
+
}
|
|
2753
|
+
};
|
|
2661
2754
|
|
|
2662
2755
|
test "RNG agreement" {
|
|
2663
2756
|
if (!showdown) return;
|
|
@@ -2720,7 +2813,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
2720
2813
|
// strictly correct as the player would not have been presented the option to move or
|
|
2721
2814
|
// switch at all.
|
|
2722
2815
|
if (isForced(active)) {
|
|
2723
|
-
out[n] = .{ .type = .Move, .data = @
|
|
2816
|
+
out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
|
|
2724
2817
|
n += 1;
|
|
2725
2818
|
return n;
|
|
2726
2819
|
}
|
|
@@ -2757,7 +2850,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
2757
2850
|
while (slot <= 4) : (slot += 1) {
|
|
2758
2851
|
const m = active.moves[slot - 1];
|
|
2759
2852
|
if (m.id == .None) break;
|
|
2760
|
-
if (m.id == side.last_selected_move) {
|
|
2853
|
+
if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
|
|
2761
2854
|
// Pokémon Showdown displays Struggle if limited to Bide but unable to pick
|
|
2762
2855
|
const struggle =
|
|
2763
2856
|
m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);
|