@pkmn/engine 0.1.0-dev.2ec67dba → 0.1.0-dev.4005e495
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.js +1 -1
- package/build/pkg/protocol.js.map +1 -1
- package/build/test/display.js +400 -0
- package/build/test/display.js.map +1 -0
- package/build/tools/debug.js +133 -0
- package/build/tools/debug.js.map +1 -0
- package/build.zig +24 -34
- package/package.json +41 -30
- 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 -18
- package/src/lib/gen1/mechanics.zig +442 -316
- 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,7 +657,18 @@ 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);
|
|
662
|
+
if (foe.stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
|
|
663
|
+
|
|
664
|
+
// For reasons passing understanding, Pokémon Showdown still inflicts residual damage to
|
|
665
|
+
// Bide's user even if the above damage has caused the foe to faint. It's simpler to always
|
|
666
|
+
// run residual here regardless of whether the foe fainted and opt-out of the default flow
|
|
667
|
+
if (showdown) {
|
|
668
|
+
residual.* = false;
|
|
669
|
+
try handleResidual(battle, player, log);
|
|
670
|
+
}
|
|
671
|
+
|
|
617
672
|
return .done;
|
|
618
673
|
}
|
|
619
674
|
|
|
@@ -625,8 +680,13 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
|
|
|
625
680
|
try log.start(battle.active(player), .ConfusionSilent);
|
|
626
681
|
}
|
|
627
682
|
try log.move(ident, side.last_selected_move, battle.active(player.foe()), from);
|
|
628
|
-
if (showdown
|
|
629
|
-
|
|
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
|
+
}
|
|
630
690
|
}
|
|
631
691
|
return .skip_can;
|
|
632
692
|
}
|
|
@@ -641,7 +701,11 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
|
|
|
641
701
|
side.last_selected_move,
|
|
642
702
|
);
|
|
643
703
|
if (showdown or battle.last_damage != 0) {
|
|
704
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
644
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
|
+
}
|
|
645
709
|
}
|
|
646
710
|
return .done;
|
|
647
711
|
}
|
|
@@ -673,6 +737,7 @@ fn canMove(
|
|
|
673
737
|
side.active.volatiles.Invulnerable = false;
|
|
674
738
|
} else if (move.effect == .Charge) {
|
|
675
739
|
try log.move(player_ident, side.last_selected_move, .{}, from);
|
|
740
|
+
setCounterable(battle, player, side, move);
|
|
676
741
|
try Effects.charge(battle, player, log);
|
|
677
742
|
return false;
|
|
678
743
|
}
|
|
@@ -689,6 +754,7 @@ fn canMove(
|
|
|
689
754
|
// regrettable, but this information is more "nice to have" than required
|
|
690
755
|
const f = if (from != null and from.? == .None) null else from;
|
|
691
756
|
try log.move(player_ident, side.last_selected_move, battle.active(target), f);
|
|
757
|
+
setCounterable(battle, player, side, move);
|
|
692
758
|
|
|
693
759
|
if (move.effect.onBegin()) {
|
|
694
760
|
try onBegin(battle, player, move, mslot, residual, log);
|
|
@@ -699,12 +765,27 @@ fn canMove(
|
|
|
699
765
|
Effects.thrashing(battle, player);
|
|
700
766
|
} else if (!showdown and move.effect == .Binding) {
|
|
701
767
|
// Pokémon Showdown handles this after hit/miss checks and damage calculation
|
|
702
|
-
Effects.binding(battle, player);
|
|
768
|
+
Effects.binding(battle, player, false);
|
|
703
769
|
}
|
|
704
770
|
|
|
705
771
|
return true;
|
|
706
772
|
}
|
|
707
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
|
+
|
|
708
789
|
fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
|
|
709
790
|
if (side.last_selected_move == .Struggle) return;
|
|
710
791
|
|
|
@@ -740,6 +821,7 @@ fn doMove(
|
|
|
740
821
|
battle: anytype,
|
|
741
822
|
player: Player,
|
|
742
823
|
mslot: u4,
|
|
824
|
+
rewrap: bool,
|
|
743
825
|
auto: bool,
|
|
744
826
|
residual: *bool,
|
|
745
827
|
log: anytype,
|
|
@@ -770,16 +852,21 @@ fn doMove(
|
|
|
770
852
|
(@enumToInt(move.type.effectiveness(foe.active.types.type1)) == 0 or
|
|
771
853
|
@enumToInt(move.type.effectiveness(foe.active.types.type2)) == 0);
|
|
772
854
|
if (immune and move.effect != .Binding) break :miss true;
|
|
773
|
-
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
|
+
}
|
|
774
859
|
break :miss move.target != .Self and !moveHit(battle, player, move, &immune, &mist);
|
|
775
860
|
};
|
|
776
861
|
assert(!immune or miss or (showdown and move.effect == .Binding));
|
|
777
862
|
|
|
863
|
+
var late = showdown and move.effect != .Explode;
|
|
778
864
|
const skip = status or immune;
|
|
779
865
|
if ((!showdown or (!skip or counter)) and !miss) blk: {
|
|
780
866
|
if (showdown and move.effect.isMulti()) {
|
|
781
867
|
Effects.multiHit(battle, player, move);
|
|
782
868
|
hits = side.active.volatiles.attacks;
|
|
869
|
+
late = false;
|
|
783
870
|
}
|
|
784
871
|
|
|
785
872
|
// Cartridge rolls for crit even for moves that can't crit (Counter/Metronome/status/OHKO)
|
|
@@ -813,7 +900,7 @@ fn doMove(
|
|
|
813
900
|
}
|
|
814
901
|
|
|
815
902
|
// Due to control flow shenanigans we need to clear last_damage for Pokémon Showdown
|
|
816
|
-
if (showdown and skip
|
|
903
|
+
if (showdown and skip) battle.last_damage = 0;
|
|
817
904
|
|
|
818
905
|
miss = if (showdown or skip)
|
|
819
906
|
miss
|
|
@@ -826,9 +913,9 @@ fn doMove(
|
|
|
826
913
|
|
|
827
914
|
if (!showdown or !miss) {
|
|
828
915
|
if (move.effect == .MirrorMove) {
|
|
829
|
-
return mirrorMove(battle, player, mslot, auto, residual, log);
|
|
916
|
+
return mirrorMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
830
917
|
} else if (move.effect == .Metronome) {
|
|
831
|
-
return metronome(battle, player, mslot, auto, residual, log);
|
|
918
|
+
return metronome(battle, player, mslot, rewrap, auto, residual, log);
|
|
832
919
|
} else if (move.effect.onEnd()) {
|
|
833
920
|
try onEnd(battle, player, move, log);
|
|
834
921
|
return null;
|
|
@@ -843,7 +930,7 @@ fn doMove(
|
|
|
843
930
|
move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
|
|
844
931
|
if (ohko) {
|
|
845
932
|
try log.immune(foe_ident, .OHKO);
|
|
846
|
-
} else if (immune and !invulnerable) {
|
|
933
|
+
} else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
|
|
847
934
|
try log.immune(foe_ident, .None);
|
|
848
935
|
} else if (mist) {
|
|
849
936
|
if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
|
|
@@ -860,15 +947,9 @@ fn doMove(
|
|
|
860
947
|
if (showdown and side.stored().hp == 0) residual.* = false;
|
|
861
948
|
} else if (move.effect == .Explode) {
|
|
862
949
|
try Effects.explode(battle, player);
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
} else if (showdown) {
|
|
867
|
-
if (move.effect == .Disable) {
|
|
868
|
-
if (foe.active.volatiles.Rage and foe.active.boosts.atk < 6) {
|
|
869
|
-
try Effects.boost(battle, player.foe(), Move.get(.Rage), log);
|
|
870
|
-
}
|
|
871
|
-
}
|
|
950
|
+
try buildRage(battle, player.foe(), log);
|
|
951
|
+
} else if (showdown and move.effect == .Disable) {
|
|
952
|
+
try buildRage(battle, player.foe(), log);
|
|
872
953
|
}
|
|
873
954
|
return null;
|
|
874
955
|
}
|
|
@@ -881,6 +962,9 @@ fn doMove(
|
|
|
881
962
|
hits = side.active.volatiles.attacks;
|
|
882
963
|
}
|
|
883
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
|
+
|
|
884
968
|
var nullified = false;
|
|
885
969
|
var hit: u4 = 0;
|
|
886
970
|
while (hit < hits) {
|
|
@@ -896,9 +980,7 @@ fn doMove(
|
|
|
896
980
|
if (hit == 0 and ohko) try log.ohko();
|
|
897
981
|
hit += 1;
|
|
898
982
|
if (foe.stored().hp == 0) break;
|
|
899
|
-
if (
|
|
900
|
-
try Effects.boost(battle, player.foe(), Move.get(.Rage), log);
|
|
901
|
-
}
|
|
983
|
+
if (!late and (!sub or move.effect == .Explode)) try buildRage(battle, player.foe(), log);
|
|
902
984
|
// If the substitute breaks during a multi-hit attack, the attack ends
|
|
903
985
|
if (nullified) break;
|
|
904
986
|
}
|
|
@@ -914,11 +996,17 @@ fn doMove(
|
|
|
914
996
|
} else if (showdown) {
|
|
915
997
|
// This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
|
|
916
998
|
if (move.effect == .Binding) {
|
|
917
|
-
Effects.binding(battle, player);
|
|
999
|
+
Effects.binding(battle, player, rewrap);
|
|
918
1000
|
if (immune) {
|
|
919
1001
|
battle.last_damage = 0;
|
|
1002
|
+
assert(foe.stored().hp > 0);
|
|
920
1003
|
// Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
|
|
921
|
-
|
|
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
|
+
}
|
|
922
1010
|
return null;
|
|
923
1011
|
}
|
|
924
1012
|
}
|
|
@@ -935,6 +1023,9 @@ fn doMove(
|
|
|
935
1023
|
|
|
936
1024
|
if (foe.stored().hp == 0) return null;
|
|
937
1025
|
|
|
1026
|
+
// Pokémon Showdown builds Rage at the wrong time for non-MultiHit move
|
|
1027
|
+
if (late and !sub and move.effect != .Disable) try buildRage(battle, player.foe(), log);
|
|
1028
|
+
|
|
938
1029
|
if (!move.effect.isSpecial()) {
|
|
939
1030
|
// On the cartridge Rage is not considered to be "special" and thus gets executed for a
|
|
940
1031
|
// second time here (after being executed in the "always happens" block above) but that
|
|
@@ -946,7 +1037,9 @@ fn doMove(
|
|
|
946
1037
|
if (move.effect == .Twineedle) {
|
|
947
1038
|
if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), log);
|
|
948
1039
|
} else if (move.effect == .Disable) {
|
|
949
|
-
|
|
1040
|
+
const result = try Effects.disable(battle, player, move, log);
|
|
1041
|
+
if (showdown) try buildRage(battle, player.foe(), log);
|
|
1042
|
+
return result;
|
|
950
1043
|
} else {
|
|
951
1044
|
try moveEffect(battle, player, move, log);
|
|
952
1045
|
}
|
|
@@ -964,8 +1057,7 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
|
|
|
964
1057
|
chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
|
|
965
1058
|
chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
|
|
966
1059
|
|
|
967
|
-
|
|
968
|
-
return std.math.rotl(u8, battle.rng.next(), 3) < chance;
|
|
1060
|
+
return Rolls.criticalHit(battle, chance);
|
|
969
1061
|
}
|
|
970
1062
|
|
|
971
1063
|
fn calcDamage(
|
|
@@ -1069,49 +1161,37 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
|
1069
1161
|
|
|
1070
1162
|
fn randomizeDamage(battle: anytype) void {
|
|
1071
1163
|
if (battle.last_damage <= 1) return;
|
|
1072
|
-
|
|
1073
|
-
const random = if (showdown)
|
|
1074
|
-
battle.rng.range(u8, 217, 256)
|
|
1075
|
-
else loop: {
|
|
1076
|
-
while (true) {
|
|
1077
|
-
const r = std.math.rotr(u8, battle.rng.next(), 1);
|
|
1078
|
-
if (r >= 217) break :loop r;
|
|
1079
|
-
}
|
|
1080
|
-
};
|
|
1081
|
-
|
|
1164
|
+
const random = Rolls.damage(battle);
|
|
1082
1165
|
battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
|
|
1083
1166
|
}
|
|
1084
1167
|
|
|
1085
1168
|
fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
|
|
1086
1169
|
const side = battle.side(player);
|
|
1170
|
+
const foe = battle.foe(player);
|
|
1087
1171
|
|
|
1088
1172
|
if (!try checkHit(battle, player, move, log)) return null;
|
|
1089
1173
|
|
|
1090
1174
|
battle.last_damage = switch (side.last_selected_move) {
|
|
1091
|
-
.SuperFang => @max(
|
|
1175
|
+
.SuperFang => @max(foe.stored().hp / 2, 1),
|
|
1092
1176
|
.SeismicToss, .NightShade => side.stored().level,
|
|
1093
1177
|
.SonicBoom => 20,
|
|
1094
1178
|
.DragonRage => 40,
|
|
1095
1179
|
// GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
|
|
1096
1180
|
.Psywave => power: {
|
|
1097
1181
|
const max = @intCast(u8, @as(u16, side.stored().level) * 3 / 2);
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
// GLITCH: Psywave infinite glitch loop
|
|
1102
|
-
if (max <= 1) return Result.Error;
|
|
1103
|
-
while (true) {
|
|
1104
|
-
const r = battle.rng.next();
|
|
1105
|
-
if (r < max) break :power r;
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1182
|
+
// GLITCH: Psywave infinite glitch loop
|
|
1183
|
+
if (!showdown and max <= 1) return Result.Error;
|
|
1184
|
+
break :power Rolls.psywave(battle, max);
|
|
1108
1185
|
},
|
|
1109
1186
|
else => unreachable,
|
|
1110
1187
|
};
|
|
1111
1188
|
|
|
1112
1189
|
if (battle.last_damage == 0) return if (showdown) null else Result.Error;
|
|
1113
1190
|
|
|
1191
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1114
1192
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
1193
|
+
if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
|
|
1194
|
+
|
|
1115
1195
|
return null;
|
|
1116
1196
|
}
|
|
1117
1197
|
|
|
@@ -1123,19 +1203,17 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
|
|
|
1123
1203
|
return null;
|
|
1124
1204
|
}
|
|
1125
1205
|
|
|
1126
|
-
// Pretend
|
|
1127
|
-
const foe_last_used_move =
|
|
1128
|
-
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
|
|
1129
1207
|
const foe_last_selected_move =
|
|
1130
1208
|
Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
|
|
1131
|
-
.
|
|
1209
|
+
.Splash
|
|
1132
1210
|
else
|
|
1133
1211
|
foe.last_selected_move);
|
|
1134
1212
|
|
|
1135
|
-
const used =
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1213
|
+
const used = (if (player.foe() == .P1)
|
|
1214
|
+
battle.last_moves.p1_counterable
|
|
1215
|
+
else
|
|
1216
|
+
battle.last_moves.p2_counterable) != 0;
|
|
1139
1217
|
|
|
1140
1218
|
const selected = foe_last_selected_move.bp > 0 and
|
|
1141
1219
|
foe.last_selected_move != .Counter and
|
|
@@ -1154,12 +1232,14 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
|
|
|
1154
1232
|
return null;
|
|
1155
1233
|
}
|
|
1156
1234
|
|
|
1157
|
-
battle.last_damage
|
|
1235
|
+
battle.last_damage *|= 2;
|
|
1158
1236
|
|
|
1159
1237
|
// Pokémon Showdown calls checkHit before Counter
|
|
1160
1238
|
if (!showdown and !try checkHit(battle, player, move, log)) return null;
|
|
1161
1239
|
|
|
1240
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1162
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);
|
|
1163
1243
|
return null;
|
|
1164
1244
|
}
|
|
1165
1245
|
|
|
@@ -1203,6 +1283,7 @@ fn mirrorMove(
|
|
|
1203
1283
|
battle: anytype,
|
|
1204
1284
|
player: Player,
|
|
1205
1285
|
mslot: u4,
|
|
1286
|
+
rewrap: bool,
|
|
1206
1287
|
auto: bool,
|
|
1207
1288
|
residual: *bool,
|
|
1208
1289
|
log: anytype,
|
|
@@ -1215,41 +1296,30 @@ fn mirrorMove(
|
|
|
1215
1296
|
if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
|
|
1216
1297
|
try log.fail(battle.active(player), .None);
|
|
1217
1298
|
return null;
|
|
1299
|
+
} else if (!showdown or foe.last_used_move != .Struggle) {
|
|
1300
|
+
incrementPP(side, mslot);
|
|
1218
1301
|
}
|
|
1219
1302
|
|
|
1220
|
-
incrementPP(side, mslot);
|
|
1221
|
-
|
|
1222
1303
|
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
|
|
1223
|
-
return doMove(battle, player, mslot, auto, residual, log);
|
|
1304
|
+
return doMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
1224
1305
|
}
|
|
1225
1306
|
|
|
1226
1307
|
fn metronome(
|
|
1227
1308
|
battle: anytype,
|
|
1228
1309
|
player: Player,
|
|
1229
1310
|
mslot: u4,
|
|
1311
|
+
rewrap: bool,
|
|
1230
1312
|
auto: bool,
|
|
1231
1313
|
residual: *bool,
|
|
1232
1314
|
log: anytype,
|
|
1233
1315
|
) !?Result {
|
|
1234
1316
|
var side = battle.side(player);
|
|
1235
1317
|
|
|
1236
|
-
side.last_selected_move =
|
|
1237
|
-
const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
|
|
1238
|
-
const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
|
|
1239
|
-
break :blk @intToEnum(Move, r + mod);
|
|
1240
|
-
} else loop: {
|
|
1241
|
-
while (true) {
|
|
1242
|
-
const r = battle.rng.next();
|
|
1243
|
-
if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
|
|
1244
|
-
if (r >= @enumToInt(Move.Struggle)) continue;
|
|
1245
|
-
break :loop @intToEnum(Move, r);
|
|
1246
|
-
}
|
|
1247
|
-
};
|
|
1248
|
-
|
|
1318
|
+
side.last_selected_move = Rolls.metronome(battle);
|
|
1249
1319
|
incrementPP(side, mslot);
|
|
1250
1320
|
|
|
1251
1321
|
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
|
|
1252
|
-
return doMove(battle, player, mslot, auto, residual, log);
|
|
1322
|
+
return doMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
1253
1323
|
}
|
|
1254
1324
|
|
|
1255
1325
|
fn checkHit(battle: anytype, player: Player, move: Move.Data, log: anytype) !bool {
|
|
@@ -1303,23 +1373,24 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
|
|
|
1303
1373
|
}
|
|
1304
1374
|
|
|
1305
1375
|
// GLITCH: Thrash / Petal Dance / Rage get their accuracy overwritten on subsequent hits
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
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));
|
|
1310
1382
|
var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
|
|
1311
1383
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1312
1384
|
boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
|
|
1313
1385
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1314
1386
|
accuracy = @min(255, @max(1, accuracy));
|
|
1315
1387
|
|
|
1316
|
-
|
|
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;
|
|
1317
1391
|
|
|
1318
1392
|
// GLITCH: max accuracy is 255 so 1/256 chance of miss
|
|
1319
|
-
break :miss
|
|
1320
|
-
!battle.rng.chance(u8, @intCast(u8, accuracy), 256)
|
|
1321
|
-
else
|
|
1322
|
-
battle.rng.next() >= accuracy;
|
|
1393
|
+
break :miss !Rolls.hit(battle, accuracy);
|
|
1323
1394
|
};
|
|
1324
1395
|
|
|
1325
1396
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1434,6 +1505,15 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
|
|
|
1434
1505
|
}
|
|
1435
1506
|
|
|
1436
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
|
+
|
|
1437
1517
|
var damage = @max(stored.stats.hp / 16, 1);
|
|
1438
1518
|
|
|
1439
1519
|
// GLITCH: Leech Seed + Toxic glitch
|
|
@@ -1445,10 +1525,6 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
|
|
|
1445
1525
|
const amount = @min(damage, stored.hp);
|
|
1446
1526
|
stored.hp -= amount;
|
|
1447
1527
|
|
|
1448
|
-
var foe = battle.foe(player);
|
|
1449
|
-
var foe_stored = foe.stored();
|
|
1450
|
-
const foe_ident = battle.active(player.foe());
|
|
1451
|
-
|
|
1452
1528
|
// As above, Pokémon Showdown uses damageOf but its not relevant
|
|
1453
1529
|
if (amount > 0) try log.damage(ident, stored, .LeechSeed);
|
|
1454
1530
|
|
|
@@ -1464,13 +1540,13 @@ fn endTurn(battle: anytype, log: anytype) @TypeOf(log).Error!Result {
|
|
|
1464
1540
|
assert(!battle.side(.P1).active.volatiles.MultiHit);
|
|
1465
1541
|
assert(!battle.side(.P2).active.volatiles.MultiHit);
|
|
1466
1542
|
|
|
1543
|
+
battle.turn += 1;
|
|
1544
|
+
|
|
1467
1545
|
if (showdown and options.ebc and checkEBC(battle)) {
|
|
1468
1546
|
try log.tie();
|
|
1469
1547
|
return Result.Tie;
|
|
1470
1548
|
}
|
|
1471
1549
|
|
|
1472
|
-
battle.turn += 1;
|
|
1473
|
-
|
|
1474
1550
|
if (showdown and battle.turn >= 1000) {
|
|
1475
1551
|
try log.tie();
|
|
1476
1552
|
return Result.Tie;
|
|
@@ -1537,6 +1613,42 @@ fn checkEBC(battle: anytype) bool {
|
|
|
1537
1613
|
return false;
|
|
1538
1614
|
}
|
|
1539
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
|
+
|
|
1540
1652
|
inline fn onBegin(
|
|
1541
1653
|
battle: anytype,
|
|
1542
1654
|
player: Player,
|
|
@@ -1627,10 +1739,7 @@ pub const Effects = struct {
|
|
|
1627
1739
|
side.active.volatiles.Bide = true;
|
|
1628
1740
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1629
1741
|
side.active.volatiles.state = 0;
|
|
1630
|
-
side.active.volatiles.attacks =
|
|
1631
|
-
battle.rng.range(u4, 2, 4)
|
|
1632
|
-
else
|
|
1633
|
-
(battle.rng.next() & 1) + 2);
|
|
1742
|
+
side.active.volatiles.attacks = Rolls.bideThrashDuration(battle);
|
|
1634
1743
|
|
|
1635
1744
|
try log.start(battle.active(player), .Bide);
|
|
1636
1745
|
}
|
|
@@ -1652,14 +1761,8 @@ pub const Effects = struct {
|
|
|
1652
1761
|
return;
|
|
1653
1762
|
}
|
|
1654
1763
|
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
else
|
|
1658
|
-
battle.rng.next() < 1 + (if (move.effect == .BurnChance1)
|
|
1659
|
-
Gen12.percent(10)
|
|
1660
|
-
else
|
|
1661
|
-
Gen12.percent(30));
|
|
1662
|
-
if (!chance) return;
|
|
1764
|
+
if (foe.active.types.includes(move.type)) return;
|
|
1765
|
+
if (!Rolls.secondaryChance(battle, move.effect == .BurnChance1)) return;
|
|
1663
1766
|
|
|
1664
1767
|
foe_stored.status = Status.init(.BRN);
|
|
1665
1768
|
foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
|
|
@@ -1683,11 +1786,7 @@ pub const Effects = struct {
|
|
|
1683
1786
|
const sub = foe.active.volatiles.Substitute;
|
|
1684
1787
|
|
|
1685
1788
|
if (move.effect == .ConfusionChance) {
|
|
1686
|
-
|
|
1687
|
-
battle.rng.chance(u8, 25, 256)
|
|
1688
|
-
else
|
|
1689
|
-
battle.rng.next() < Gen12.percent(10);
|
|
1690
|
-
if (!chance) return;
|
|
1789
|
+
if (!Rolls.confusionChance(battle)) return;
|
|
1691
1790
|
} else {
|
|
1692
1791
|
if (showdown) {
|
|
1693
1792
|
if (!try checkHit(battle, player, move, log)) {
|
|
@@ -1706,10 +1805,7 @@ pub const Effects = struct {
|
|
|
1706
1805
|
|
|
1707
1806
|
if (foe.active.volatiles.Confusion) return;
|
|
1708
1807
|
foe.active.volatiles.Confusion = true;
|
|
1709
|
-
foe.active.volatiles.confusion =
|
|
1710
|
-
battle.rng.range(u8, 2, 6)
|
|
1711
|
-
else
|
|
1712
|
-
(battle.rng.next() & 3) + 2);
|
|
1808
|
+
foe.active.volatiles.confusion = Rolls.confusionDuration(battle);
|
|
1713
1809
|
|
|
1714
1810
|
try log.start(battle.active(player.foe()), .Confusion);
|
|
1715
1811
|
}
|
|
@@ -1759,11 +1855,8 @@ pub const Effects = struct {
|
|
|
1759
1855
|
return Result.Error;
|
|
1760
1856
|
}
|
|
1761
1857
|
|
|
1762
|
-
volatiles.disabled_move = @intCast(u3,
|
|
1763
|
-
volatiles.disabled_duration =
|
|
1764
|
-
battle.rng.range(u8, 1, 9)
|
|
1765
|
-
else
|
|
1766
|
-
(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);
|
|
1767
1860
|
|
|
1768
1861
|
try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
|
|
1769
1862
|
return null;
|
|
@@ -1775,7 +1868,7 @@ pub const Effects = struct {
|
|
|
1775
1868
|
|
|
1776
1869
|
if (battle.last_damage == 0) {
|
|
1777
1870
|
assert(showdown);
|
|
1778
|
-
return;
|
|
1871
|
+
if (!battle.foe(player).active.volatiles.Substitute) return;
|
|
1779
1872
|
}
|
|
1780
1873
|
|
|
1781
1874
|
const drain = @max(battle.last_damage / 2, 1);
|
|
@@ -1802,15 +1895,7 @@ pub const Effects = struct {
|
|
|
1802
1895
|
var volatiles = &battle.foe(player).active.volatiles;
|
|
1803
1896
|
|
|
1804
1897
|
if (volatiles.Substitute) return;
|
|
1805
|
-
|
|
1806
|
-
const chance = if (showdown)
|
|
1807
|
-
battle.rng.chance(u8, @as(u8, if (move.effect == .FlinchChance1) 26 else 77), 256)
|
|
1808
|
-
else
|
|
1809
|
-
battle.rng.next() < 1 + (if (move.effect == .FlinchChance1)
|
|
1810
|
-
Gen12.percent(10)
|
|
1811
|
-
else
|
|
1812
|
-
Gen12.percent(30));
|
|
1813
|
-
if (!chance) return;
|
|
1898
|
+
if (!Rolls.secondaryChance(battle, move.effect == .FlinchChance1)) return;
|
|
1814
1899
|
|
|
1815
1900
|
volatiles.Flinch = true;
|
|
1816
1901
|
volatiles.Recharging = false;
|
|
@@ -1836,12 +1921,8 @@ pub const Effects = struct {
|
|
|
1836
1921
|
return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
|
|
1837
1922
|
}
|
|
1838
1923
|
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
else
|
|
1842
|
-
battle.rng.next() < 1 + Gen12.percent(10);
|
|
1843
|
-
if (!chance) return;
|
|
1844
|
-
|
|
1924
|
+
if (foe.active.types.includes(move.type)) return;
|
|
1925
|
+
if (!Rolls.secondaryChance(battle, true)) return;
|
|
1845
1926
|
// Freeze Clause Mod
|
|
1846
1927
|
if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
1847
1928
|
|
|
@@ -1978,15 +2059,15 @@ pub const Effects = struct {
|
|
|
1978
2059
|
}
|
|
1979
2060
|
break :has_mimic false;
|
|
1980
2061
|
};
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
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;
|
|
1984
2065
|
return try log.fail(battle.active(player.foe()), .None);
|
|
1985
2066
|
}
|
|
1986
2067
|
}
|
|
1987
2068
|
if (!try checkHit(battle, player, move, log)) return;
|
|
1988
2069
|
|
|
1989
|
-
const rslot =
|
|
2070
|
+
const rslot = Rolls.moveSlot(battle, &foe.active.moves, 0);
|
|
1990
2071
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
1991
2072
|
|
|
1992
2073
|
try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
|
|
@@ -2007,7 +2088,8 @@ pub const Effects = struct {
|
|
|
2007
2088
|
assert(!side.active.volatiles.MultiHit);
|
|
2008
2089
|
side.active.volatiles.MultiHit = true;
|
|
2009
2090
|
|
|
2010
|
-
side.active.volatiles.attacks =
|
|
2091
|
+
side.active.volatiles.attacks =
|
|
2092
|
+
if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
|
|
2011
2093
|
}
|
|
2012
2094
|
|
|
2013
2095
|
fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2051,14 +2133,8 @@ pub const Effects = struct {
|
|
|
2051
2133
|
}
|
|
2052
2134
|
|
|
2053
2135
|
// Body Slam can't paralyze a Normal type Pokémon
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
else
|
|
2057
|
-
battle.rng.next() < 1 + (if (move.effect == .ParalyzeChance1)
|
|
2058
|
-
Gen12.percent(10)
|
|
2059
|
-
else
|
|
2060
|
-
Gen12.percent(30));
|
|
2061
|
-
if (!chance) return;
|
|
2136
|
+
if (foe.active.types.includes(move.type)) return;
|
|
2137
|
+
if (!Rolls.secondaryChance(battle, move.effect == .ParalyzeChance1)) return;
|
|
2062
2138
|
|
|
2063
2139
|
foe_stored.status = Status.init(.PAR);
|
|
2064
2140
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
@@ -2101,16 +2177,7 @@ pub const Effects = struct {
|
|
|
2101
2177
|
|
|
2102
2178
|
if (move.effect == .Poison) {
|
|
2103
2179
|
if (!showdown and !try checkHit(battle, player, move, log)) return;
|
|
2104
|
-
} else
|
|
2105
|
-
const chance = if (showdown)
|
|
2106
|
-
battle.rng.chance(u8, @as(u8, if (move.effect == .PoisonChance1) 52 else 103), 256)
|
|
2107
|
-
else
|
|
2108
|
-
battle.rng.next() < 1 + (if (move.effect == .PoisonChance1)
|
|
2109
|
-
Gen12.percent(20)
|
|
2110
|
-
else
|
|
2111
|
-
Gen12.percent(40));
|
|
2112
|
-
if (!chance) return;
|
|
2113
|
-
}
|
|
2180
|
+
} else if (!Rolls.poisonChance(battle, move.effect == .PoisonChance1)) return;
|
|
2114
2181
|
|
|
2115
2182
|
foe_stored.status = Status.init(.PSN);
|
|
2116
2183
|
if (toxic) {
|
|
@@ -2124,10 +2191,8 @@ pub const Effects = struct {
|
|
|
2124
2191
|
|
|
2125
2192
|
fn rage(battle: anytype, player: Player) !void {
|
|
2126
2193
|
var volatiles = &battle.side(player).active.volatiles;
|
|
2127
|
-
if (volatiles.Rage) return;
|
|
2128
|
-
volatiles.Rage = true;
|
|
2129
2194
|
assert(!volatiles.Bide);
|
|
2130
|
-
volatiles.
|
|
2195
|
+
volatiles.Rage = true;
|
|
2131
2196
|
}
|
|
2132
2197
|
|
|
2133
2198
|
fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
|
|
@@ -2179,16 +2244,7 @@ pub const Effects = struct {
|
|
|
2179
2244
|
}
|
|
2180
2245
|
foe.active.volatiles.Recharging = false;
|
|
2181
2246
|
|
|
2182
|
-
|
|
2183
|
-
battle.rng.range(u8, 1, 8)
|
|
2184
|
-
else loop: {
|
|
2185
|
-
while (true) {
|
|
2186
|
-
const r = battle.rng.next() & 7;
|
|
2187
|
-
if (r != 0) break :loop r;
|
|
2188
|
-
}
|
|
2189
|
-
});
|
|
2190
|
-
|
|
2191
|
-
foe_stored.status = Status.slp(duration);
|
|
2247
|
+
foe_stored.status = Status.slp(Rolls.sleepDuration(battle));
|
|
2192
2248
|
try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
|
|
2193
2249
|
}
|
|
2194
2250
|
|
|
@@ -2230,8 +2286,9 @@ pub const Effects = struct {
|
|
|
2230
2286
|
fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
2231
2287
|
if (!showdown or battle.side(player).last_selected_move == .Teleport) return;
|
|
2232
2288
|
|
|
2233
|
-
// 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
|
|
2234
2290
|
_ = try checkHit(battle, player, move, log);
|
|
2291
|
+
battle.last_damage = 0;
|
|
2235
2292
|
}
|
|
2236
2293
|
|
|
2237
2294
|
fn thrashing(battle: anytype, player: Player) void {
|
|
@@ -2240,11 +2297,7 @@ pub const Effects = struct {
|
|
|
2240
2297
|
assert(!volatiles.Bide);
|
|
2241
2298
|
|
|
2242
2299
|
volatiles.Thrashing = true;
|
|
2243
|
-
volatiles.
|
|
2244
|
-
volatiles.attacks = @intCast(u3, if (showdown)
|
|
2245
|
-
battle.rng.range(u8, 2, 4)
|
|
2246
|
-
else
|
|
2247
|
-
(battle.rng.next() & 1) + 2);
|
|
2300
|
+
volatiles.attacks = Rolls.bideThrashDuration(battle);
|
|
2248
2301
|
}
|
|
2249
2302
|
|
|
2250
2303
|
fn transform(battle: anytype, player: Player, log: anytype) !void {
|
|
@@ -2276,7 +2329,7 @@ pub const Effects = struct {
|
|
|
2276
2329
|
try log.transform(battle.active(player), foe_ident);
|
|
2277
2330
|
}
|
|
2278
2331
|
|
|
2279
|
-
fn binding(battle: anytype, player: Player) void {
|
|
2332
|
+
fn binding(battle: anytype, player: Player, rewrap: bool) void {
|
|
2280
2333
|
var side = battle.side(player);
|
|
2281
2334
|
var foe = battle.foe(player);
|
|
2282
2335
|
|
|
@@ -2284,9 +2337,11 @@ pub const Effects = struct {
|
|
|
2284
2337
|
side.active.volatiles.Binding = true;
|
|
2285
2338
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2286
2339
|
// (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
|
|
2287
|
-
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);
|
|
2288
2343
|
|
|
2289
|
-
side.active.volatiles.attacks = distribution(battle) - 1;
|
|
2344
|
+
side.active.volatiles.attacks = Rolls.distribution(battle) - 1;
|
|
2290
2345
|
}
|
|
2291
2346
|
|
|
2292
2347
|
fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2313,9 +2368,11 @@ pub const Effects = struct {
|
|
|
2313
2368
|
return try log.fail(ident, .None);
|
|
2314
2369
|
}
|
|
2315
2370
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2316
|
-
const stat = unmodifiedStats(battle,
|
|
2371
|
+
const stat = unmodifiedStats(battle, side).atk;
|
|
2317
2372
|
stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2318
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;
|
|
2319
2376
|
},
|
|
2320
2377
|
.DefenseUp1, .DefenseUp2 => {
|
|
2321
2378
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
@@ -2331,7 +2388,7 @@ pub const Effects = struct {
|
|
|
2331
2388
|
return try log.fail(ident, .None);
|
|
2332
2389
|
}
|
|
2333
2390
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2334
|
-
const stat = unmodifiedStats(battle,
|
|
2391
|
+
const stat = unmodifiedStats(battle, side).def;
|
|
2335
2392
|
stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2336
2393
|
try log.boost(ident, .Defense, n);
|
|
2337
2394
|
},
|
|
@@ -2348,7 +2405,7 @@ pub const Effects = struct {
|
|
|
2348
2405
|
return try log.fail(ident, .None);
|
|
2349
2406
|
}
|
|
2350
2407
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2351
|
-
const stat = unmodifiedStats(battle,
|
|
2408
|
+
const stat = unmodifiedStats(battle, side).spe;
|
|
2352
2409
|
stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2353
2410
|
try log.boost(ident, .Speed, 2);
|
|
2354
2411
|
},
|
|
@@ -2368,7 +2425,7 @@ pub const Effects = struct {
|
|
|
2368
2425
|
return try log.fail(ident, .None);
|
|
2369
2426
|
}
|
|
2370
2427
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2371
|
-
const stat = unmodifiedStats(battle,
|
|
2428
|
+
const stat = unmodifiedStats(battle, side).spc;
|
|
2372
2429
|
stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2373
2430
|
try log.boost(ident, .SpecialAttack, n);
|
|
2374
2431
|
try log.boost(ident, .SpecialDefense, n);
|
|
@@ -2395,11 +2452,7 @@ pub const Effects = struct {
|
|
|
2395
2452
|
}
|
|
2396
2453
|
|
|
2397
2454
|
if (move.effect.isStatDownChance()) {
|
|
2398
|
-
|
|
2399
|
-
battle.rng.chance(u8, 85, 256)
|
|
2400
|
-
else
|
|
2401
|
-
battle.rng.next() < Gen12.percent(33) + 1;
|
|
2402
|
-
if (!chance or foe.active.volatiles.Invulnerable) return;
|
|
2455
|
+
if (!Rolls.unboost(battle) or foe.active.volatiles.Invulnerable) return;
|
|
2403
2456
|
} else if (!showdown and !try checkHit(battle, player, move, log)) {
|
|
2404
2457
|
return; // checkHit already checks for Invulnerable
|
|
2405
2458
|
}
|
|
@@ -2421,7 +2474,7 @@ pub const Effects = struct {
|
|
|
2421
2474
|
return try log.fail(foe_ident, .None);
|
|
2422
2475
|
}
|
|
2423
2476
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2424
|
-
const stat = unmodifiedStats(battle,
|
|
2477
|
+
const stat = unmodifiedStats(battle, foe).atk;
|
|
2425
2478
|
stats.atk = @max(1, stat * mod[0] / mod[1]);
|
|
2426
2479
|
try log.boost(foe_ident, .Attack, -1);
|
|
2427
2480
|
},
|
|
@@ -2439,7 +2492,7 @@ pub const Effects = struct {
|
|
|
2439
2492
|
return try log.fail(foe_ident, .None);
|
|
2440
2493
|
}
|
|
2441
2494
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2442
|
-
const stat = unmodifiedStats(battle,
|
|
2495
|
+
const stat = unmodifiedStats(battle, foe).def;
|
|
2443
2496
|
stats.def = @max(1, stat * mod[0] / mod[1]);
|
|
2444
2497
|
try log.boost(foe_ident, .Defense, -@as(i8, n));
|
|
2445
2498
|
},
|
|
@@ -2456,7 +2509,7 @@ pub const Effects = struct {
|
|
|
2456
2509
|
return try log.fail(foe_ident, .None);
|
|
2457
2510
|
}
|
|
2458
2511
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2459
|
-
const stat = unmodifiedStats(battle,
|
|
2512
|
+
const stat = unmodifiedStats(battle, foe).spe;
|
|
2460
2513
|
stats.spe = @max(1, stat * mod[0] / mod[1]);
|
|
2461
2514
|
try log.boost(foe_ident, .Speed, -1);
|
|
2462
2515
|
assert(boosts.spe >= -6);
|
|
@@ -2476,7 +2529,7 @@ pub const Effects = struct {
|
|
|
2476
2529
|
return try log.fail(foe_ident, .None);
|
|
2477
2530
|
}
|
|
2478
2531
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2479
|
-
const stat = unmodifiedStats(battle,
|
|
2532
|
+
const stat = unmodifiedStats(battle, foe).spc;
|
|
2480
2533
|
stats.spc = @max(1, stat * mod[0] / mod[1]);
|
|
2481
2534
|
try log.boost(foe_ident, .SpecialAttack, -1);
|
|
2482
2535
|
try log.boost(foe_ident, .SpecialDefense, -1);
|
|
@@ -2495,8 +2548,7 @@ pub const Effects = struct {
|
|
|
2495
2548
|
}
|
|
2496
2549
|
};
|
|
2497
2550
|
|
|
2498
|
-
fn unmodifiedStats(battle: anytype,
|
|
2499
|
-
const side = battle.side(who);
|
|
2551
|
+
fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
|
|
2500
2552
|
if (!side.active.volatiles.Transform) return &side.stored().stats;
|
|
2501
2553
|
const id = ID.from(side.active.volatiles.transform);
|
|
2502
2554
|
return &battle.side(id.player).pokemon[id.id - 1].stats;
|
|
@@ -2510,7 +2562,7 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
|
|
|
2510
2562
|
}
|
|
2511
2563
|
}
|
|
2512
2564
|
|
|
2513
|
-
fn isForced(active: ActivePokemon) bool {
|
|
2565
|
+
inline fn isForced(active: ActivePokemon) bool {
|
|
2514
2566
|
return active.volatiles.Recharging or active.volatiles.Rage or
|
|
2515
2567
|
active.volatiles.Thrashing or active.volatiles.Charging;
|
|
2516
2568
|
}
|
|
@@ -2562,68 +2614,140 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
|
|
|
2562
2614
|
}
|
|
2563
2615
|
}
|
|
2564
2616
|
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
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;
|
|
2573
2633
|
}
|
|
2574
2634
|
}
|
|
2575
|
-
return false;
|
|
2576
|
-
}
|
|
2577
2635
|
|
|
2578
|
-
fn
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2636
|
+
inline fn hit(battle: anytype, accuracy: u16) bool {
|
|
2637
|
+
if (showdown) return battle.rng.chance(u8, @intCast(u8, accuracy), 256);
|
|
2638
|
+
return battle.rng.next() < accuracy;
|
|
2639
|
+
}
|
|
2582
2640
|
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
else
|
|
2588
|
-
(battle.rng.next() & 3) + 2);
|
|
2641
|
+
inline fn confusionDuration(battle: anytype) u3 {
|
|
2642
|
+
if (showdown) return @intCast(u3, battle.rng.range(u8, 2, 6));
|
|
2643
|
+
return @intCast(u3, (battle.rng.next() & 3) + 2);
|
|
2644
|
+
}
|
|
2589
2645
|
|
|
2590
|
-
|
|
2591
|
-
|
|
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
|
+
}
|
|
2592
2650
|
|
|
2593
|
-
|
|
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
|
+
}
|
|
2594
2655
|
|
|
2595
|
-
fn
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
}
|
|
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
|
+
}
|
|
2600
2660
|
|
|
2601
|
-
fn
|
|
2602
|
-
|
|
2603
|
-
if (
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2661
|
+
inline fn secondaryChance(battle: anytype, low: bool) bool {
|
|
2662
|
+
if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 26 else 77), 256);
|
|
2663
|
+
return battle.rng.next() < 1 + (if (low) Gen12.percent(10) else Gen12.percent(30));
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2666
|
+
inline fn poisonChance(battle: anytype, low: bool) bool {
|
|
2667
|
+
if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 52 else 103), 256);
|
|
2668
|
+
return battle.rng.next() < 1 + (if (low) Gen12.percent(20) else Gen12.percent(40));
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2671
|
+
inline fn sleepDuration(battle: anytype) u3 {
|
|
2672
|
+
if (showdown) return @intCast(u3, battle.rng.range(u8, 1, 8));
|
|
2673
|
+
while (true) {
|
|
2674
|
+
const r = battle.rng.next() & 7;
|
|
2675
|
+
if (r != 0) return @intCast(u3, r);
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
inline fn bideThrashDuration(battle: anytype) u3 {
|
|
2680
|
+
if (showdown) return @intCast(u3, battle.rng.range(u4, 2, 4));
|
|
2681
|
+
return @intCast(u3, (battle.rng.next() & 1) + 2);
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
fn moveSlot(battle: anytype, moves: []MoveSlot, check_pp: u4) u4 {
|
|
2685
|
+
if (showdown) {
|
|
2686
|
+
if (check_pp == 0) {
|
|
2687
|
+
var i: usize = moves.len;
|
|
2688
|
+
while (i > 0) {
|
|
2689
|
+
i -= 1;
|
|
2690
|
+
if (moves[i].id != .None) {
|
|
2691
|
+
return battle.rng.range(u4, 0, @intCast(u4, i + 1)) + 1;
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
} else {
|
|
2695
|
+
var r = battle.rng.range(u4, 0, @intCast(u4, check_pp)) + 1;
|
|
2696
|
+
var i: usize = 0;
|
|
2697
|
+
while (i < moves.len and r > 0) : (i += 1) {
|
|
2698
|
+
if (moves[i].pp > 0) {
|
|
2699
|
+
r -= 1;
|
|
2700
|
+
if (r == 0) break;
|
|
2701
|
+
}
|
|
2616
2702
|
}
|
|
2703
|
+
return @intCast(u4, i + 1);
|
|
2617
2704
|
}
|
|
2618
|
-
|
|
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;
|
|
2619
2710
|
}
|
|
2620
2711
|
}
|
|
2621
2712
|
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
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);
|
|
2625
2716
|
}
|
|
2626
|
-
|
|
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
|
+
};
|
|
2627
2751
|
|
|
2628
2752
|
test "RNG agreement" {
|
|
2629
2753
|
if (!showdown) return;
|
|
@@ -2723,9 +2847,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
2723
2847
|
while (slot <= 4) : (slot += 1) {
|
|
2724
2848
|
const m = active.moves[slot - 1];
|
|
2725
2849
|
if (m.id == .None) break;
|
|
2726
|
-
if (m.id == side.last_selected_move) {
|
|
2727
|
-
// Pokémon Showdown displays Struggle if limited to Bide
|
|
2728
|
-
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;
|
|
2729
2855
|
out[n] = .{ .type = .Move, .data = s };
|
|
2730
2856
|
n += 1;
|
|
2731
2857
|
return n;
|