@pkmn/engine 0.1.0-dev.c4dec0f7 → 0.1.0-dev.d716e940
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 +50 -22
- 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 +24 -34
- package/package.json +40 -29
- package/src/bin/install-pkmn-engine +60 -33
- package/src/bin/pkmn-debug +62 -0
- package/src/lib/binding/node.zig +5 -5
- package/src/lib/binding/wasm.zig +3 -3
- package/src/lib/common/debug.zig +1 -0
- package/src/lib/common/options.zig +3 -3
- package/src/lib/common/protocol.zig +38 -38
- package/src/lib/gen1/README.md +119 -85
- package/src/lib/gen1/data.zig +19 -17
- package/src/lib/gen1/helpers.zig +3 -14
- package/src/lib/gen1/mechanics.zig +431 -313
- package/src/lib/gen2/data.zig +4 -4
- package/src/lib/pkmn.zig +7 -7
- package/src/test/display/pkmn.html.tmpl +368 -0
|
@@ -56,30 +56,34 @@ const BOOSTS = &[_][2]u8{
|
|
|
56
56
|
|
|
57
57
|
const MAX_STAT_VALUE = 999;
|
|
58
58
|
|
|
59
|
+
const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
|
|
60
|
+
|
|
59
61
|
pub fn update(battle: anytype, c1: Choice, c2: Choice, log: anytype) !Result {
|
|
60
62
|
assert(c1.type != .Pass or c2.type != .Pass or battle.turn == 0);
|
|
61
63
|
if (battle.turn == 0) return start(battle, log);
|
|
62
64
|
|
|
65
|
+
var s1 = false;
|
|
66
|
+
var s2 = false;
|
|
63
67
|
var f1: ?Move = null;
|
|
64
68
|
var f2: ?Move = null;
|
|
65
69
|
|
|
66
|
-
if (selectMove(battle, .P1, c1, c2, &f1)) |r| return r;
|
|
67
|
-
if (selectMove(battle, .P2, c2, c1, &f2)) |r| return r;
|
|
70
|
+
if (selectMove(battle, .P1, c1, c2, &s1, &f1)) |r| return r;
|
|
71
|
+
if (selectMove(battle, .P2, c2, c1, &s2, &f2)) |r| return r;
|
|
72
|
+
|
|
73
|
+
var p1 = battle.side(.P1);
|
|
74
|
+
var p2 = battle.side(.P2);
|
|
75
|
+
|
|
76
|
+
var r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
|
|
77
|
+
var r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
|
|
68
78
|
|
|
69
79
|
if (turnOrder(battle, c1, c2) == .P1) {
|
|
70
|
-
if (try doTurn(battle, .P1, c1, f1, .P2, c2, f2, log)) |r| return r;
|
|
80
|
+
if (try doTurn(battle, .P1, c1, r1, s1, f1, .P2, c2, r2, s2, f2, log)) |r| return r;
|
|
71
81
|
} else {
|
|
72
|
-
if (try doTurn(battle, .P2, c2, f2, .P1, c1, f1, log)) |r| return r;
|
|
82
|
+
if (try doTurn(battle, .P2, c2, r2, s2, f2, .P1, c1, r1, s1, f1, log)) |r| return r;
|
|
73
83
|
}
|
|
74
84
|
|
|
75
|
-
|
|
76
|
-
if (
|
|
77
|
-
p1.active.volatiles.Binding = false;
|
|
78
|
-
}
|
|
79
|
-
var p2 = battle.side(.P2);
|
|
80
|
-
if (p2.active.volatiles.attacks == 0) {
|
|
81
|
-
p2.active.volatiles.Binding = false;
|
|
82
|
-
}
|
|
85
|
+
if (p1.active.volatiles.attacks == 0) p1.active.volatiles.Binding = false;
|
|
86
|
+
if (p2.active.volatiles.attacks == 0) p2.active.volatiles.Binding = false;
|
|
83
87
|
|
|
84
88
|
return endTurn(battle, log);
|
|
85
89
|
}
|
|
@@ -112,6 +116,7 @@ fn selectMove(
|
|
|
112
116
|
player: Player,
|
|
113
117
|
choice: Choice,
|
|
114
118
|
foe_choice: Choice,
|
|
119
|
+
skip_turn: *bool,
|
|
115
120
|
from: *?Move,
|
|
116
121
|
) ?Result {
|
|
117
122
|
if (choice.type == .Pass) return null;
|
|
@@ -124,17 +129,26 @@ fn selectMove(
|
|
|
124
129
|
(choice.type == .Move and choice.data == @boolToInt(showdown)));
|
|
125
130
|
|
|
126
131
|
// pre-battle menu
|
|
127
|
-
if (volatiles.Recharging)
|
|
132
|
+
if (volatiles.Recharging) {
|
|
133
|
+
if (showdown and battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
128
136
|
if (volatiles.Rage) {
|
|
129
137
|
from.* = side.last_used_move;
|
|
130
|
-
if (showdown)
|
|
138
|
+
if (showdown) {
|
|
139
|
+
if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
|
|
140
|
+
saveMove(battle, player, null);
|
|
141
|
+
}
|
|
131
142
|
return null;
|
|
132
143
|
}
|
|
133
144
|
// Pokémon Showdown removes Flinch at the end-of-turn in its residual handler
|
|
134
145
|
if (!showdown) volatiles.Flinch = false;
|
|
135
146
|
if (volatiles.Thrashing or volatiles.Charging) {
|
|
136
147
|
from.* = side.last_selected_move;
|
|
137
|
-
if (showdown)
|
|
148
|
+
if (showdown) {
|
|
149
|
+
if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
|
|
150
|
+
saveMove(battle, player, null);
|
|
151
|
+
}
|
|
138
152
|
return null;
|
|
139
153
|
}
|
|
140
154
|
|
|
@@ -144,7 +158,10 @@ fn selectMove(
|
|
|
144
158
|
// pre-move select
|
|
145
159
|
if (Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP) or volatiles.Bide) {
|
|
146
160
|
assert(showdown or choice.data == 0);
|
|
147
|
-
if (showdown)
|
|
161
|
+
if (showdown) {
|
|
162
|
+
if (volatiles.Bide and battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
|
|
163
|
+
saveMove(battle, player, choice);
|
|
164
|
+
}
|
|
148
165
|
return null;
|
|
149
166
|
}
|
|
150
167
|
if (volatiles.Binding) {
|
|
@@ -163,9 +180,9 @@ fn selectMove(
|
|
|
163
180
|
// GLITCH: https://glitchcity.wiki/Partial_trapping_move_Mirror_Move_link_battle_glitch
|
|
164
181
|
if (foe_choice.type == .Switch) {
|
|
165
182
|
const slot = if (player == .P1)
|
|
166
|
-
battle.
|
|
183
|
+
battle.last_moves.p1_index
|
|
167
184
|
else
|
|
168
|
-
battle.
|
|
185
|
+
battle.last_moves.p2_index;
|
|
169
186
|
const last = side.active.move(@intCast(u4, slot));
|
|
170
187
|
if (last.id == .Metronome) side.last_selected_move = last.id;
|
|
171
188
|
if (last.id == .MirrorMove) return Result.Error;
|
|
@@ -175,6 +192,7 @@ fn selectMove(
|
|
|
175
192
|
}
|
|
176
193
|
|
|
177
194
|
if (battle.foe(player).active.volatiles.Binding) {
|
|
195
|
+
skip_turn.* = true;
|
|
178
196
|
if (showdown) {
|
|
179
197
|
saveMove(battle, player, choice);
|
|
180
198
|
} else {
|
|
@@ -185,6 +203,7 @@ fn selectMove(
|
|
|
185
203
|
}
|
|
186
204
|
|
|
187
205
|
// move select
|
|
206
|
+
volatiles.state = 0;
|
|
188
207
|
if (choice.data == 0) {
|
|
189
208
|
const struggle = ok: {
|
|
190
209
|
for (side.active.moves, 0..) |move, i| {
|
|
@@ -218,9 +237,9 @@ fn saveMove(battle: anytype, player: Player, choice: ?Choice) void {
|
|
|
218
237
|
|
|
219
238
|
side.last_selected_move = move.id;
|
|
220
239
|
if (player == .P1) {
|
|
221
|
-
battle.
|
|
240
|
+
battle.last_moves.p1_index = @intCast(u3, c.data);
|
|
222
241
|
} else {
|
|
223
|
-
battle.
|
|
242
|
+
battle.last_moves.p2_index = @intCast(u3, c.data);
|
|
224
243
|
}
|
|
225
244
|
}
|
|
226
245
|
}
|
|
@@ -240,9 +259,9 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, log: anyty
|
|
|
240
259
|
side.order[slot - 1] = out;
|
|
241
260
|
|
|
242
261
|
if (player == .P1) {
|
|
243
|
-
battle.
|
|
262
|
+
battle.last_moves.p1_index = 1;
|
|
244
263
|
} else {
|
|
245
|
-
battle.
|
|
264
|
+
battle.last_moves.p2_index = 1;
|
|
246
265
|
}
|
|
247
266
|
|
|
248
267
|
side.last_used_move = .None;
|
|
@@ -299,13 +318,11 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
|
|
|
299
318
|
const spe2 = battle.side(.P2).active.stats.spe;
|
|
300
319
|
if (spe1 == spe2) {
|
|
301
320
|
// Pokémon Showdown's beforeTurnCallback shenanigans
|
|
302
|
-
if (showdown and m1 == .Counter and m2 == .Counter)
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
battle.rng.range(u8, 0, 2) == 0
|
|
306
|
-
else
|
|
307
|
-
battle.rng.next() < Gen12.percent(50) + 1;
|
|
321
|
+
if (showdown and !double_switch and m1 == .Counter and m2 == .Counter) {
|
|
322
|
+
battle.rng.advance(1);
|
|
323
|
+
}
|
|
308
324
|
|
|
325
|
+
const p1 = Rolls.speedTie(battle);
|
|
309
326
|
if (!showdown) return if (p1) .P1 else .P2;
|
|
310
327
|
|
|
311
328
|
// Pokémon Showdown's "lockedmove" volatile's onBeforeTurn uses BattleQueue#changeAction,
|
|
@@ -315,7 +332,7 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
|
|
|
315
332
|
const t1 = battle.side(.P1).active.volatiles.Thrashing;
|
|
316
333
|
const t2 = battle.side(.P2).active.volatiles.Thrashing;
|
|
317
334
|
// If *both* sides are thrashing it really should be another speed tie, but we've patched
|
|
318
|
-
// that out and enforce host ordering of events, so P1 just goes first
|
|
335
|
+
// that out and enforce host ordering of events, so P1 just goes first regardless of who
|
|
319
336
|
// won the original coin flip
|
|
320
337
|
if (t1 and t2) return .P1;
|
|
321
338
|
return if (p1) if (t1 and !t2) .P2 else .P1 else if (t2 and !t1) .P1 else .P2;
|
|
@@ -328,9 +345,13 @@ fn doTurn(
|
|
|
328
345
|
battle: anytype,
|
|
329
346
|
player: Player,
|
|
330
347
|
player_choice: Choice,
|
|
348
|
+
player_rewrap: bool,
|
|
349
|
+
player_skip: bool,
|
|
331
350
|
player_from: ?Move,
|
|
332
351
|
foe_player: Player,
|
|
333
352
|
foe_choice: Choice,
|
|
353
|
+
foe_rewrap: bool,
|
|
354
|
+
foe_skip: bool,
|
|
334
355
|
foe_from: ?Move,
|
|
335
356
|
log: anytype,
|
|
336
357
|
) !?Result {
|
|
@@ -338,7 +359,16 @@ fn doTurn(
|
|
|
338
359
|
|
|
339
360
|
var residual = true;
|
|
340
361
|
var replace = battle.side(player).stored().hp == 0;
|
|
341
|
-
if (try executeMove(
|
|
362
|
+
if (try executeMove(
|
|
363
|
+
battle,
|
|
364
|
+
player,
|
|
365
|
+
player_choice,
|
|
366
|
+
player_rewrap,
|
|
367
|
+
player_skip,
|
|
368
|
+
player_from,
|
|
369
|
+
&residual,
|
|
370
|
+
log,
|
|
371
|
+
)) |r| return r;
|
|
342
372
|
if (!replace) {
|
|
343
373
|
if (player_choice.type != .Switch) {
|
|
344
374
|
if (try checkFaint(battle, foe_player, log)) |r| return r;
|
|
@@ -347,8 +377,18 @@ fn doTurn(
|
|
|
347
377
|
if (try checkFaint(battle, player, log)) |r| return r;
|
|
348
378
|
} else if (foe_choice.type == .Pass) return null;
|
|
349
379
|
|
|
380
|
+
residual = true;
|
|
350
381
|
replace = battle.side(foe_player).stored().hp == 0;
|
|
351
|
-
if (try executeMove(
|
|
382
|
+
if (try executeMove(
|
|
383
|
+
battle,
|
|
384
|
+
foe_player,
|
|
385
|
+
foe_choice,
|
|
386
|
+
foe_rewrap,
|
|
387
|
+
foe_skip,
|
|
388
|
+
foe_from,
|
|
389
|
+
&residual,
|
|
390
|
+
log,
|
|
391
|
+
)) |r| return r;
|
|
352
392
|
if (!replace) {
|
|
353
393
|
if (foe_choice.type != .Switch) {
|
|
354
394
|
if (try checkFaint(battle, player, log)) |r| return r;
|
|
@@ -371,6 +411,8 @@ fn executeMove(
|
|
|
371
411
|
battle: anytype,
|
|
372
412
|
player: Player,
|
|
373
413
|
choice: Choice,
|
|
414
|
+
rewrap: bool,
|
|
415
|
+
skip: bool,
|
|
374
416
|
from: ?Move,
|
|
375
417
|
residual: *bool,
|
|
376
418
|
log: anytype,
|
|
@@ -382,9 +424,10 @@ fn executeMove(
|
|
|
382
424
|
return null;
|
|
383
425
|
}
|
|
384
426
|
|
|
427
|
+
// This is the correct place to check for SKIP_TURN and abort early, however since Pokémon
|
|
428
|
+
// Showdown overwrites the SKIP_TURN sentinel with its botched move select we need to add an
|
|
429
|
+
// additional skip boolean to accomplish the same thing in the Binding check of BeforeMove
|
|
385
430
|
if (side.last_selected_move == .SKIP_TURN) {
|
|
386
|
-
// Pokémon Showdown overwrites the SKIP_TURN sentinel with its botched move select,
|
|
387
|
-
// Binding instead gets handled in beforeMove after sleep and freeze
|
|
388
431
|
assert(!showdown);
|
|
389
432
|
if (battle.foe(player).active.volatiles.Binding) {
|
|
390
433
|
try log.cant(battle.active(player), .Bound);
|
|
@@ -403,9 +446,9 @@ fn executeMove(
|
|
|
403
446
|
// choice.data == 0 only happens with Struggle on Pokémon Showdown
|
|
404
447
|
assert(!showdown);
|
|
405
448
|
mslot = @intCast(u4, if (player == .P1)
|
|
406
|
-
battle.
|
|
449
|
+
battle.last_moves.p1_index
|
|
407
450
|
else
|
|
408
|
-
battle.
|
|
451
|
+
battle.last_moves.p2_index);
|
|
409
452
|
const stored = side.stored();
|
|
410
453
|
// GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
|
|
411
454
|
auto = side.last_selected_move == .HyperBeam or side.active.volatiles.Bide or
|
|
@@ -427,14 +470,14 @@ fn executeMove(
|
|
|
427
470
|
// Incorrect mslot with Pokémon Showdown choice semantics so we need to recover from index
|
|
428
471
|
assert(mslot == 1);
|
|
429
472
|
mslot = @intCast(u4, if (player == .P1)
|
|
430
|
-
battle.
|
|
473
|
+
battle.last_moves.p1_index
|
|
431
474
|
else
|
|
432
|
-
battle.
|
|
475
|
+
battle.last_moves.p2_index);
|
|
433
476
|
}
|
|
434
477
|
|
|
435
478
|
var skip_can = false;
|
|
436
479
|
var skip_pp = false;
|
|
437
|
-
switch (try beforeMove(battle, player, from, log)) {
|
|
480
|
+
switch (try beforeMove(battle, player, skip, from, residual, log)) {
|
|
438
481
|
.done => return null,
|
|
439
482
|
.skip_can => skip_can = true,
|
|
440
483
|
.skip_pp => skip_pp = true,
|
|
@@ -446,12 +489,19 @@ fn executeMove(
|
|
|
446
489
|
return null;
|
|
447
490
|
}
|
|
448
491
|
|
|
449
|
-
return doMove(battle, player, mslot, auto, residual, log);
|
|
492
|
+
return doMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
450
493
|
}
|
|
451
494
|
|
|
452
|
-
const BeforeMove =
|
|
495
|
+
const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
|
|
453
496
|
|
|
454
|
-
fn beforeMove(
|
|
497
|
+
fn beforeMove(
|
|
498
|
+
battle: anytype,
|
|
499
|
+
player: Player,
|
|
500
|
+
skip: bool,
|
|
501
|
+
from: ?Move,
|
|
502
|
+
residual: *bool,
|
|
503
|
+
log: anytype,
|
|
504
|
+
) !BeforeMove {
|
|
455
505
|
var side = battle.side(player);
|
|
456
506
|
const foe = battle.foe(player);
|
|
457
507
|
var active = &side.active;
|
|
@@ -479,13 +529,15 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
|
|
|
479
529
|
return .done;
|
|
480
530
|
}
|
|
481
531
|
|
|
482
|
-
if (foe.active.volatiles.Binding) {
|
|
532
|
+
if (skip or foe.active.volatiles.Binding) {
|
|
483
533
|
try log.cant(ident, .Bound);
|
|
484
534
|
return .done;
|
|
485
535
|
}
|
|
486
536
|
|
|
487
|
-
if (
|
|
488
|
-
|
|
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;
|
|
489
541
|
try log.cant(ident, .Flinch);
|
|
490
542
|
return .done;
|
|
491
543
|
}
|
|
@@ -506,13 +558,6 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
|
|
|
506
558
|
// Pokémon Showdown's disable condition has a single onBeforeMove handler
|
|
507
559
|
if (showdown and try disabled(side, ident, log)) return .done;
|
|
508
560
|
|
|
509
|
-
// Pokémon Showdown checks for Flinch *after* instead of before
|
|
510
|
-
if (showdown and volatiles.Flinch) {
|
|
511
|
-
volatiles.Flinch = false;
|
|
512
|
-
try log.cant(ident, .Flinch);
|
|
513
|
-
return .done;
|
|
514
|
-
}
|
|
515
|
-
|
|
516
561
|
// This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
|
|
517
562
|
// before the side had a selected a move - we simply need to assume this leads to a desync
|
|
518
563
|
if (side.last_selected_move == .None) {
|
|
@@ -530,14 +575,9 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
|
|
|
530
575
|
} else {
|
|
531
576
|
try log.activate(ident, .Confusion);
|
|
532
577
|
|
|
533
|
-
|
|
534
|
-
!battle.rng.chance(u8, 128, 256)
|
|
535
|
-
else
|
|
536
|
-
battle.rng.next() >= Gen12.percent(50) + 1;
|
|
537
|
-
|
|
538
|
-
if (confused) {
|
|
578
|
+
if (Rolls.confused(battle)) {
|
|
539
579
|
assert(!volatiles.MultiHit);
|
|
540
|
-
volatiles.state = 0;
|
|
580
|
+
if (!volatiles.Rage) volatiles.state = 0;
|
|
541
581
|
volatiles.Bide = false;
|
|
542
582
|
volatiles.Thrashing = false;
|
|
543
583
|
volatiles.MultiHit = false;
|
|
@@ -561,8 +601,12 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
|
|
|
561
601
|
player
|
|
562
602
|
else
|
|
563
603
|
player.foe();
|
|
604
|
+
|
|
605
|
+
const uncapped = battle.last_damage;
|
|
564
606
|
// Skipping adjustDamage / randomizeDamage / checkHit
|
|
565
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;
|
|
566
610
|
|
|
567
611
|
return .done;
|
|
568
612
|
}
|
|
@@ -571,28 +615,28 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
|
|
|
571
615
|
|
|
572
616
|
if (!showdown and try disabled(side, ident, log)) return .done;
|
|
573
617
|
|
|
574
|
-
if (Status.is(stored.status, .PAR)) {
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
volatiles.Thrashing = false;
|
|
584
|
-
volatiles.Charging = false;
|
|
585
|
-
volatiles.Binding = false;
|
|
586
|
-
// GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
|
|
587
|
-
try log.cant(ident, .Paralysis);
|
|
588
|
-
return .done;
|
|
589
|
-
}
|
|
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;
|
|
590
627
|
}
|
|
591
628
|
|
|
592
629
|
if (volatiles.Bide) {
|
|
593
630
|
assert(!volatiles.Thrashing and !volatiles.Rage);
|
|
594
631
|
|
|
595
|
-
|
|
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
|
+
}
|
|
596
640
|
|
|
597
641
|
assert(volatiles.attacks > 0);
|
|
598
642
|
|
|
@@ -613,8 +657,17 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
|
|
|
613
657
|
return .done;
|
|
614
658
|
}
|
|
615
659
|
|
|
660
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
616
661
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
617
|
-
try buildRage(battle, player.foe(), log);
|
|
662
|
+
if (foe.stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
|
|
663
|
+
|
|
664
|
+
// For reasons passing understanding, Pokémon Showdown still inflicts residual damage to
|
|
665
|
+
// Bide's user even if the above damage has caused the foe to faint. It's simpler to always
|
|
666
|
+
// run residual here regardless of whether the foe fainted and opt-out of the default flow
|
|
667
|
+
if (showdown) {
|
|
668
|
+
residual.* = false;
|
|
669
|
+
try handleResidual(battle, player, log);
|
|
670
|
+
}
|
|
618
671
|
|
|
619
672
|
return .done;
|
|
620
673
|
}
|
|
@@ -627,8 +680,13 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
|
|
|
627
680
|
try log.start(battle.active(player), .ConfusionSilent);
|
|
628
681
|
}
|
|
629
682
|
try log.move(ident, side.last_selected_move, battle.active(player.foe()), from);
|
|
630
|
-
if (showdown
|
|
631
|
-
|
|
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
|
+
}
|
|
632
690
|
}
|
|
633
691
|
return .skip_can;
|
|
634
692
|
}
|
|
@@ -643,7 +701,11 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
|
|
|
643
701
|
side.last_selected_move,
|
|
644
702
|
);
|
|
645
703
|
if (showdown or battle.last_damage != 0) {
|
|
704
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
646
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
|
+
}
|
|
647
709
|
}
|
|
648
710
|
return .done;
|
|
649
711
|
}
|
|
@@ -675,6 +737,7 @@ fn canMove(
|
|
|
675
737
|
side.active.volatiles.Invulnerable = false;
|
|
676
738
|
} else if (move.effect == .Charge) {
|
|
677
739
|
try log.move(player_ident, side.last_selected_move, .{}, from);
|
|
740
|
+
setCounterable(battle, player, side, move);
|
|
678
741
|
try Effects.charge(battle, player, log);
|
|
679
742
|
return false;
|
|
680
743
|
}
|
|
@@ -691,6 +754,7 @@ fn canMove(
|
|
|
691
754
|
// regrettable, but this information is more "nice to have" than required
|
|
692
755
|
const f = if (from != null and from.? == .None) null else from;
|
|
693
756
|
try log.move(player_ident, side.last_selected_move, battle.active(target), f);
|
|
757
|
+
setCounterable(battle, player, side, move);
|
|
694
758
|
|
|
695
759
|
if (move.effect.onBegin()) {
|
|
696
760
|
try onBegin(battle, player, move, mslot, residual, log);
|
|
@@ -701,12 +765,27 @@ fn canMove(
|
|
|
701
765
|
Effects.thrashing(battle, player);
|
|
702
766
|
} else if (!showdown and move.effect == .Binding) {
|
|
703
767
|
// Pokémon Showdown handles this after hit/miss checks and damage calculation
|
|
704
|
-
Effects.binding(battle, player);
|
|
768
|
+
Effects.binding(battle, player, false);
|
|
705
769
|
}
|
|
706
770
|
|
|
707
771
|
return true;
|
|
708
772
|
}
|
|
709
773
|
|
|
774
|
+
fn setCounterable(battle: anytype, player: Player, side: *Side, move: Move.Data) void {
|
|
775
|
+
// The Counter desync is caused by the cartridge not calling GetCurrentMove until now, meaning
|
|
776
|
+
// in cases where an early return happens the data for a players last selected move does not get
|
|
777
|
+
// reloaded and HandleCounterMove actually bases its success/failure off of stale information.
|
|
778
|
+
// This boolean state we track here doesn't exist on the cartridge because it instead manifests
|
|
779
|
+
// as the desync results from actually having two separate battle states that subtly disagree
|
|
780
|
+
const counterable = side.last_selected_move != .Counter and move.bp > 0 and
|
|
781
|
+
(move.type == .Normal or move.type == .Fighting);
|
|
782
|
+
if (player == .P1) {
|
|
783
|
+
battle.last_moves.p1_counterable = @boolToInt(counterable);
|
|
784
|
+
} else {
|
|
785
|
+
battle.last_moves.p2_counterable = @boolToInt(counterable);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
710
789
|
fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
|
|
711
790
|
if (side.last_selected_move == .Struggle) return;
|
|
712
791
|
|
|
@@ -742,6 +821,7 @@ fn doMove(
|
|
|
742
821
|
battle: anytype,
|
|
743
822
|
player: Player,
|
|
744
823
|
mslot: u4,
|
|
824
|
+
rewrap: bool,
|
|
745
825
|
auto: bool,
|
|
746
826
|
residual: *bool,
|
|
747
827
|
log: anytype,
|
|
@@ -772,12 +852,15 @@ fn doMove(
|
|
|
772
852
|
(@enumToInt(move.type.effectiveness(foe.active.types.type1)) == 0 or
|
|
773
853
|
@enumToInt(move.type.effectiveness(foe.active.types.type2)) == 0);
|
|
774
854
|
if (immune and move.effect != .Binding) break :miss true;
|
|
775
|
-
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
|
+
}
|
|
776
859
|
break :miss move.target != .Self and !moveHit(battle, player, move, &immune, &mist);
|
|
777
860
|
};
|
|
778
861
|
assert(!immune or miss or (showdown and move.effect == .Binding));
|
|
779
862
|
|
|
780
|
-
var late = showdown;
|
|
863
|
+
var late = showdown and move.effect != .Explode;
|
|
781
864
|
const skip = status or immune;
|
|
782
865
|
if ((!showdown or (!skip or counter)) and !miss) blk: {
|
|
783
866
|
if (showdown and move.effect.isMulti()) {
|
|
@@ -830,9 +913,9 @@ fn doMove(
|
|
|
830
913
|
|
|
831
914
|
if (!showdown or !miss) {
|
|
832
915
|
if (move.effect == .MirrorMove) {
|
|
833
|
-
return mirrorMove(battle, player, mslot, auto, residual, log);
|
|
916
|
+
return mirrorMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
834
917
|
} else if (move.effect == .Metronome) {
|
|
835
|
-
return metronome(battle, player, mslot, auto, residual, log);
|
|
918
|
+
return metronome(battle, player, mslot, rewrap, auto, residual, log);
|
|
836
919
|
} else if (move.effect.onEnd()) {
|
|
837
920
|
try onEnd(battle, player, move, log);
|
|
838
921
|
return null;
|
|
@@ -847,7 +930,7 @@ fn doMove(
|
|
|
847
930
|
move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
|
|
848
931
|
if (ohko) {
|
|
849
932
|
try log.immune(foe_ident, .OHKO);
|
|
850
|
-
} else if (immune and !invulnerable) {
|
|
933
|
+
} else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
|
|
851
934
|
try log.immune(foe_ident, .None);
|
|
852
935
|
} else if (mist) {
|
|
853
936
|
if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
|
|
@@ -879,6 +962,9 @@ fn doMove(
|
|
|
879
962
|
hits = side.active.volatiles.attacks;
|
|
880
963
|
}
|
|
881
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
|
+
|
|
882
968
|
var nullified = false;
|
|
883
969
|
var hit: u4 = 0;
|
|
884
970
|
while (hit < hits) {
|
|
@@ -894,7 +980,7 @@ fn doMove(
|
|
|
894
980
|
if (hit == 0 and ohko) try log.ohko();
|
|
895
981
|
hit += 1;
|
|
896
982
|
if (foe.stored().hp == 0) break;
|
|
897
|
-
if (!late) try buildRage(battle, player.foe(), log);
|
|
983
|
+
if (!late and (!sub or move.effect == .Explode)) try buildRage(battle, player.foe(), log);
|
|
898
984
|
// If the substitute breaks during a multi-hit attack, the attack ends
|
|
899
985
|
if (nullified) break;
|
|
900
986
|
}
|
|
@@ -910,11 +996,17 @@ fn doMove(
|
|
|
910
996
|
} else if (showdown) {
|
|
911
997
|
// This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
|
|
912
998
|
if (move.effect == .Binding) {
|
|
913
|
-
Effects.binding(battle, player);
|
|
999
|
+
Effects.binding(battle, player, rewrap);
|
|
914
1000
|
if (immune) {
|
|
915
1001
|
battle.last_damage = 0;
|
|
1002
|
+
assert(foe.stored().hp > 0);
|
|
916
1003
|
// Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
|
|
917
|
-
|
|
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
|
+
}
|
|
918
1010
|
return null;
|
|
919
1011
|
}
|
|
920
1012
|
}
|
|
@@ -931,8 +1023,8 @@ fn doMove(
|
|
|
931
1023
|
|
|
932
1024
|
if (foe.stored().hp == 0) return null;
|
|
933
1025
|
|
|
934
|
-
// Pokémon Showdown builds Rage at the wrong time for non-MultiHit
|
|
935
|
-
if (late) 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);
|
|
936
1028
|
|
|
937
1029
|
if (!move.effect.isSpecial()) {
|
|
938
1030
|
// On the cartridge Rage is not considered to be "special" and thus gets executed for a
|
|
@@ -945,7 +1037,9 @@ fn doMove(
|
|
|
945
1037
|
if (move.effect == .Twineedle) {
|
|
946
1038
|
if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), log);
|
|
947
1039
|
} else if (move.effect == .Disable) {
|
|
948
|
-
|
|
1040
|
+
const result = try Effects.disable(battle, player, move, log);
|
|
1041
|
+
if (showdown) try buildRage(battle, player.foe(), log);
|
|
1042
|
+
return result;
|
|
949
1043
|
} else {
|
|
950
1044
|
try moveEffect(battle, player, move, log);
|
|
951
1045
|
}
|
|
@@ -963,8 +1057,7 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
|
|
|
963
1057
|
chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
|
|
964
1058
|
chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
|
|
965
1059
|
|
|
966
|
-
|
|
967
|
-
return std.math.rotl(u8, battle.rng.next(), 3) < chance;
|
|
1060
|
+
return Rolls.criticalHit(battle, chance);
|
|
968
1061
|
}
|
|
969
1062
|
|
|
970
1063
|
fn calcDamage(
|
|
@@ -1068,50 +1161,36 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
|
1068
1161
|
|
|
1069
1162
|
fn randomizeDamage(battle: anytype) void {
|
|
1070
1163
|
if (battle.last_damage <= 1) return;
|
|
1071
|
-
|
|
1072
|
-
const random = if (showdown)
|
|
1073
|
-
battle.rng.range(u8, 217, 256)
|
|
1074
|
-
else loop: {
|
|
1075
|
-
while (true) {
|
|
1076
|
-
const r = std.math.rotr(u8, battle.rng.next(), 1);
|
|
1077
|
-
if (r >= 217) break :loop r;
|
|
1078
|
-
}
|
|
1079
|
-
};
|
|
1080
|
-
|
|
1164
|
+
const random = Rolls.damage(battle);
|
|
1081
1165
|
battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
|
|
1082
1166
|
}
|
|
1083
1167
|
|
|
1084
1168
|
fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
|
|
1085
1169
|
const side = battle.side(player);
|
|
1170
|
+
const foe = battle.foe(player);
|
|
1086
1171
|
|
|
1087
1172
|
if (!try checkHit(battle, player, move, log)) return null;
|
|
1088
1173
|
|
|
1089
1174
|
battle.last_damage = switch (side.last_selected_move) {
|
|
1090
|
-
.SuperFang => @max(
|
|
1175
|
+
.SuperFang => @max(foe.stored().hp / 2, 1),
|
|
1091
1176
|
.SeismicToss, .NightShade => side.stored().level,
|
|
1092
1177
|
.SonicBoom => 20,
|
|
1093
1178
|
.DragonRage => 40,
|
|
1094
1179
|
// GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
|
|
1095
1180
|
.Psywave => power: {
|
|
1096
1181
|
const max = @intCast(u8, @as(u16, side.stored().level) * 3 / 2);
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
// GLITCH: Psywave infinite glitch loop
|
|
1101
|
-
if (max <= 1) return Result.Error;
|
|
1102
|
-
while (true) {
|
|
1103
|
-
const r = battle.rng.next();
|
|
1104
|
-
if (r < max) break :power r;
|
|
1105
|
-
}
|
|
1106
|
-
}
|
|
1182
|
+
// GLITCH: Psywave infinite glitch loop
|
|
1183
|
+
if (!showdown and max <= 1) return Result.Error;
|
|
1184
|
+
break :power Rolls.psywave(battle, max);
|
|
1107
1185
|
},
|
|
1108
1186
|
else => unreachable,
|
|
1109
1187
|
};
|
|
1110
1188
|
|
|
1111
1189
|
if (battle.last_damage == 0) return if (showdown) null else Result.Error;
|
|
1112
1190
|
|
|
1191
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1113
1192
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
1114
|
-
try buildRage(battle, player.foe(), log);
|
|
1193
|
+
if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
|
|
1115
1194
|
|
|
1116
1195
|
return null;
|
|
1117
1196
|
}
|
|
@@ -1124,19 +1203,17 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
|
|
|
1124
1203
|
return null;
|
|
1125
1204
|
}
|
|
1126
1205
|
|
|
1127
|
-
// Pretend
|
|
1128
|
-
const foe_last_used_move =
|
|
1129
|
-
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
|
|
1130
1207
|
const foe_last_selected_move =
|
|
1131
1208
|
Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
|
|
1132
|
-
.
|
|
1209
|
+
.Splash
|
|
1133
1210
|
else
|
|
1134
1211
|
foe.last_selected_move);
|
|
1135
1212
|
|
|
1136
|
-
const used =
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1213
|
+
const used = (if (player.foe() == .P1)
|
|
1214
|
+
battle.last_moves.p1_counterable
|
|
1215
|
+
else
|
|
1216
|
+
battle.last_moves.p2_counterable) != 0;
|
|
1140
1217
|
|
|
1141
1218
|
const selected = foe_last_selected_move.bp > 0 and
|
|
1142
1219
|
foe.last_selected_move != .Counter and
|
|
@@ -1155,12 +1232,14 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
|
|
|
1155
1232
|
return null;
|
|
1156
1233
|
}
|
|
1157
1234
|
|
|
1158
|
-
battle.last_damage
|
|
1235
|
+
battle.last_damage *|= 2;
|
|
1159
1236
|
|
|
1160
1237
|
// Pokémon Showdown calls checkHit before Counter
|
|
1161
1238
|
if (!showdown and !try checkHit(battle, player, move, log)) return null;
|
|
1162
1239
|
|
|
1240
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1163
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);
|
|
1164
1243
|
return null;
|
|
1165
1244
|
}
|
|
1166
1245
|
|
|
@@ -1204,6 +1283,7 @@ fn mirrorMove(
|
|
|
1204
1283
|
battle: anytype,
|
|
1205
1284
|
player: Player,
|
|
1206
1285
|
mslot: u4,
|
|
1286
|
+
rewrap: bool,
|
|
1207
1287
|
auto: bool,
|
|
1208
1288
|
residual: *bool,
|
|
1209
1289
|
log: anytype,
|
|
@@ -1216,41 +1296,30 @@ fn mirrorMove(
|
|
|
1216
1296
|
if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
|
|
1217
1297
|
try log.fail(battle.active(player), .None);
|
|
1218
1298
|
return null;
|
|
1299
|
+
} else if (!showdown or foe.last_used_move != .Struggle) {
|
|
1300
|
+
incrementPP(side, mslot);
|
|
1219
1301
|
}
|
|
1220
1302
|
|
|
1221
|
-
incrementPP(side, mslot);
|
|
1222
|
-
|
|
1223
1303
|
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
|
|
1224
|
-
return doMove(battle, player, mslot, auto, residual, log);
|
|
1304
|
+
return doMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
1225
1305
|
}
|
|
1226
1306
|
|
|
1227
1307
|
fn metronome(
|
|
1228
1308
|
battle: anytype,
|
|
1229
1309
|
player: Player,
|
|
1230
1310
|
mslot: u4,
|
|
1311
|
+
rewrap: bool,
|
|
1231
1312
|
auto: bool,
|
|
1232
1313
|
residual: *bool,
|
|
1233
1314
|
log: anytype,
|
|
1234
1315
|
) !?Result {
|
|
1235
1316
|
var side = battle.side(player);
|
|
1236
1317
|
|
|
1237
|
-
side.last_selected_move =
|
|
1238
|
-
const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
|
|
1239
|
-
const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
|
|
1240
|
-
break :blk @intToEnum(Move, r + mod);
|
|
1241
|
-
} else loop: {
|
|
1242
|
-
while (true) {
|
|
1243
|
-
const r = battle.rng.next();
|
|
1244
|
-
if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
|
|
1245
|
-
if (r >= @enumToInt(Move.Struggle)) continue;
|
|
1246
|
-
break :loop @intToEnum(Move, r);
|
|
1247
|
-
}
|
|
1248
|
-
};
|
|
1249
|
-
|
|
1318
|
+
side.last_selected_move = Rolls.metronome(battle);
|
|
1250
1319
|
incrementPP(side, mslot);
|
|
1251
1320
|
|
|
1252
1321
|
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
|
|
1253
|
-
return doMove(battle, player, mslot, auto, residual, log);
|
|
1322
|
+
return doMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
1254
1323
|
}
|
|
1255
1324
|
|
|
1256
1325
|
fn checkHit(battle: anytype, player: Player, move: Move.Data, log: anytype) !bool {
|
|
@@ -1304,23 +1373,24 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
|
|
|
1304
1373
|
}
|
|
1305
1374
|
|
|
1306
1375
|
// GLITCH: Thrash / Petal Dance / Rage get their accuracy overwritten on subsequent hits
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
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));
|
|
1311
1382
|
var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
|
|
1312
1383
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1313
1384
|
boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
|
|
1314
1385
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1315
1386
|
accuracy = @min(255, @max(1, accuracy));
|
|
1316
1387
|
|
|
1317
|
-
|
|
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;
|
|
1318
1391
|
|
|
1319
1392
|
// GLITCH: max accuracy is 255 so 1/256 chance of miss
|
|
1320
|
-
break :miss
|
|
1321
|
-
!battle.rng.chance(u8, @intCast(u8, accuracy), 256)
|
|
1322
|
-
else
|
|
1323
|
-
battle.rng.next() >= accuracy;
|
|
1393
|
+
break :miss !Rolls.hit(battle, accuracy);
|
|
1324
1394
|
};
|
|
1325
1395
|
|
|
1326
1396
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1435,6 +1505,15 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
|
|
|
1435
1505
|
}
|
|
1436
1506
|
|
|
1437
1507
|
if (volatiles.LeechSeed) {
|
|
1508
|
+
var foe = battle.foe(player);
|
|
1509
|
+
var foe_stored = foe.stored();
|
|
1510
|
+
const foe_ident = battle.active(player.foe());
|
|
1511
|
+
|
|
1512
|
+
if (foe_stored.hp == 0) {
|
|
1513
|
+
assert(showdown);
|
|
1514
|
+
return;
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1438
1517
|
var damage = @max(stored.stats.hp / 16, 1);
|
|
1439
1518
|
|
|
1440
1519
|
// GLITCH: Leech Seed + Toxic glitch
|
|
@@ -1446,10 +1525,6 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
|
|
|
1446
1525
|
const amount = @min(damage, stored.hp);
|
|
1447
1526
|
stored.hp -= amount;
|
|
1448
1527
|
|
|
1449
|
-
var foe = battle.foe(player);
|
|
1450
|
-
var foe_stored = foe.stored();
|
|
1451
|
-
const foe_ident = battle.active(player.foe());
|
|
1452
|
-
|
|
1453
1528
|
// As above, Pokémon Showdown uses damageOf but its not relevant
|
|
1454
1529
|
if (amount > 0) try log.damage(ident, stored, .LeechSeed);
|
|
1455
1530
|
|
|
@@ -1465,13 +1540,13 @@ fn endTurn(battle: anytype, log: anytype) @TypeOf(log).Error!Result {
|
|
|
1465
1540
|
assert(!battle.side(.P1).active.volatiles.MultiHit);
|
|
1466
1541
|
assert(!battle.side(.P2).active.volatiles.MultiHit);
|
|
1467
1542
|
|
|
1543
|
+
battle.turn += 1;
|
|
1544
|
+
|
|
1468
1545
|
if (showdown and options.ebc and checkEBC(battle)) {
|
|
1469
1546
|
try log.tie();
|
|
1470
1547
|
return Result.Tie;
|
|
1471
1548
|
}
|
|
1472
1549
|
|
|
1473
|
-
battle.turn += 1;
|
|
1474
|
-
|
|
1475
1550
|
if (showdown and battle.turn >= 1000) {
|
|
1476
1551
|
try log.tie();
|
|
1477
1552
|
return Result.Tie;
|
|
@@ -1538,6 +1613,42 @@ fn checkEBC(battle: anytype) bool {
|
|
|
1538
1613
|
return false;
|
|
1539
1614
|
}
|
|
1540
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
|
+
|
|
1541
1652
|
inline fn onBegin(
|
|
1542
1653
|
battle: anytype,
|
|
1543
1654
|
player: Player,
|
|
@@ -1628,10 +1739,7 @@ pub const Effects = struct {
|
|
|
1628
1739
|
side.active.volatiles.Bide = true;
|
|
1629
1740
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1630
1741
|
side.active.volatiles.state = 0;
|
|
1631
|
-
side.active.volatiles.attacks =
|
|
1632
|
-
battle.rng.range(u4, 2, 4)
|
|
1633
|
-
else
|
|
1634
|
-
(battle.rng.next() & 1) + 2);
|
|
1742
|
+
side.active.volatiles.attacks = Rolls.bideThrashDuration(battle);
|
|
1635
1743
|
|
|
1636
1744
|
try log.start(battle.active(player), .Bide);
|
|
1637
1745
|
}
|
|
@@ -1653,14 +1761,8 @@ pub const Effects = struct {
|
|
|
1653
1761
|
return;
|
|
1654
1762
|
}
|
|
1655
1763
|
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
else
|
|
1659
|
-
battle.rng.next() < 1 + (if (move.effect == .BurnChance1)
|
|
1660
|
-
Gen12.percent(10)
|
|
1661
|
-
else
|
|
1662
|
-
Gen12.percent(30));
|
|
1663
|
-
if (!chance) return;
|
|
1764
|
+
if (foe.active.types.includes(move.type)) return;
|
|
1765
|
+
if (!Rolls.secondaryChance(battle, move.effect == .BurnChance1)) return;
|
|
1664
1766
|
|
|
1665
1767
|
foe_stored.status = Status.init(.BRN);
|
|
1666
1768
|
foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
|
|
@@ -1684,11 +1786,7 @@ pub const Effects = struct {
|
|
|
1684
1786
|
const sub = foe.active.volatiles.Substitute;
|
|
1685
1787
|
|
|
1686
1788
|
if (move.effect == .ConfusionChance) {
|
|
1687
|
-
|
|
1688
|
-
battle.rng.chance(u8, 25, 256)
|
|
1689
|
-
else
|
|
1690
|
-
battle.rng.next() < Gen12.percent(10);
|
|
1691
|
-
if (!chance) return;
|
|
1789
|
+
if (!Rolls.confusionChance(battle)) return;
|
|
1692
1790
|
} else {
|
|
1693
1791
|
if (showdown) {
|
|
1694
1792
|
if (!try checkHit(battle, player, move, log)) {
|
|
@@ -1707,10 +1805,7 @@ pub const Effects = struct {
|
|
|
1707
1805
|
|
|
1708
1806
|
if (foe.active.volatiles.Confusion) return;
|
|
1709
1807
|
foe.active.volatiles.Confusion = true;
|
|
1710
|
-
foe.active.volatiles.confusion =
|
|
1711
|
-
battle.rng.range(u8, 2, 6)
|
|
1712
|
-
else
|
|
1713
|
-
(battle.rng.next() & 3) + 2);
|
|
1808
|
+
foe.active.volatiles.confusion = Rolls.confusionDuration(battle);
|
|
1714
1809
|
|
|
1715
1810
|
try log.start(battle.active(player.foe()), .Confusion);
|
|
1716
1811
|
}
|
|
@@ -1760,11 +1855,8 @@ pub const Effects = struct {
|
|
|
1760
1855
|
return Result.Error;
|
|
1761
1856
|
}
|
|
1762
1857
|
|
|
1763
|
-
volatiles.disabled_move = @intCast(u3,
|
|
1764
|
-
volatiles.disabled_duration =
|
|
1765
|
-
battle.rng.range(u8, 1, 9)
|
|
1766
|
-
else
|
|
1767
|
-
(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);
|
|
1768
1860
|
|
|
1769
1861
|
try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
|
|
1770
1862
|
return null;
|
|
@@ -1776,7 +1868,7 @@ pub const Effects = struct {
|
|
|
1776
1868
|
|
|
1777
1869
|
if (battle.last_damage == 0) {
|
|
1778
1870
|
assert(showdown);
|
|
1779
|
-
return;
|
|
1871
|
+
if (!battle.foe(player).active.volatiles.Substitute) return;
|
|
1780
1872
|
}
|
|
1781
1873
|
|
|
1782
1874
|
const drain = @max(battle.last_damage / 2, 1);
|
|
@@ -1803,15 +1895,7 @@ pub const Effects = struct {
|
|
|
1803
1895
|
var volatiles = &battle.foe(player).active.volatiles;
|
|
1804
1896
|
|
|
1805
1897
|
if (volatiles.Substitute) return;
|
|
1806
|
-
|
|
1807
|
-
const chance = if (showdown)
|
|
1808
|
-
battle.rng.chance(u8, @as(u8, if (move.effect == .FlinchChance1) 26 else 77), 256)
|
|
1809
|
-
else
|
|
1810
|
-
battle.rng.next() < 1 + (if (move.effect == .FlinchChance1)
|
|
1811
|
-
Gen12.percent(10)
|
|
1812
|
-
else
|
|
1813
|
-
Gen12.percent(30));
|
|
1814
|
-
if (!chance) return;
|
|
1898
|
+
if (!Rolls.secondaryChance(battle, move.effect == .FlinchChance1)) return;
|
|
1815
1899
|
|
|
1816
1900
|
volatiles.Flinch = true;
|
|
1817
1901
|
volatiles.Recharging = false;
|
|
@@ -1837,12 +1921,8 @@ pub const Effects = struct {
|
|
|
1837
1921
|
return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
|
|
1838
1922
|
}
|
|
1839
1923
|
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
else
|
|
1843
|
-
battle.rng.next() < 1 + Gen12.percent(10);
|
|
1844
|
-
if (!chance) return;
|
|
1845
|
-
|
|
1924
|
+
if (foe.active.types.includes(move.type)) return;
|
|
1925
|
+
if (!Rolls.secondaryChance(battle, true)) return;
|
|
1846
1926
|
// Freeze Clause Mod
|
|
1847
1927
|
if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
1848
1928
|
|
|
@@ -1979,15 +2059,15 @@ pub const Effects = struct {
|
|
|
1979
2059
|
}
|
|
1980
2060
|
break :has_mimic false;
|
|
1981
2061
|
};
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
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;
|
|
1985
2065
|
return try log.fail(battle.active(player.foe()), .None);
|
|
1986
2066
|
}
|
|
1987
2067
|
}
|
|
1988
2068
|
if (!try checkHit(battle, player, move, log)) return;
|
|
1989
2069
|
|
|
1990
|
-
const rslot =
|
|
2070
|
+
const rslot = Rolls.moveSlot(battle, &foe.active.moves, 0);
|
|
1991
2071
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
1992
2072
|
|
|
1993
2073
|
try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
|
|
@@ -2008,7 +2088,8 @@ pub const Effects = struct {
|
|
|
2008
2088
|
assert(!side.active.volatiles.MultiHit);
|
|
2009
2089
|
side.active.volatiles.MultiHit = true;
|
|
2010
2090
|
|
|
2011
|
-
side.active.volatiles.attacks =
|
|
2091
|
+
side.active.volatiles.attacks =
|
|
2092
|
+
if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
|
|
2012
2093
|
}
|
|
2013
2094
|
|
|
2014
2095
|
fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2052,14 +2133,8 @@ pub const Effects = struct {
|
|
|
2052
2133
|
}
|
|
2053
2134
|
|
|
2054
2135
|
// Body Slam can't paralyze a Normal type Pokémon
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
else
|
|
2058
|
-
battle.rng.next() < 1 + (if (move.effect == .ParalyzeChance1)
|
|
2059
|
-
Gen12.percent(10)
|
|
2060
|
-
else
|
|
2061
|
-
Gen12.percent(30));
|
|
2062
|
-
if (!chance) return;
|
|
2136
|
+
if (foe.active.types.includes(move.type)) return;
|
|
2137
|
+
if (!Rolls.secondaryChance(battle, move.effect == .ParalyzeChance1)) return;
|
|
2063
2138
|
|
|
2064
2139
|
foe_stored.status = Status.init(.PAR);
|
|
2065
2140
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
@@ -2102,16 +2177,7 @@ pub const Effects = struct {
|
|
|
2102
2177
|
|
|
2103
2178
|
if (move.effect == .Poison) {
|
|
2104
2179
|
if (!showdown and !try checkHit(battle, player, move, log)) return;
|
|
2105
|
-
} else
|
|
2106
|
-
const chance = if (showdown)
|
|
2107
|
-
battle.rng.chance(u8, @as(u8, if (move.effect == .PoisonChance1) 52 else 103), 256)
|
|
2108
|
-
else
|
|
2109
|
-
battle.rng.next() < 1 + (if (move.effect == .PoisonChance1)
|
|
2110
|
-
Gen12.percent(20)
|
|
2111
|
-
else
|
|
2112
|
-
Gen12.percent(40));
|
|
2113
|
-
if (!chance) return;
|
|
2114
|
-
}
|
|
2180
|
+
} else if (!Rolls.poisonChance(battle, move.effect == .PoisonChance1)) return;
|
|
2115
2181
|
|
|
2116
2182
|
foe_stored.status = Status.init(.PSN);
|
|
2117
2183
|
if (toxic) {
|
|
@@ -2125,10 +2191,8 @@ pub const Effects = struct {
|
|
|
2125
2191
|
|
|
2126
2192
|
fn rage(battle: anytype, player: Player) !void {
|
|
2127
2193
|
var volatiles = &battle.side(player).active.volatiles;
|
|
2128
|
-
if (volatiles.Rage) return;
|
|
2129
|
-
volatiles.Rage = true;
|
|
2130
2194
|
assert(!volatiles.Bide);
|
|
2131
|
-
volatiles.
|
|
2195
|
+
volatiles.Rage = true;
|
|
2132
2196
|
}
|
|
2133
2197
|
|
|
2134
2198
|
fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
|
|
@@ -2180,16 +2244,7 @@ pub const Effects = struct {
|
|
|
2180
2244
|
}
|
|
2181
2245
|
foe.active.volatiles.Recharging = false;
|
|
2182
2246
|
|
|
2183
|
-
|
|
2184
|
-
battle.rng.range(u8, 1, 8)
|
|
2185
|
-
else loop: {
|
|
2186
|
-
while (true) {
|
|
2187
|
-
const r = battle.rng.next() & 7;
|
|
2188
|
-
if (r != 0) break :loop r;
|
|
2189
|
-
}
|
|
2190
|
-
});
|
|
2191
|
-
|
|
2192
|
-
foe_stored.status = Status.slp(duration);
|
|
2247
|
+
foe_stored.status = Status.slp(Rolls.sleepDuration(battle));
|
|
2193
2248
|
try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
|
|
2194
2249
|
}
|
|
2195
2250
|
|
|
@@ -2231,8 +2286,9 @@ pub const Effects = struct {
|
|
|
2231
2286
|
fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
2232
2287
|
if (!showdown or battle.side(player).last_selected_move == .Teleport) return;
|
|
2233
2288
|
|
|
2234
|
-
// Whirlwind/Roar should not roll to hit but Pokémon Showdown does anyway
|
|
2289
|
+
// Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
|
|
2235
2290
|
_ = try checkHit(battle, player, move, log);
|
|
2291
|
+
battle.last_damage = 0;
|
|
2236
2292
|
}
|
|
2237
2293
|
|
|
2238
2294
|
fn thrashing(battle: anytype, player: Player) void {
|
|
@@ -2241,11 +2297,7 @@ pub const Effects = struct {
|
|
|
2241
2297
|
assert(!volatiles.Bide);
|
|
2242
2298
|
|
|
2243
2299
|
volatiles.Thrashing = true;
|
|
2244
|
-
volatiles.
|
|
2245
|
-
volatiles.attacks = @intCast(u3, if (showdown)
|
|
2246
|
-
battle.rng.range(u8, 2, 4)
|
|
2247
|
-
else
|
|
2248
|
-
(battle.rng.next() & 1) + 2);
|
|
2300
|
+
volatiles.attacks = Rolls.bideThrashDuration(battle);
|
|
2249
2301
|
}
|
|
2250
2302
|
|
|
2251
2303
|
fn transform(battle: anytype, player: Player, log: anytype) !void {
|
|
@@ -2277,7 +2329,7 @@ pub const Effects = struct {
|
|
|
2277
2329
|
try log.transform(battle.active(player), foe_ident);
|
|
2278
2330
|
}
|
|
2279
2331
|
|
|
2280
|
-
fn binding(battle: anytype, player: Player) void {
|
|
2332
|
+
fn binding(battle: anytype, player: Player, rewrap: bool) void {
|
|
2281
2333
|
var side = battle.side(player);
|
|
2282
2334
|
var foe = battle.foe(player);
|
|
2283
2335
|
|
|
@@ -2285,9 +2337,11 @@ pub const Effects = struct {
|
|
|
2285
2337
|
side.active.volatiles.Binding = true;
|
|
2286
2338
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2287
2339
|
// (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
|
|
2288
|
-
if (!showdown)
|
|
2340
|
+
if (!showdown) {
|
|
2341
|
+
foe.active.volatiles.Recharging = false;
|
|
2342
|
+
} else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
|
|
2289
2343
|
|
|
2290
|
-
side.active.volatiles.attacks = distribution(battle) - 1;
|
|
2344
|
+
side.active.volatiles.attacks = Rolls.distribution(battle) - 1;
|
|
2291
2345
|
}
|
|
2292
2346
|
|
|
2293
2347
|
fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2314,9 +2368,11 @@ pub const Effects = struct {
|
|
|
2314
2368
|
return try log.fail(ident, .None);
|
|
2315
2369
|
}
|
|
2316
2370
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2317
|
-
const stat = unmodifiedStats(battle,
|
|
2371
|
+
const stat = unmodifiedStats(battle, side).atk;
|
|
2318
2372
|
stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2319
2373
|
try log.boost(ident, reason, n);
|
|
2374
|
+
// Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
|
|
2375
|
+
if (showdown and move.effect == .Rage) return;
|
|
2320
2376
|
},
|
|
2321
2377
|
.DefenseUp1, .DefenseUp2 => {
|
|
2322
2378
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
@@ -2332,7 +2388,7 @@ pub const Effects = struct {
|
|
|
2332
2388
|
return try log.fail(ident, .None);
|
|
2333
2389
|
}
|
|
2334
2390
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2335
|
-
const stat = unmodifiedStats(battle,
|
|
2391
|
+
const stat = unmodifiedStats(battle, side).def;
|
|
2336
2392
|
stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2337
2393
|
try log.boost(ident, .Defense, n);
|
|
2338
2394
|
},
|
|
@@ -2349,7 +2405,7 @@ pub const Effects = struct {
|
|
|
2349
2405
|
return try log.fail(ident, .None);
|
|
2350
2406
|
}
|
|
2351
2407
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2352
|
-
const stat = unmodifiedStats(battle,
|
|
2408
|
+
const stat = unmodifiedStats(battle, side).spe;
|
|
2353
2409
|
stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2354
2410
|
try log.boost(ident, .Speed, 2);
|
|
2355
2411
|
},
|
|
@@ -2369,7 +2425,7 @@ pub const Effects = struct {
|
|
|
2369
2425
|
return try log.fail(ident, .None);
|
|
2370
2426
|
}
|
|
2371
2427
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2372
|
-
const stat = unmodifiedStats(battle,
|
|
2428
|
+
const stat = unmodifiedStats(battle, side).spc;
|
|
2373
2429
|
stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2374
2430
|
try log.boost(ident, .SpecialAttack, n);
|
|
2375
2431
|
try log.boost(ident, .SpecialDefense, n);
|
|
@@ -2396,11 +2452,7 @@ pub const Effects = struct {
|
|
|
2396
2452
|
}
|
|
2397
2453
|
|
|
2398
2454
|
if (move.effect.isStatDownChance()) {
|
|
2399
|
-
|
|
2400
|
-
battle.rng.chance(u8, 85, 256)
|
|
2401
|
-
else
|
|
2402
|
-
battle.rng.next() < Gen12.percent(33) + 1;
|
|
2403
|
-
if (!chance or foe.active.volatiles.Invulnerable) return;
|
|
2455
|
+
if (!Rolls.unboost(battle) or foe.active.volatiles.Invulnerable) return;
|
|
2404
2456
|
} else if (!showdown and !try checkHit(battle, player, move, log)) {
|
|
2405
2457
|
return; // checkHit already checks for Invulnerable
|
|
2406
2458
|
}
|
|
@@ -2422,7 +2474,7 @@ pub const Effects = struct {
|
|
|
2422
2474
|
return try log.fail(foe_ident, .None);
|
|
2423
2475
|
}
|
|
2424
2476
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2425
|
-
const stat = unmodifiedStats(battle,
|
|
2477
|
+
const stat = unmodifiedStats(battle, foe).atk;
|
|
2426
2478
|
stats.atk = @max(1, stat * mod[0] / mod[1]);
|
|
2427
2479
|
try log.boost(foe_ident, .Attack, -1);
|
|
2428
2480
|
},
|
|
@@ -2440,7 +2492,7 @@ pub const Effects = struct {
|
|
|
2440
2492
|
return try log.fail(foe_ident, .None);
|
|
2441
2493
|
}
|
|
2442
2494
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2443
|
-
const stat = unmodifiedStats(battle,
|
|
2495
|
+
const stat = unmodifiedStats(battle, foe).def;
|
|
2444
2496
|
stats.def = @max(1, stat * mod[0] / mod[1]);
|
|
2445
2497
|
try log.boost(foe_ident, .Defense, -@as(i8, n));
|
|
2446
2498
|
},
|
|
@@ -2457,7 +2509,7 @@ pub const Effects = struct {
|
|
|
2457
2509
|
return try log.fail(foe_ident, .None);
|
|
2458
2510
|
}
|
|
2459
2511
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2460
|
-
const stat = unmodifiedStats(battle,
|
|
2512
|
+
const stat = unmodifiedStats(battle, foe).spe;
|
|
2461
2513
|
stats.spe = @max(1, stat * mod[0] / mod[1]);
|
|
2462
2514
|
try log.boost(foe_ident, .Speed, -1);
|
|
2463
2515
|
assert(boosts.spe >= -6);
|
|
@@ -2477,7 +2529,7 @@ pub const Effects = struct {
|
|
|
2477
2529
|
return try log.fail(foe_ident, .None);
|
|
2478
2530
|
}
|
|
2479
2531
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2480
|
-
const stat = unmodifiedStats(battle,
|
|
2532
|
+
const stat = unmodifiedStats(battle, foe).spc;
|
|
2481
2533
|
stats.spc = @max(1, stat * mod[0] / mod[1]);
|
|
2482
2534
|
try log.boost(foe_ident, .SpecialAttack, -1);
|
|
2483
2535
|
try log.boost(foe_ident, .SpecialDefense, -1);
|
|
@@ -2496,8 +2548,7 @@ pub const Effects = struct {
|
|
|
2496
2548
|
}
|
|
2497
2549
|
};
|
|
2498
2550
|
|
|
2499
|
-
fn unmodifiedStats(battle: anytype,
|
|
2500
|
-
const side = battle.side(who);
|
|
2551
|
+
fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
|
|
2501
2552
|
if (!side.active.volatiles.Transform) return &side.stored().stats;
|
|
2502
2553
|
const id = ID.from(side.active.volatiles.transform);
|
|
2503
2554
|
return &battle.side(id.player).pokemon[id.id - 1].stats;
|
|
@@ -2563,75 +2614,140 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
|
|
|
2563
2614
|
}
|
|
2564
2615
|
}
|
|
2565
2616
|
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2617
|
+
pub const Rolls = struct {
|
|
2618
|
+
inline fn speedTie(battle: anytype) bool {
|
|
2619
|
+
if (showdown) return battle.rng.range(u8, 0, 2) == 0;
|
|
2620
|
+
return battle.rng.next() < Gen12.percent(50) + 1;
|
|
2621
|
+
}
|
|
2622
|
+
|
|
2623
|
+
inline fn criticalHit(battle: anytype, chance: u16) bool {
|
|
2624
|
+
if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
|
|
2625
|
+
return std.math.rotl(u8, battle.rng.next(), 3) < chance;
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2628
|
+
inline fn damage(battle: anytype) u8 {
|
|
2629
|
+
if (showdown) return battle.rng.range(u8, 217, 256);
|
|
2630
|
+
while (true) {
|
|
2631
|
+
const r = std.math.rotr(u8, battle.rng.next(), 1);
|
|
2632
|
+
if (r >= 217) return r;
|
|
2574
2633
|
}
|
|
2575
2634
|
}
|
|
2576
|
-
return false;
|
|
2577
|
-
}
|
|
2578
2635
|
|
|
2579
|
-
inline fn
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
try Effects.boost(battle, who, Move.get(.Rage), log);
|
|
2636
|
+
inline fn hit(battle: anytype, accuracy: u16) bool {
|
|
2637
|
+
if (showdown) return battle.rng.chance(u8, @intCast(u8, accuracy), 256);
|
|
2638
|
+
return battle.rng.next() < accuracy;
|
|
2583
2639
|
}
|
|
2584
|
-
}
|
|
2585
2640
|
|
|
2586
|
-
fn
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2641
|
+
inline fn confusionDuration(battle: anytype) u3 {
|
|
2642
|
+
if (showdown) return @intCast(u3, battle.rng.range(u8, 2, 6));
|
|
2643
|
+
return @intCast(u3, (battle.rng.next() & 3) + 2);
|
|
2644
|
+
}
|
|
2590
2645
|
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
else
|
|
2596
|
-
(battle.rng.next() & 3) + 2);
|
|
2646
|
+
inline fn confused(battle: anytype) bool {
|
|
2647
|
+
if (showdown) return !battle.rng.chance(u8, 128, 256);
|
|
2648
|
+
return battle.rng.next() >= Gen12.percent(50) + 1;
|
|
2649
|
+
}
|
|
2597
2650
|
|
|
2598
|
-
|
|
2599
|
-
|
|
2651
|
+
inline fn paralyzed(battle: anytype) bool {
|
|
2652
|
+
if (showdown) return battle.rng.chance(u8, 63, 256);
|
|
2653
|
+
return battle.rng.next() < Gen12.percent(25);
|
|
2654
|
+
}
|
|
2600
2655
|
|
|
2601
|
-
|
|
2656
|
+
inline fn confusionChance(battle: anytype) bool {
|
|
2657
|
+
if (showdown) return battle.rng.chance(u8, 25, 256);
|
|
2658
|
+
return battle.rng.next() < Gen12.percent(10);
|
|
2659
|
+
}
|
|
2602
2660
|
|
|
2603
|
-
fn
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
}
|
|
2661
|
+
inline fn secondaryChance(battle: anytype, low: bool) bool {
|
|
2662
|
+
if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 26 else 77), 256);
|
|
2663
|
+
return battle.rng.next() < 1 + (if (low) Gen12.percent(10) else Gen12.percent(30));
|
|
2664
|
+
}
|
|
2608
2665
|
|
|
2609
|
-
fn
|
|
2610
|
-
|
|
2611
|
-
if (
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2666
|
+
inline fn poisonChance(battle: anytype, low: bool) bool {
|
|
2667
|
+
if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 52 else 103), 256);
|
|
2668
|
+
return battle.rng.next() < 1 + (if (low) Gen12.percent(20) else Gen12.percent(40));
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2671
|
+
inline fn sleepDuration(battle: anytype) u3 {
|
|
2672
|
+
if (showdown) return @intCast(u3, battle.rng.range(u8, 1, 8));
|
|
2673
|
+
while (true) {
|
|
2674
|
+
const r = battle.rng.next() & 7;
|
|
2675
|
+
if (r != 0) return @intCast(u3, r);
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
inline fn bideThrashDuration(battle: anytype) u3 {
|
|
2680
|
+
if (showdown) return @intCast(u3, battle.rng.range(u4, 2, 4));
|
|
2681
|
+
return @intCast(u3, (battle.rng.next() & 1) + 2);
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
fn moveSlot(battle: anytype, moves: []MoveSlot, check_pp: u4) u4 {
|
|
2685
|
+
if (showdown) {
|
|
2686
|
+
if (check_pp == 0) {
|
|
2687
|
+
var i: usize = moves.len;
|
|
2688
|
+
while (i > 0) {
|
|
2689
|
+
i -= 1;
|
|
2690
|
+
if (moves[i].id != .None) {
|
|
2691
|
+
return battle.rng.range(u4, 0, @intCast(u4, i + 1)) + 1;
|
|
2692
|
+
}
|
|
2624
2693
|
}
|
|
2694
|
+
} else {
|
|
2695
|
+
var r = battle.rng.range(u4, 0, @intCast(u4, check_pp)) + 1;
|
|
2696
|
+
var i: usize = 0;
|
|
2697
|
+
while (i < moves.len and r > 0) : (i += 1) {
|
|
2698
|
+
if (moves[i].pp > 0) {
|
|
2699
|
+
r -= 1;
|
|
2700
|
+
if (r == 0) break;
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
return @intCast(u4, i + 1);
|
|
2625
2704
|
}
|
|
2626
|
-
|
|
2705
|
+
}
|
|
2706
|
+
|
|
2707
|
+
while (true) {
|
|
2708
|
+
const r = @intCast(u4, battle.rng.next() & 3);
|
|
2709
|
+
if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
|
|
2627
2710
|
}
|
|
2628
2711
|
}
|
|
2629
2712
|
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2713
|
+
inline fn disableDuration(battle: anytype) u4 {
|
|
2714
|
+
if (showdown) return @intCast(u4, battle.rng.range(u8, 1, 9));
|
|
2715
|
+
return @intCast(u4, (battle.rng.next() & 7) + 1);
|
|
2633
2716
|
}
|
|
2634
|
-
|
|
2717
|
+
|
|
2718
|
+
inline fn metronome(battle: anytype) Move {
|
|
2719
|
+
if (showdown) {
|
|
2720
|
+
const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
|
|
2721
|
+
const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
|
|
2722
|
+
return @intToEnum(Move, r + mod);
|
|
2723
|
+
}
|
|
2724
|
+
while (true) {
|
|
2725
|
+
const r = battle.rng.next();
|
|
2726
|
+
if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
|
|
2727
|
+
if (r >= @enumToInt(Move.Struggle)) continue;
|
|
2728
|
+
return @intToEnum(Move, r);
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2732
|
+
inline fn psywave(battle: anytype, max: u8) u8 {
|
|
2733
|
+
if (showdown) return battle.rng.range(u8, 0, max);
|
|
2734
|
+
while (true) {
|
|
2735
|
+
const r = battle.rng.next();
|
|
2736
|
+
if (r < max) return r;
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
|
|
2740
|
+
inline fn distribution(battle: anytype) u3 {
|
|
2741
|
+
if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
|
|
2742
|
+
const r = (battle.rng.next() & 3);
|
|
2743
|
+
return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
|
|
2744
|
+
}
|
|
2745
|
+
|
|
2746
|
+
inline fn unboost(battle: anytype) bool {
|
|
2747
|
+
if (showdown) return battle.rng.chance(u8, 85, 256);
|
|
2748
|
+
return battle.rng.next() < Gen12.percent(33) + 1;
|
|
2749
|
+
}
|
|
2750
|
+
};
|
|
2635
2751
|
|
|
2636
2752
|
test "RNG agreement" {
|
|
2637
2753
|
if (!showdown) return;
|
|
@@ -2731,9 +2847,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
2731
2847
|
while (slot <= 4) : (slot += 1) {
|
|
2732
2848
|
const m = active.moves[slot - 1];
|
|
2733
2849
|
if (m.id == .None) break;
|
|
2734
|
-
if (m.id == side.last_selected_move) {
|
|
2735
|
-
// Pokémon Showdown displays Struggle if limited to Bide
|
|
2736
|
-
const
|
|
2850
|
+
if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
|
|
2851
|
+
// Pokémon Showdown displays Struggle if limited to Bide but unable to pick
|
|
2852
|
+
const struggle =
|
|
2853
|
+
m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);
|
|
2854
|
+
const s = if (struggle) 0 else slot;
|
|
2737
2855
|
out[n] = .{ .type = .Move, .data = s };
|
|
2738
2856
|
n += 1;
|
|
2739
2857
|
return n;
|