@pkmn/engine 0.1.0-dev.7f2014fa → 0.1.0-dev.7f37a47c
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 +24 -52
- package/build/pkg/data/layout.json +1 -1
- package/build/pkg/data.d.ts +2 -1
- package/build/pkg/data.js +5 -3
- package/build/pkg/data.js.map +1 -1
- package/build/pkg/gen1.d.ts +4 -5
- package/build/pkg/gen1.js +24 -35
- package/build/pkg/gen1.js.map +1 -1
- package/build/pkg/index.d.ts +13 -16
- package/build/pkg/index.js +1 -1
- package/build/pkg/index.js.map +1 -1
- package/build/pkg/protocol.js +1 -1
- package/build/pkg/protocol.js.map +1 -1
- package/build.zig +34 -24
- package/package.json +30 -41
- package/src/bin/install-pkmn-engine +33 -60
- package/src/lib/binding/node.zig +5 -5
- package/src/lib/binding/wasm.zig +3 -3
- package/src/lib/common/debug.zig +0 -1
- package/src/lib/common/options.zig +3 -3
- package/src/lib/common/protocol.zig +38 -38
- package/src/lib/gen1/README.md +78 -109
- package/src/lib/gen1/data.zig +18 -20
- package/src/lib/gen1/helpers.zig +17 -11
- package/src/lib/gen1/mechanics.zig +334 -477
- package/src/lib/gen2/data.zig +4 -4
- package/src/lib/pkmn.zig +7 -7
- package/build/pkg/addon.d.ts +0 -7
- package/build/pkg/addon.js +0 -111
- package/build/pkg/addon.js.map +0 -1
- package/build/test/display.js +0 -400
- package/build/test/display.js.map +0 -1
- package/build/tools/debug.js +0 -133
- package/build/tools/debug.js.map +0 -1
- package/src/bin/pkmn-debug +0 -62
- package/src/test/display/pkmn.html.tmpl +0 -368
|
@@ -56,34 +56,30 @@ 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
|
-
|
|
61
59
|
pub fn update(battle: anytype, c1: Choice, c2: Choice, log: anytype) !Result {
|
|
62
60
|
assert(c1.type != .Pass or c2.type != .Pass or battle.turn == 0);
|
|
63
61
|
if (battle.turn == 0) return start(battle, log);
|
|
64
62
|
|
|
65
|
-
var s1 = false;
|
|
66
|
-
var s2 = false;
|
|
67
63
|
var f1: ?Move = null;
|
|
68
64
|
var f2: ?Move = null;
|
|
69
65
|
|
|
70
|
-
if (selectMove(battle, .P1, c1, c2, &
|
|
71
|
-
if (selectMove(battle, .P2, c2, c1, &
|
|
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;
|
|
66
|
+
if (selectMove(battle, .P1, c1, c2, &f1)) |r| return r;
|
|
67
|
+
if (selectMove(battle, .P2, c2, c1, &f2)) |r| return r;
|
|
78
68
|
|
|
79
69
|
if (turnOrder(battle, c1, c2) == .P1) {
|
|
80
|
-
if (try doTurn(battle, .P1, c1,
|
|
70
|
+
if (try doTurn(battle, .P1, c1, f1, .P2, c2, f2, log)) |r| return r;
|
|
81
71
|
} else {
|
|
82
|
-
if (try doTurn(battle, .P2, c2,
|
|
72
|
+
if (try doTurn(battle, .P2, c2, f2, .P1, c1, f1, log)) |r| return r;
|
|
83
73
|
}
|
|
84
74
|
|
|
85
|
-
|
|
86
|
-
if (
|
|
75
|
+
var p1 = battle.side(.P1);
|
|
76
|
+
if (p1.active.volatiles.attacks == 0) {
|
|
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
|
+
}
|
|
87
83
|
|
|
88
84
|
return endTurn(battle, log);
|
|
89
85
|
}
|
|
@@ -116,7 +112,6 @@ fn selectMove(
|
|
|
116
112
|
player: Player,
|
|
117
113
|
choice: Choice,
|
|
118
114
|
foe_choice: Choice,
|
|
119
|
-
skip_turn: *bool,
|
|
120
115
|
from: *?Move,
|
|
121
116
|
) ?Result {
|
|
122
117
|
if (choice.type == .Pass) return null;
|
|
@@ -129,26 +124,17 @@ fn selectMove(
|
|
|
129
124
|
(choice.type == .Move and choice.data == @boolToInt(showdown)));
|
|
130
125
|
|
|
131
126
|
// pre-battle menu
|
|
132
|
-
if (volatiles.Recharging)
|
|
133
|
-
if (showdown and battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
|
|
134
|
-
return null;
|
|
135
|
-
}
|
|
127
|
+
if (volatiles.Recharging) return null;
|
|
136
128
|
if (volatiles.Rage) {
|
|
137
129
|
from.* = side.last_used_move;
|
|
138
|
-
if (showdown)
|
|
139
|
-
if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
|
|
140
|
-
saveMove(battle, player, null);
|
|
141
|
-
}
|
|
130
|
+
if (showdown) saveMove(battle, player, null);
|
|
142
131
|
return null;
|
|
143
132
|
}
|
|
144
133
|
// Pokémon Showdown removes Flinch at the end-of-turn in its residual handler
|
|
145
134
|
if (!showdown) volatiles.Flinch = false;
|
|
146
135
|
if (volatiles.Thrashing or volatiles.Charging) {
|
|
147
136
|
from.* = side.last_selected_move;
|
|
148
|
-
if (showdown)
|
|
149
|
-
if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
|
|
150
|
-
saveMove(battle, player, null);
|
|
151
|
-
}
|
|
137
|
+
if (showdown) saveMove(battle, player, null);
|
|
152
138
|
return null;
|
|
153
139
|
}
|
|
154
140
|
|
|
@@ -158,10 +144,7 @@ fn selectMove(
|
|
|
158
144
|
// pre-move select
|
|
159
145
|
if (Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP) or volatiles.Bide) {
|
|
160
146
|
assert(showdown or choice.data == 0);
|
|
161
|
-
if (showdown)
|
|
162
|
-
if (volatiles.Bide and battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
|
|
163
|
-
saveMove(battle, player, choice);
|
|
164
|
-
}
|
|
147
|
+
if (showdown) saveMove(battle, player, choice);
|
|
165
148
|
return null;
|
|
166
149
|
}
|
|
167
150
|
if (volatiles.Binding) {
|
|
@@ -180,9 +163,9 @@ fn selectMove(
|
|
|
180
163
|
// GLITCH: https://glitchcity.wiki/Partial_trapping_move_Mirror_Move_link_battle_glitch
|
|
181
164
|
if (foe_choice.type == .Switch) {
|
|
182
165
|
const slot = if (player == .P1)
|
|
183
|
-
battle.
|
|
166
|
+
battle.last_selected_indexes.p1
|
|
184
167
|
else
|
|
185
|
-
battle.
|
|
168
|
+
battle.last_selected_indexes.p2;
|
|
186
169
|
const last = side.active.move(@intCast(u4, slot));
|
|
187
170
|
if (last.id == .Metronome) side.last_selected_move = last.id;
|
|
188
171
|
if (last.id == .MirrorMove) return Result.Error;
|
|
@@ -192,7 +175,6 @@ fn selectMove(
|
|
|
192
175
|
}
|
|
193
176
|
|
|
194
177
|
if (battle.foe(player).active.volatiles.Binding) {
|
|
195
|
-
skip_turn.* = true;
|
|
196
178
|
if (showdown) {
|
|
197
179
|
saveMove(battle, player, choice);
|
|
198
180
|
} else {
|
|
@@ -203,7 +185,6 @@ fn selectMove(
|
|
|
203
185
|
}
|
|
204
186
|
|
|
205
187
|
// move select
|
|
206
|
-
volatiles.state = 0;
|
|
207
188
|
if (choice.data == 0) {
|
|
208
189
|
const struggle = ok: {
|
|
209
190
|
for (side.active.moves, 0..) |move, i| {
|
|
@@ -237,9 +218,9 @@ fn saveMove(battle: anytype, player: Player, choice: ?Choice) void {
|
|
|
237
218
|
|
|
238
219
|
side.last_selected_move = move.id;
|
|
239
220
|
if (player == .P1) {
|
|
240
|
-
battle.
|
|
221
|
+
battle.last_selected_indexes.p1 = @intCast(u4, c.data);
|
|
241
222
|
} else {
|
|
242
|
-
battle.
|
|
223
|
+
battle.last_selected_indexes.p2 = @intCast(u4, c.data);
|
|
243
224
|
}
|
|
244
225
|
}
|
|
245
226
|
}
|
|
@@ -259,9 +240,9 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, log: anyty
|
|
|
259
240
|
side.order[slot - 1] = out;
|
|
260
241
|
|
|
261
242
|
if (player == .P1) {
|
|
262
|
-
battle.
|
|
243
|
+
battle.last_selected_indexes.p1 = 1;
|
|
263
244
|
} else {
|
|
264
|
-
battle.
|
|
245
|
+
battle.last_selected_indexes.p2 = 1;
|
|
265
246
|
}
|
|
266
247
|
|
|
267
248
|
side.last_used_move = .None;
|
|
@@ -320,7 +301,11 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
|
|
|
320
301
|
// Pokémon Showdown's beforeTurnCallback shenanigans
|
|
321
302
|
if (showdown and m1 == .Counter and m2 == .Counter) battle.rng.advance(1);
|
|
322
303
|
|
|
323
|
-
const p1 =
|
|
304
|
+
const p1 = if (showdown)
|
|
305
|
+
battle.rng.range(u8, 0, 2) == 0
|
|
306
|
+
else
|
|
307
|
+
battle.rng.next() < Gen12.percent(50) + 1;
|
|
308
|
+
|
|
324
309
|
if (!showdown) return if (p1) .P1 else .P2;
|
|
325
310
|
|
|
326
311
|
// Pokémon Showdown's "lockedmove" volatile's onBeforeTurn uses BattleQueue#changeAction,
|
|
@@ -330,7 +315,7 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
|
|
|
330
315
|
const t1 = battle.side(.P1).active.volatiles.Thrashing;
|
|
331
316
|
const t2 = battle.side(.P2).active.volatiles.Thrashing;
|
|
332
317
|
// If *both* sides are thrashing it really should be another speed tie, but we've patched
|
|
333
|
-
// that out and enforce host ordering of events, so P1 just goes first
|
|
318
|
+
// that out and enforce host ordering of events, so P1 just goes first reqgardless of who
|
|
334
319
|
// won the original coin flip
|
|
335
320
|
if (t1 and t2) return .P1;
|
|
336
321
|
return if (p1) if (t1 and !t2) .P2 else .P1 else if (t2 and !t1) .P1 else .P2;
|
|
@@ -343,13 +328,9 @@ fn doTurn(
|
|
|
343
328
|
battle: anytype,
|
|
344
329
|
player: Player,
|
|
345
330
|
player_choice: Choice,
|
|
346
|
-
player_rewrap: bool,
|
|
347
|
-
player_skip: bool,
|
|
348
331
|
player_from: ?Move,
|
|
349
332
|
foe_player: Player,
|
|
350
333
|
foe_choice: Choice,
|
|
351
|
-
foe_rewrap: bool,
|
|
352
|
-
foe_skip: bool,
|
|
353
334
|
foe_from: ?Move,
|
|
354
335
|
log: anytype,
|
|
355
336
|
) !?Result {
|
|
@@ -357,16 +338,7 @@ fn doTurn(
|
|
|
357
338
|
|
|
358
339
|
var residual = true;
|
|
359
340
|
var replace = battle.side(player).stored().hp == 0;
|
|
360
|
-
if (try executeMove(
|
|
361
|
-
battle,
|
|
362
|
-
player,
|
|
363
|
-
player_choice,
|
|
364
|
-
player_rewrap,
|
|
365
|
-
player_skip,
|
|
366
|
-
player_from,
|
|
367
|
-
&residual,
|
|
368
|
-
log,
|
|
369
|
-
)) |r| return r;
|
|
341
|
+
if (try executeMove(battle, player, player_choice, player_from, &residual, log)) |r| return r;
|
|
370
342
|
if (!replace) {
|
|
371
343
|
if (player_choice.type != .Switch) {
|
|
372
344
|
if (try checkFaint(battle, foe_player, log)) |r| return r;
|
|
@@ -375,18 +347,8 @@ fn doTurn(
|
|
|
375
347
|
if (try checkFaint(battle, player, log)) |r| return r;
|
|
376
348
|
} else if (foe_choice.type == .Pass) return null;
|
|
377
349
|
|
|
378
|
-
residual = true;
|
|
379
350
|
replace = battle.side(foe_player).stored().hp == 0;
|
|
380
|
-
if (try executeMove(
|
|
381
|
-
battle,
|
|
382
|
-
foe_player,
|
|
383
|
-
foe_choice,
|
|
384
|
-
foe_rewrap,
|
|
385
|
-
foe_skip,
|
|
386
|
-
foe_from,
|
|
387
|
-
&residual,
|
|
388
|
-
log,
|
|
389
|
-
)) |r| return r;
|
|
351
|
+
if (try executeMove(battle, foe_player, foe_choice, foe_from, &residual, log)) |r| return r;
|
|
390
352
|
if (!replace) {
|
|
391
353
|
if (foe_choice.type != .Switch) {
|
|
392
354
|
if (try checkFaint(battle, player, log)) |r| return r;
|
|
@@ -409,8 +371,6 @@ fn executeMove(
|
|
|
409
371
|
battle: anytype,
|
|
410
372
|
player: Player,
|
|
411
373
|
choice: Choice,
|
|
412
|
-
rewrap: bool,
|
|
413
|
-
skip: bool,
|
|
414
374
|
from: ?Move,
|
|
415
375
|
residual: *bool,
|
|
416
376
|
log: anytype,
|
|
@@ -422,10 +382,9 @@ fn executeMove(
|
|
|
422
382
|
return null;
|
|
423
383
|
}
|
|
424
384
|
|
|
425
|
-
// This is the correct place to check for SKIP_TURN and abort early, however since Pokémon
|
|
426
|
-
// Showdown overwrites the SKIP_TURN sentinel with its botched move select we need to add an
|
|
427
|
-
// additional skip boolean to accomplish the same thing in the Binding check of BeforeMove
|
|
428
385
|
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
|
|
429
388
|
assert(!showdown);
|
|
430
389
|
if (battle.foe(player).active.volatiles.Binding) {
|
|
431
390
|
try log.cant(battle.active(player), .Bound);
|
|
@@ -444,9 +403,9 @@ fn executeMove(
|
|
|
444
403
|
// choice.data == 0 only happens with Struggle on Pokémon Showdown
|
|
445
404
|
assert(!showdown);
|
|
446
405
|
mslot = @intCast(u4, if (player == .P1)
|
|
447
|
-
battle.
|
|
406
|
+
battle.last_selected_indexes.p1
|
|
448
407
|
else
|
|
449
|
-
battle.
|
|
408
|
+
battle.last_selected_indexes.p2);
|
|
450
409
|
const stored = side.stored();
|
|
451
410
|
// GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
|
|
452
411
|
auto = side.last_selected_move == .HyperBeam or side.active.volatiles.Bide or
|
|
@@ -468,14 +427,14 @@ fn executeMove(
|
|
|
468
427
|
// Incorrect mslot with Pokémon Showdown choice semantics so we need to recover from index
|
|
469
428
|
assert(mslot == 1);
|
|
470
429
|
mslot = @intCast(u4, if (player == .P1)
|
|
471
|
-
battle.
|
|
430
|
+
battle.last_selected_indexes.p1
|
|
472
431
|
else
|
|
473
|
-
battle.
|
|
432
|
+
battle.last_selected_indexes.p2);
|
|
474
433
|
}
|
|
475
434
|
|
|
476
435
|
var skip_can = false;
|
|
477
436
|
var skip_pp = false;
|
|
478
|
-
switch (try beforeMove(battle, player,
|
|
437
|
+
switch (try beforeMove(battle, player, from, log)) {
|
|
479
438
|
.done => return null,
|
|
480
439
|
.skip_can => skip_can = true,
|
|
481
440
|
.skip_pp => skip_pp = true,
|
|
@@ -483,23 +442,14 @@ fn executeMove(
|
|
|
483
442
|
.err => return @as(?Result, Result.Error),
|
|
484
443
|
}
|
|
485
444
|
|
|
486
|
-
if (!skip_can and !try canMove(battle, player, mslot, auto, skip_pp, from,
|
|
487
|
-
return null;
|
|
488
|
-
}
|
|
445
|
+
if (!skip_can and !try canMove(battle, player, mslot, auto, skip_pp, from, log)) return null;
|
|
489
446
|
|
|
490
|
-
return doMove(battle, player, mslot,
|
|
447
|
+
return doMove(battle, player, mslot, auto, residual, log);
|
|
491
448
|
}
|
|
492
449
|
|
|
493
|
-
const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
|
|
450
|
+
const BeforeMove = union(enum) { done, skip_can, skip_pp, ok, err };
|
|
494
451
|
|
|
495
|
-
fn beforeMove(
|
|
496
|
-
battle: anytype,
|
|
497
|
-
player: Player,
|
|
498
|
-
skip: bool,
|
|
499
|
-
from: ?Move,
|
|
500
|
-
residual: *bool,
|
|
501
|
-
log: anytype,
|
|
502
|
-
) !BeforeMove {
|
|
452
|
+
fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !BeforeMove {
|
|
503
453
|
var side = battle.side(player);
|
|
504
454
|
const foe = battle.foe(player);
|
|
505
455
|
var active = &side.active;
|
|
@@ -527,15 +477,13 @@ fn beforeMove(
|
|
|
527
477
|
return .done;
|
|
528
478
|
}
|
|
529
479
|
|
|
530
|
-
if (
|
|
480
|
+
if (foe.active.volatiles.Binding) {
|
|
531
481
|
try log.cant(ident, .Bound);
|
|
532
482
|
return .done;
|
|
533
483
|
}
|
|
534
484
|
|
|
535
|
-
if (volatiles.Flinch) {
|
|
536
|
-
|
|
537
|
-
// Pokémon can sometimes flinch multiple times from the same original hit
|
|
538
|
-
if (!showdown) volatiles.Flinch = false;
|
|
485
|
+
if (!showdown and volatiles.Flinch) {
|
|
486
|
+
volatiles.Flinch = false;
|
|
539
487
|
try log.cant(ident, .Flinch);
|
|
540
488
|
return .done;
|
|
541
489
|
}
|
|
@@ -556,6 +504,13 @@ fn beforeMove(
|
|
|
556
504
|
// Pokémon Showdown's disable condition has a single onBeforeMove handler
|
|
557
505
|
if (showdown and try disabled(side, ident, log)) return .done;
|
|
558
506
|
|
|
507
|
+
// Pokémon Showdown checks for Flinch *after* instead of before
|
|
508
|
+
if (showdown and volatiles.Flinch) {
|
|
509
|
+
volatiles.Flinch = false;
|
|
510
|
+
try log.cant(ident, .Flinch);
|
|
511
|
+
return .done;
|
|
512
|
+
}
|
|
513
|
+
|
|
559
514
|
// This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
|
|
560
515
|
// before the side had a selected a move - we simply need to assume this leads to a desync
|
|
561
516
|
if (side.last_selected_move == .None) {
|
|
@@ -573,9 +528,14 @@ fn beforeMove(
|
|
|
573
528
|
} else {
|
|
574
529
|
try log.activate(ident, .Confusion);
|
|
575
530
|
|
|
576
|
-
if (
|
|
531
|
+
const confused = if (showdown)
|
|
532
|
+
!battle.rng.chance(u8, 128, 256)
|
|
533
|
+
else
|
|
534
|
+
battle.rng.next() >= Gen12.percent(50) + 1;
|
|
535
|
+
|
|
536
|
+
if (confused) {
|
|
577
537
|
assert(!volatiles.MultiHit);
|
|
578
|
-
|
|
538
|
+
volatiles.state = 0;
|
|
579
539
|
volatiles.Bide = false;
|
|
580
540
|
volatiles.Thrashing = false;
|
|
581
541
|
volatiles.MultiHit = false;
|
|
@@ -599,12 +559,8 @@ fn beforeMove(
|
|
|
599
559
|
player
|
|
600
560
|
else
|
|
601
561
|
player.foe();
|
|
602
|
-
|
|
603
|
-
const uncapped = battle.last_damage;
|
|
604
562
|
// Skipping adjustDamage / randomizeDamage / checkHit
|
|
605
563
|
_ = try applyDamage(battle, player, target, .Confusion, log);
|
|
606
|
-
// Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
|
|
607
|
-
if (showdown) battle.last_damage = uncapped;
|
|
608
564
|
|
|
609
565
|
return .done;
|
|
610
566
|
}
|
|
@@ -613,15 +569,22 @@ fn beforeMove(
|
|
|
613
569
|
|
|
614
570
|
if (!showdown and try disabled(side, ident, log)) return .done;
|
|
615
571
|
|
|
616
|
-
if (Status.is(stored.status, .PAR)
|
|
617
|
-
if (
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
572
|
+
if (Status.is(stored.status, .PAR)) {
|
|
573
|
+
const paralyzed = if (showdown)
|
|
574
|
+
battle.rng.chance(u8, 63, 256)
|
|
575
|
+
else
|
|
576
|
+
battle.rng.next() < Gen12.percent(25);
|
|
577
|
+
|
|
578
|
+
if (paralyzed) {
|
|
579
|
+
volatiles.state = 0;
|
|
580
|
+
volatiles.Bide = false;
|
|
581
|
+
volatiles.Thrashing = false;
|
|
582
|
+
volatiles.Charging = false;
|
|
583
|
+
volatiles.Binding = false;
|
|
584
|
+
// GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
|
|
585
|
+
try log.cant(ident, .Paralysis);
|
|
586
|
+
return .done;
|
|
587
|
+
}
|
|
625
588
|
}
|
|
626
589
|
|
|
627
590
|
if (volatiles.Bide) {
|
|
@@ -648,18 +611,7 @@ fn beforeMove(
|
|
|
648
611
|
return .done;
|
|
649
612
|
}
|
|
650
613
|
|
|
651
|
-
const sub = showdown and foe.active.volatiles.Substitute;
|
|
652
614
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
653
|
-
if (foe.stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
|
|
654
|
-
|
|
655
|
-
// For reasons passing understanding, Pokémon Showdown still inflicts residual damage to
|
|
656
|
-
// Bide's user even if the above damage has caused the foe to faint. It's simpler to always
|
|
657
|
-
// run residual here regardless of whether the foe fainted and opt-out of the default flow
|
|
658
|
-
if (showdown) {
|
|
659
|
-
residual.* = false;
|
|
660
|
-
try handleResidual(battle, player, log);
|
|
661
|
-
}
|
|
662
|
-
|
|
663
615
|
return .done;
|
|
664
616
|
}
|
|
665
617
|
|
|
@@ -671,13 +623,8 @@ fn beforeMove(
|
|
|
671
623
|
try log.start(battle.active(player), .ConfusionSilent);
|
|
672
624
|
}
|
|
673
625
|
try log.move(ident, side.last_selected_move, battle.active(player.foe()), from);
|
|
674
|
-
if (showdown) {
|
|
675
|
-
|
|
676
|
-
// used in useMove and doesn't have the notion of skipping canMove semantics
|
|
677
|
-
side.last_used_move = side.last_selected_move;
|
|
678
|
-
if (handleThrashing(battle, active)) {
|
|
679
|
-
try log.start(battle.active(player), .ConfusionSilent);
|
|
680
|
-
}
|
|
626
|
+
if (showdown and handleThrashing(battle, active)) {
|
|
627
|
+
try log.start(battle.active(player), .ConfusionSilent);
|
|
681
628
|
}
|
|
682
629
|
return .skip_can;
|
|
683
630
|
}
|
|
@@ -692,11 +639,7 @@ fn beforeMove(
|
|
|
692
639
|
side.last_selected_move,
|
|
693
640
|
);
|
|
694
641
|
if (showdown or battle.last_damage != 0) {
|
|
695
|
-
const sub = showdown and foe.active.volatiles.Substitute;
|
|
696
642
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
697
|
-
if (battle.foe(player).stored().hp > 0 and !sub) {
|
|
698
|
-
try buildRage(battle, player.foe(), log);
|
|
699
|
-
}
|
|
700
643
|
}
|
|
701
644
|
return .done;
|
|
702
645
|
}
|
|
@@ -711,7 +654,6 @@ fn canMove(
|
|
|
711
654
|
auto: bool,
|
|
712
655
|
skip_pp: bool,
|
|
713
656
|
from: ?Move,
|
|
714
|
-
residual: *bool,
|
|
715
657
|
log: anytype,
|
|
716
658
|
) !bool {
|
|
717
659
|
var side = battle.side(player);
|
|
@@ -745,21 +687,8 @@ fn canMove(
|
|
|
745
687
|
const f = if (from != null and from.? == .None) null else from;
|
|
746
688
|
try log.move(player_ident, side.last_selected_move, battle.active(target), f);
|
|
747
689
|
|
|
748
|
-
// The Counter desync is caused by the cartridge not calling GetCurrentMove until now, meaning
|
|
749
|
-
// in cases where an early return happens the data for a players last selected move does not get
|
|
750
|
-
// reloaded and HandleCounterMove actually bases its success/failure off of stale information.
|
|
751
|
-
// This boolean state we track here doesn't exist on the cartridge because it instead manifests
|
|
752
|
-
// as the desync results from actually having two separate battle states that subtly disagree.
|
|
753
|
-
const counterable = side.last_selected_move != .Counter and move.bp > 0 and
|
|
754
|
-
(move.type == .Normal or move.type == .Fighting);
|
|
755
|
-
if (player == .P1) {
|
|
756
|
-
battle.last_moves.p1_counterable = @boolToInt(counterable);
|
|
757
|
-
} else {
|
|
758
|
-
battle.last_moves.p2_counterable = @boolToInt(counterable);
|
|
759
|
-
}
|
|
760
|
-
|
|
761
690
|
if (move.effect.onBegin()) {
|
|
762
|
-
try onBegin(battle, player, move, mslot,
|
|
691
|
+
try onBegin(battle, player, move, mslot, log);
|
|
763
692
|
return false;
|
|
764
693
|
}
|
|
765
694
|
|
|
@@ -767,7 +696,7 @@ fn canMove(
|
|
|
767
696
|
Effects.thrashing(battle, player);
|
|
768
697
|
} else if (!showdown and move.effect == .Binding) {
|
|
769
698
|
// Pokémon Showdown handles this after hit/miss checks and damage calculation
|
|
770
|
-
Effects.binding(battle, player
|
|
699
|
+
Effects.binding(battle, player);
|
|
771
700
|
}
|
|
772
701
|
|
|
773
702
|
return true;
|
|
@@ -808,7 +737,6 @@ fn doMove(
|
|
|
808
737
|
battle: anytype,
|
|
809
738
|
player: Player,
|
|
810
739
|
mslot: u4,
|
|
811
|
-
rewrap: bool,
|
|
812
740
|
auto: bool,
|
|
813
741
|
residual: *bool,
|
|
814
742
|
log: anytype,
|
|
@@ -839,21 +767,16 @@ fn doMove(
|
|
|
839
767
|
(@enumToInt(move.type.effectiveness(foe.active.types.type1)) == 0 or
|
|
840
768
|
@enumToInt(move.type.effectiveness(foe.active.types.type2)) == 0);
|
|
841
769
|
if (immune and move.effect != .Binding) break :miss true;
|
|
842
|
-
if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe)
|
|
843
|
-
battle.last_damage = 0;
|
|
844
|
-
break :miss true;
|
|
845
|
-
}
|
|
770
|
+
if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) break :miss true;
|
|
846
771
|
break :miss move.target != .Self and !moveHit(battle, player, move, &immune, &mist);
|
|
847
772
|
};
|
|
848
773
|
assert(!immune or miss or (showdown and move.effect == .Binding));
|
|
849
774
|
|
|
850
|
-
var late = showdown and move.effect != .Explode;
|
|
851
775
|
const skip = status or immune;
|
|
852
776
|
if ((!showdown or (!skip or counter)) and !miss) blk: {
|
|
853
777
|
if (showdown and move.effect.isMulti()) {
|
|
854
778
|
Effects.multiHit(battle, player, move);
|
|
855
779
|
hits = side.active.volatiles.attacks;
|
|
856
|
-
late = false;
|
|
857
780
|
}
|
|
858
781
|
|
|
859
782
|
// Cartridge rolls for crit even for moves that can't crit (Counter/Metronome/status/OHKO)
|
|
@@ -887,7 +810,7 @@ fn doMove(
|
|
|
887
810
|
}
|
|
888
811
|
|
|
889
812
|
// Due to control flow shenanigans we need to clear last_damage for Pokémon Showdown
|
|
890
|
-
if (showdown and skip) battle.last_damage = 0;
|
|
813
|
+
if (showdown and skip and move.effect != .Bide) battle.last_damage = 0;
|
|
891
814
|
|
|
892
815
|
miss = if (showdown or skip)
|
|
893
816
|
miss
|
|
@@ -900,9 +823,9 @@ fn doMove(
|
|
|
900
823
|
|
|
901
824
|
if (!showdown or !miss) {
|
|
902
825
|
if (move.effect == .MirrorMove) {
|
|
903
|
-
return mirrorMove(battle, player, mslot,
|
|
826
|
+
return mirrorMove(battle, player, mslot, auto, residual, log);
|
|
904
827
|
} else if (move.effect == .Metronome) {
|
|
905
|
-
return metronome(battle, player, mslot,
|
|
828
|
+
return metronome(battle, player, mslot, auto, residual, log);
|
|
906
829
|
} else if (move.effect.onEnd()) {
|
|
907
830
|
try onEnd(battle, player, move, log);
|
|
908
831
|
return null;
|
|
@@ -917,7 +840,7 @@ fn doMove(
|
|
|
917
840
|
move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
|
|
918
841
|
if (ohko) {
|
|
919
842
|
try log.immune(foe_ident, .OHKO);
|
|
920
|
-
} else if (immune and !invulnerable
|
|
843
|
+
} else if (immune and !invulnerable) {
|
|
921
844
|
try log.immune(foe_ident, .None);
|
|
922
845
|
} else if (mist) {
|
|
923
846
|
if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
|
|
@@ -934,9 +857,15 @@ fn doMove(
|
|
|
934
857
|
if (showdown and side.stored().hp == 0) residual.* = false;
|
|
935
858
|
} else if (move.effect == .Explode) {
|
|
936
859
|
try Effects.explode(battle, player);
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
860
|
+
if (foe.active.volatiles.Rage and foe.active.boosts.atk < 6) {
|
|
861
|
+
try Effects.boost(battle, player.foe(), Move.get(.Rage), log);
|
|
862
|
+
}
|
|
863
|
+
} else if (showdown) {
|
|
864
|
+
if (move.effect == .Disable) {
|
|
865
|
+
if (foe.active.volatiles.Rage and foe.active.boosts.atk < 6) {
|
|
866
|
+
try Effects.boost(battle, player.foe(), Move.get(.Rage), log);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
940
869
|
}
|
|
941
870
|
return null;
|
|
942
871
|
}
|
|
@@ -949,9 +878,6 @@ fn doMove(
|
|
|
949
878
|
hits = side.active.volatiles.attacks;
|
|
950
879
|
}
|
|
951
880
|
|
|
952
|
-
// Pokémon Showdown only builds Rage for Disable/Explosion (hit/miss) when attacking into a sub
|
|
953
|
-
const sub = showdown and foe.active.volatiles.Substitute;
|
|
954
|
-
|
|
955
881
|
var nullified = false;
|
|
956
882
|
var hit: u4 = 0;
|
|
957
883
|
while (hit < hits) {
|
|
@@ -967,7 +893,9 @@ fn doMove(
|
|
|
967
893
|
if (hit == 0 and ohko) try log.ohko();
|
|
968
894
|
hit += 1;
|
|
969
895
|
if (foe.stored().hp == 0) break;
|
|
970
|
-
if (
|
|
896
|
+
if (foe.active.volatiles.Rage and foe.active.boosts.atk < 6) {
|
|
897
|
+
try Effects.boost(battle, player.foe(), Move.get(.Rage), log);
|
|
898
|
+
}
|
|
971
899
|
// If the substitute breaks during a multi-hit attack, the attack ends
|
|
972
900
|
if (nullified) break;
|
|
973
901
|
}
|
|
@@ -983,17 +911,11 @@ fn doMove(
|
|
|
983
911
|
} else if (showdown) {
|
|
984
912
|
// This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
|
|
985
913
|
if (move.effect == .Binding) {
|
|
986
|
-
Effects.binding(battle, player
|
|
914
|
+
Effects.binding(battle, player);
|
|
987
915
|
if (immune) {
|
|
988
916
|
battle.last_damage = 0;
|
|
989
|
-
assert(foe.stored().hp > 0);
|
|
990
917
|
// Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
|
|
991
|
-
|
|
992
|
-
try log.activate(battle.active(player.foe()), .Substitute);
|
|
993
|
-
} else {
|
|
994
|
-
try log.damage(battle.active(player.foe()), foe.stored(), .None);
|
|
995
|
-
try buildRage(battle, player.foe(), log);
|
|
996
|
-
}
|
|
918
|
+
try log.damage(battle.active(player.foe()), foe.stored(), .None);
|
|
997
919
|
return null;
|
|
998
920
|
}
|
|
999
921
|
}
|
|
@@ -1010,9 +932,6 @@ fn doMove(
|
|
|
1010
932
|
|
|
1011
933
|
if (foe.stored().hp == 0) return null;
|
|
1012
934
|
|
|
1013
|
-
// Pokémon Showdown builds Rage at the wrong time for non-MultiHit move
|
|
1014
|
-
if (late and !sub and move.effect != .Disable) try buildRage(battle, player.foe(), log);
|
|
1015
|
-
|
|
1016
935
|
if (!move.effect.isSpecial()) {
|
|
1017
936
|
// On the cartridge Rage is not considered to be "special" and thus gets executed for a
|
|
1018
937
|
// second time here (after being executed in the "always happens" block above) but that
|
|
@@ -1024,9 +943,7 @@ fn doMove(
|
|
|
1024
943
|
if (move.effect == .Twineedle) {
|
|
1025
944
|
if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), log);
|
|
1026
945
|
} else if (move.effect == .Disable) {
|
|
1027
|
-
|
|
1028
|
-
if (showdown) try buildRage(battle, player.foe(), log);
|
|
1029
|
-
return result;
|
|
946
|
+
return Effects.disable(battle, player, move, log);
|
|
1030
947
|
} else {
|
|
1031
948
|
try moveEffect(battle, player, move, log);
|
|
1032
949
|
}
|
|
@@ -1044,7 +961,8 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
|
|
|
1044
961
|
chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
|
|
1045
962
|
chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
|
|
1046
963
|
|
|
1047
|
-
return
|
|
964
|
+
if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
|
|
965
|
+
return std.math.rotl(u8, battle.rng.next(), 3) < chance;
|
|
1048
966
|
}
|
|
1049
967
|
|
|
1050
968
|
fn calcDamage(
|
|
@@ -1148,37 +1066,49 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
|
1148
1066
|
|
|
1149
1067
|
fn randomizeDamage(battle: anytype) void {
|
|
1150
1068
|
if (battle.last_damage <= 1) return;
|
|
1151
|
-
|
|
1069
|
+
|
|
1070
|
+
const random = if (showdown)
|
|
1071
|
+
battle.rng.range(u8, 217, 256)
|
|
1072
|
+
else loop: {
|
|
1073
|
+
while (true) {
|
|
1074
|
+
const r = std.math.rotr(u8, battle.rng.next(), 1);
|
|
1075
|
+
if (r >= 217) break :loop r;
|
|
1076
|
+
}
|
|
1077
|
+
};
|
|
1078
|
+
|
|
1152
1079
|
battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
|
|
1153
1080
|
}
|
|
1154
1081
|
|
|
1155
1082
|
fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
|
|
1156
1083
|
const side = battle.side(player);
|
|
1157
|
-
const foe = battle.foe(player);
|
|
1158
1084
|
|
|
1159
1085
|
if (!try checkHit(battle, player, move, log)) return null;
|
|
1160
1086
|
|
|
1161
1087
|
battle.last_damage = switch (side.last_selected_move) {
|
|
1162
|
-
.SuperFang => @max(foe.stored().hp / 2, 1),
|
|
1088
|
+
.SuperFang => @max(battle.foe(player).stored().hp / 2, 1),
|
|
1163
1089
|
.SeismicToss, .NightShade => side.stored().level,
|
|
1164
1090
|
.SonicBoom => 20,
|
|
1165
1091
|
.DragonRage => 40,
|
|
1166
1092
|
// GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
|
|
1167
1093
|
.Psywave => power: {
|
|
1168
1094
|
const max = @intCast(u8, @as(u16, side.stored().level) * 3 / 2);
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1095
|
+
if (showdown) {
|
|
1096
|
+
break :power battle.rng.range(u8, 0, max);
|
|
1097
|
+
} else {
|
|
1098
|
+
// GLITCH: Psywave infinite glitch loop
|
|
1099
|
+
if (max <= 1) return Result.Error;
|
|
1100
|
+
while (true) {
|
|
1101
|
+
const r = battle.rng.next();
|
|
1102
|
+
if (r < max) break :power r;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1172
1105
|
},
|
|
1173
1106
|
else => unreachable,
|
|
1174
1107
|
};
|
|
1175
1108
|
|
|
1176
1109
|
if (battle.last_damage == 0) return if (showdown) null else Result.Error;
|
|
1177
1110
|
|
|
1178
|
-
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1179
1111
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
1180
|
-
if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
|
|
1181
|
-
|
|
1182
1112
|
return null;
|
|
1183
1113
|
}
|
|
1184
1114
|
|
|
@@ -1190,17 +1120,19 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
|
|
|
1190
1120
|
return null;
|
|
1191
1121
|
}
|
|
1192
1122
|
|
|
1193
|
-
// Pretend
|
|
1123
|
+
// Pretend Counter was used as a stand-in when no move has been used to fail below with 0 BP
|
|
1124
|
+
const foe_last_used_move =
|
|
1125
|
+
Move.get(if (foe.last_used_move == .None) .Counter else foe.last_used_move);
|
|
1194
1126
|
const foe_last_selected_move =
|
|
1195
1127
|
Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
|
|
1196
|
-
.
|
|
1128
|
+
.Counter
|
|
1197
1129
|
else
|
|
1198
1130
|
foe.last_selected_move);
|
|
1199
1131
|
|
|
1200
|
-
const used =
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1132
|
+
const used = foe_last_used_move.bp > 0 and
|
|
1133
|
+
foe.last_used_move != .Counter and
|
|
1134
|
+
(foe_last_used_move.type == .Normal or
|
|
1135
|
+
foe_last_used_move.type == .Fighting);
|
|
1204
1136
|
|
|
1205
1137
|
const selected = foe_last_selected_move.bp > 0 and
|
|
1206
1138
|
foe.last_selected_move != .Counter and
|
|
@@ -1224,9 +1156,7 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
|
|
|
1224
1156
|
// Pokémon Showdown calls checkHit before Counter
|
|
1225
1157
|
if (!showdown and !try checkHit(battle, player, move, log)) return null;
|
|
1226
1158
|
|
|
1227
|
-
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1228
1159
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
1229
|
-
if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
|
|
1230
1160
|
return null;
|
|
1231
1161
|
}
|
|
1232
1162
|
|
|
@@ -1270,7 +1200,6 @@ fn mirrorMove(
|
|
|
1270
1200
|
battle: anytype,
|
|
1271
1201
|
player: Player,
|
|
1272
1202
|
mslot: u4,
|
|
1273
|
-
rewrap: bool,
|
|
1274
1203
|
auto: bool,
|
|
1275
1204
|
residual: *bool,
|
|
1276
1205
|
log: anytype,
|
|
@@ -1283,30 +1212,41 @@ fn mirrorMove(
|
|
|
1283
1212
|
if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
|
|
1284
1213
|
try log.fail(battle.active(player), .None);
|
|
1285
1214
|
return null;
|
|
1286
|
-
} else if (!showdown or foe.last_used_move != .Struggle) {
|
|
1287
|
-
incrementPP(side, mslot);
|
|
1288
1215
|
}
|
|
1289
1216
|
|
|
1290
|
-
|
|
1291
|
-
|
|
1217
|
+
incrementPP(side, mslot);
|
|
1218
|
+
|
|
1219
|
+
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, log)) return null;
|
|
1220
|
+
return doMove(battle, player, mslot, auto, residual, log);
|
|
1292
1221
|
}
|
|
1293
1222
|
|
|
1294
1223
|
fn metronome(
|
|
1295
1224
|
battle: anytype,
|
|
1296
1225
|
player: Player,
|
|
1297
1226
|
mslot: u4,
|
|
1298
|
-
rewrap: bool,
|
|
1299
1227
|
auto: bool,
|
|
1300
1228
|
residual: *bool,
|
|
1301
1229
|
log: anytype,
|
|
1302
1230
|
) !?Result {
|
|
1303
1231
|
var side = battle.side(player);
|
|
1304
1232
|
|
|
1305
|
-
side.last_selected_move =
|
|
1233
|
+
side.last_selected_move = if (showdown) blk: {
|
|
1234
|
+
const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
|
|
1235
|
+
const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
|
|
1236
|
+
break :blk @intToEnum(Move, r + mod);
|
|
1237
|
+
} else loop: {
|
|
1238
|
+
while (true) {
|
|
1239
|
+
const r = battle.rng.next();
|
|
1240
|
+
if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
|
|
1241
|
+
if (r >= @enumToInt(Move.Struggle)) continue;
|
|
1242
|
+
break :loop @intToEnum(Move, r);
|
|
1243
|
+
}
|
|
1244
|
+
};
|
|
1245
|
+
|
|
1306
1246
|
incrementPP(side, mslot);
|
|
1307
1247
|
|
|
1308
|
-
if (!try canMove(battle, player, mslot, auto, false, .Metronome,
|
|
1309
|
-
return doMove(battle, player, mslot,
|
|
1248
|
+
if (!try canMove(battle, player, mslot, auto, false, .Metronome, log)) return null;
|
|
1249
|
+
return doMove(battle, player, mslot, auto, residual, log);
|
|
1310
1250
|
}
|
|
1311
1251
|
|
|
1312
1252
|
fn checkHit(battle: anytype, player: Player, move: Move.Data, log: anytype) !bool {
|
|
@@ -1338,9 +1278,7 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
|
|
|
1338
1278
|
if (move.effect == .Swift) return true;
|
|
1339
1279
|
if (foe.active.volatiles.Invulnerable) break :miss true;
|
|
1340
1280
|
}
|
|
1341
|
-
if (move.effect == .DreamEater and
|
|
1342
|
-
(showdown and foe.active.volatiles.Substitute)))
|
|
1343
|
-
{
|
|
1281
|
+
if (move.effect == .DreamEater and !Status.is(foe.stored().status, .SLP)) {
|
|
1344
1282
|
immune.* = true;
|
|
1345
1283
|
if (showdown) return false;
|
|
1346
1284
|
break :miss true;
|
|
@@ -1360,24 +1298,23 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
|
|
|
1360
1298
|
}
|
|
1361
1299
|
|
|
1362
1300
|
// GLITCH: Thrash / Petal Dance / Rage get their accuracy overwritten on subsequent hits
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
var accuracy = if (overwritten) state else @as(u16, Gen12.percent(move.accuracy));
|
|
1301
|
+
var state = &side.active.volatiles.state;
|
|
1302
|
+
const overwritten = (move.effect == .Thrashing or move.effect == .Rage) and state.* > 0;
|
|
1303
|
+
assert(!overwritten or (0 < state.* and state.* <= 255 and !side.active.volatiles.Bide));
|
|
1304
|
+
var accuracy = if (overwritten) state.* else @as(u16, Gen12.percent(move.accuracy));
|
|
1369
1305
|
var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
|
|
1370
1306
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1371
1307
|
boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
|
|
1372
1308
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1373
1309
|
accuracy = @min(255, @max(1, accuracy));
|
|
1374
1310
|
|
|
1375
|
-
|
|
1376
|
-
if (showdown) overwrite = side.active.volatiles.Rage or side.active.volatiles.Thrashing;
|
|
1377
|
-
if (overwrite) side.active.volatiles.state = accuracy;
|
|
1311
|
+
if (move.effect == .Thrashing or move.effect == .Rage) state.* = accuracy;
|
|
1378
1312
|
|
|
1379
1313
|
// GLITCH: max accuracy is 255 so 1/256 chance of miss
|
|
1380
|
-
break :miss
|
|
1314
|
+
break :miss if (showdown)
|
|
1315
|
+
!battle.rng.chance(u8, @intCast(u8, accuracy), 256)
|
|
1316
|
+
else
|
|
1317
|
+
battle.rng.next() >= accuracy;
|
|
1381
1318
|
};
|
|
1382
1319
|
|
|
1383
1320
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1492,15 +1429,6 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
|
|
|
1492
1429
|
}
|
|
1493
1430
|
|
|
1494
1431
|
if (volatiles.LeechSeed) {
|
|
1495
|
-
var foe = battle.foe(player);
|
|
1496
|
-
var foe_stored = foe.stored();
|
|
1497
|
-
const foe_ident = battle.active(player.foe());
|
|
1498
|
-
|
|
1499
|
-
if (foe_stored.hp == 0) {
|
|
1500
|
-
assert(showdown);
|
|
1501
|
-
return;
|
|
1502
|
-
}
|
|
1503
|
-
|
|
1504
1432
|
var damage = @max(stored.stats.hp / 16, 1);
|
|
1505
1433
|
|
|
1506
1434
|
// GLITCH: Leech Seed + Toxic glitch
|
|
@@ -1512,6 +1440,10 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
|
|
|
1512
1440
|
const amount = @min(damage, stored.hp);
|
|
1513
1441
|
stored.hp -= amount;
|
|
1514
1442
|
|
|
1443
|
+
var foe = battle.foe(player);
|
|
1444
|
+
var foe_stored = foe.stored();
|
|
1445
|
+
const foe_ident = battle.active(player.foe());
|
|
1446
|
+
|
|
1515
1447
|
// As above, Pokémon Showdown uses damageOf but its not relevant
|
|
1516
1448
|
if (amount > 0) try log.damage(ident, stored, .LeechSeed);
|
|
1517
1449
|
|
|
@@ -1548,20 +1480,16 @@ fn endTurn(battle: anytype, log: anytype) @TypeOf(log).Error!Result {
|
|
|
1548
1480
|
|
|
1549
1481
|
fn checkEBC(battle: anytype) bool {
|
|
1550
1482
|
for (battle.sides, 0..) |side, i| {
|
|
1551
|
-
const foe = battle.sides[~@intCast(u1, i)];
|
|
1552
|
-
|
|
1553
1483
|
var foe_all_ghosts = true;
|
|
1554
1484
|
var foe_all_transform = true;
|
|
1555
|
-
for (foe.order, 0..) |id, j| {
|
|
1556
|
-
if (id == 0) break;
|
|
1557
|
-
const active = j == 0;
|
|
1558
|
-
const pokemon = foe.pokemon[id - 1];
|
|
1559
1485
|
|
|
1560
|
-
|
|
1561
|
-
|
|
1486
|
+
for (battle.sides[~@intCast(u1, i)].pokemon) |pokemon| {
|
|
1487
|
+
if (pokemon.species == .None) continue;
|
|
1488
|
+
|
|
1489
|
+
const ghost = pokemon.hp == 0 or pokemon.types.includes(.Ghost);
|
|
1562
1490
|
foe_all_ghosts = foe_all_ghosts and ghost;
|
|
1563
1491
|
foe_all_transform = foe_all_transform and pokemon.hp == 0 or transform: {
|
|
1564
|
-
for (
|
|
1492
|
+
for (pokemon.moves) |m| {
|
|
1565
1493
|
if (m.id == .None) break :transform true;
|
|
1566
1494
|
if (m.id != .Transform) break :transform false;
|
|
1567
1495
|
}
|
|
@@ -1569,14 +1497,10 @@ fn checkEBC(battle: anytype) bool {
|
|
|
1569
1497
|
};
|
|
1570
1498
|
}
|
|
1571
1499
|
|
|
1572
|
-
for (side.
|
|
1573
|
-
if (id == 0) break;
|
|
1574
|
-
const active = j == 0;
|
|
1575
|
-
const pokemon = side.pokemon[id - 1];
|
|
1576
|
-
|
|
1500
|
+
for (side.pokemon) |pokemon| {
|
|
1577
1501
|
if (pokemon.hp == 0 or Status.is(pokemon.status, .FRZ)) continue;
|
|
1578
1502
|
const transform = foe_all_transform and transform: {
|
|
1579
|
-
for (
|
|
1503
|
+
for (pokemon.moves) |m| {
|
|
1580
1504
|
if (m.id == .None) break :transform true;
|
|
1581
1505
|
if (m.id != .Transform) break :transform false;
|
|
1582
1506
|
}
|
|
@@ -1584,7 +1508,7 @@ fn checkEBC(battle: anytype) bool {
|
|
|
1584
1508
|
};
|
|
1585
1509
|
if (transform) continue;
|
|
1586
1510
|
const no_pp = foe_all_ghosts and no_pp: {
|
|
1587
|
-
for (
|
|
1511
|
+
for (pokemon.moves) |m| {
|
|
1588
1512
|
if (m.pp != 0) break :no_pp false;
|
|
1589
1513
|
}
|
|
1590
1514
|
break :no_pp true;
|
|
@@ -1600,50 +1524,7 @@ fn checkEBC(battle: anytype) bool {
|
|
|
1600
1524
|
return false;
|
|
1601
1525
|
}
|
|
1602
1526
|
|
|
1603
|
-
fn
|
|
1604
|
-
if (side.active.volatiles.disabled_move != 0) {
|
|
1605
|
-
// A Pokémon that transforms after being disabled may end up with less move slots
|
|
1606
|
-
const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
|
|
1607
|
-
// side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
|
|
1608
|
-
const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
|
|
1609
|
-
m
|
|
1610
|
-
else
|
|
1611
|
-
side.last_selected_move;
|
|
1612
|
-
if (m != .None and m == last) {
|
|
1613
|
-
side.active.volatiles.Charging = false;
|
|
1614
|
-
try log.disabled(ident, last);
|
|
1615
|
-
return true;
|
|
1616
|
-
}
|
|
1617
|
-
}
|
|
1618
|
-
return false;
|
|
1619
|
-
}
|
|
1620
|
-
|
|
1621
|
-
inline fn buildRage(battle: anytype, who: Player, log: anytype) !void {
|
|
1622
|
-
const side = battle.side(who);
|
|
1623
|
-
if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
|
|
1624
|
-
try Effects.boost(battle, who, Move.get(.Rage), log);
|
|
1625
|
-
}
|
|
1626
|
-
}
|
|
1627
|
-
|
|
1628
|
-
fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
|
|
1629
|
-
var volatiles = &active.volatiles;
|
|
1630
|
-
assert(volatiles.Thrashing);
|
|
1631
|
-
if (volatiles.attacks > 0) return false;
|
|
1632
|
-
|
|
1633
|
-
volatiles.Thrashing = false;
|
|
1634
|
-
volatiles.Confusion = true;
|
|
1635
|
-
volatiles.confusion = Rolls.confusionDuration(battle);
|
|
1636
|
-
return true;
|
|
1637
|
-
}
|
|
1638
|
-
|
|
1639
|
-
inline fn onBegin(
|
|
1640
|
-
battle: anytype,
|
|
1641
|
-
player: Player,
|
|
1642
|
-
move: Move.Data,
|
|
1643
|
-
mslot: u8,
|
|
1644
|
-
residual: *bool,
|
|
1645
|
-
log: anytype,
|
|
1646
|
-
) !void {
|
|
1527
|
+
inline fn onBegin(battle: anytype, player: Player, move: Move.Data, mslot: u8, log: anytype) !void {
|
|
1647
1528
|
assert(move.effect.onBegin());
|
|
1648
1529
|
return switch (move.effect) {
|
|
1649
1530
|
.Confusion => Effects.confusion(battle, player, move, log),
|
|
@@ -1659,7 +1540,7 @@ inline fn onBegin(
|
|
|
1659
1540
|
.Poison => Effects.poison(battle, player, move, log),
|
|
1660
1541
|
.Reflect => Effects.reflect(battle, player, log),
|
|
1661
1542
|
.Splash => Effects.splash(battle, player, log),
|
|
1662
|
-
.Substitute => Effects.substitute(battle, player,
|
|
1543
|
+
.Substitute => Effects.substitute(battle, player, log),
|
|
1663
1544
|
.SwitchAndTeleport => Effects.switchAndTeleport(battle, player, move, log),
|
|
1664
1545
|
.Transform => Effects.transform(battle, player, log),
|
|
1665
1546
|
else => unreachable,
|
|
@@ -1726,7 +1607,10 @@ pub const Effects = struct {
|
|
|
1726
1607
|
side.active.volatiles.Bide = true;
|
|
1727
1608
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1728
1609
|
side.active.volatiles.state = 0;
|
|
1729
|
-
side.active.volatiles.attacks =
|
|
1610
|
+
side.active.volatiles.attacks = @intCast(u3, if (showdown)
|
|
1611
|
+
battle.rng.range(u4, 2, 4)
|
|
1612
|
+
else
|
|
1613
|
+
(battle.rng.next() & 1) + 2);
|
|
1730
1614
|
|
|
1731
1615
|
try log.start(battle.active(player), .Bide);
|
|
1732
1616
|
}
|
|
@@ -1748,8 +1632,14 @@ pub const Effects = struct {
|
|
|
1748
1632
|
return;
|
|
1749
1633
|
}
|
|
1750
1634
|
|
|
1751
|
-
|
|
1752
|
-
|
|
1635
|
+
const chance = !foe.active.types.includes(move.type) and if (showdown)
|
|
1636
|
+
battle.rng.chance(u8, @as(u8, if (move.effect == .BurnChance1) 26 else 77), 256)
|
|
1637
|
+
else
|
|
1638
|
+
battle.rng.next() < 1 + (if (move.effect == .BurnChance1)
|
|
1639
|
+
Gen12.percent(10)
|
|
1640
|
+
else
|
|
1641
|
+
Gen12.percent(30));
|
|
1642
|
+
if (!chance) return;
|
|
1753
1643
|
|
|
1754
1644
|
foe_stored.status = Status.init(.BRN);
|
|
1755
1645
|
foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
|
|
@@ -1773,7 +1663,11 @@ pub const Effects = struct {
|
|
|
1773
1663
|
const sub = foe.active.volatiles.Substitute;
|
|
1774
1664
|
|
|
1775
1665
|
if (move.effect == .ConfusionChance) {
|
|
1776
|
-
if (
|
|
1666
|
+
const chance = if (showdown)
|
|
1667
|
+
battle.rng.chance(u8, 25, 256)
|
|
1668
|
+
else
|
|
1669
|
+
battle.rng.next() < Gen12.percent(10);
|
|
1670
|
+
if (!chance) return;
|
|
1777
1671
|
} else {
|
|
1778
1672
|
if (showdown) {
|
|
1779
1673
|
if (!try checkHit(battle, player, move, log)) {
|
|
@@ -1792,7 +1686,10 @@ pub const Effects = struct {
|
|
|
1792
1686
|
|
|
1793
1687
|
if (foe.active.volatiles.Confusion) return;
|
|
1794
1688
|
foe.active.volatiles.Confusion = true;
|
|
1795
|
-
foe.active.volatiles.confusion =
|
|
1689
|
+
foe.active.volatiles.confusion = @intCast(u3, if (showdown)
|
|
1690
|
+
battle.rng.range(u8, 2, 6)
|
|
1691
|
+
else
|
|
1692
|
+
(battle.rng.next() & 3) + 2);
|
|
1796
1693
|
|
|
1797
1694
|
try log.start(battle.active(player.foe()), .Confusion);
|
|
1798
1695
|
}
|
|
@@ -1842,8 +1739,11 @@ pub const Effects = struct {
|
|
|
1842
1739
|
return Result.Error;
|
|
1843
1740
|
}
|
|
1844
1741
|
|
|
1845
|
-
volatiles.disabled_move = @intCast(u3,
|
|
1846
|
-
volatiles.disabled_duration =
|
|
1742
|
+
volatiles.disabled_move = @intCast(u3, randomMoveSlot(&battle.rng, &foe.active.moves, n));
|
|
1743
|
+
volatiles.disabled_duration = @intCast(u4, if (showdown)
|
|
1744
|
+
battle.rng.range(u8, 1, 9)
|
|
1745
|
+
else
|
|
1746
|
+
(battle.rng.next() & 7) + 1);
|
|
1847
1747
|
|
|
1848
1748
|
try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
|
|
1849
1749
|
return null;
|
|
@@ -1853,11 +1753,6 @@ pub const Effects = struct {
|
|
|
1853
1753
|
var side = battle.side(player);
|
|
1854
1754
|
var stored = side.stored();
|
|
1855
1755
|
|
|
1856
|
-
if (battle.last_damage == 0) {
|
|
1857
|
-
assert(showdown);
|
|
1858
|
-
if (!battle.foe(player).active.volatiles.Substitute) return;
|
|
1859
|
-
}
|
|
1860
|
-
|
|
1861
1756
|
const drain = @max(battle.last_damage / 2, 1);
|
|
1862
1757
|
battle.last_damage = drain;
|
|
1863
1758
|
|
|
@@ -1882,7 +1777,15 @@ pub const Effects = struct {
|
|
|
1882
1777
|
var volatiles = &battle.foe(player).active.volatiles;
|
|
1883
1778
|
|
|
1884
1779
|
if (volatiles.Substitute) return;
|
|
1885
|
-
|
|
1780
|
+
|
|
1781
|
+
const chance = if (showdown)
|
|
1782
|
+
battle.rng.chance(u8, @as(u8, if (move.effect == .FlinchChance1) 26 else 77), 256)
|
|
1783
|
+
else
|
|
1784
|
+
battle.rng.next() < 1 + (if (move.effect == .FlinchChance1)
|
|
1785
|
+
Gen12.percent(10)
|
|
1786
|
+
else
|
|
1787
|
+
Gen12.percent(30));
|
|
1788
|
+
if (!chance) return;
|
|
1886
1789
|
|
|
1887
1790
|
volatiles.Flinch = true;
|
|
1888
1791
|
volatiles.Recharging = false;
|
|
@@ -1908,8 +1811,12 @@ pub const Effects = struct {
|
|
|
1908
1811
|
return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
|
|
1909
1812
|
}
|
|
1910
1813
|
|
|
1911
|
-
|
|
1912
|
-
|
|
1814
|
+
const chance = !foe.active.types.includes(move.type) and if (showdown)
|
|
1815
|
+
battle.rng.chance(u8, 26, 256)
|
|
1816
|
+
else
|
|
1817
|
+
battle.rng.next() < 1 + Gen12.percent(10);
|
|
1818
|
+
if (!chance) return;
|
|
1819
|
+
|
|
1913
1820
|
// Freeze Clause Mod
|
|
1914
1821
|
if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
1915
1822
|
|
|
@@ -2046,15 +1953,15 @@ pub const Effects = struct {
|
|
|
2046
1953
|
}
|
|
2047
1954
|
break :has_mimic false;
|
|
2048
1955
|
};
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
1956
|
+
// If the foe is Invulnerable we still want to fall through to checkHit to be able to
|
|
1957
|
+
// trigger |-miss| instead of |-fail|
|
|
1958
|
+
if (!has_mimic and !foe.active.volatiles.Invulnerable) {
|
|
2052
1959
|
return try log.fail(battle.active(player.foe()), .None);
|
|
2053
1960
|
}
|
|
2054
1961
|
}
|
|
2055
1962
|
if (!try checkHit(battle, player, move, log)) return;
|
|
2056
1963
|
|
|
2057
|
-
const rslot =
|
|
1964
|
+
const rslot = randomMoveSlot(&battle.rng, &foe.active.moves, 0);
|
|
2058
1965
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
2059
1966
|
|
|
2060
1967
|
try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
|
|
@@ -2075,8 +1982,7 @@ pub const Effects = struct {
|
|
|
2075
1982
|
assert(!side.active.volatiles.MultiHit);
|
|
2076
1983
|
side.active.volatiles.MultiHit = true;
|
|
2077
1984
|
|
|
2078
|
-
side.active.volatiles.attacks =
|
|
2079
|
-
if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
|
|
1985
|
+
side.active.volatiles.attacks = if (move.effect == .MultiHit) distribution(battle) else 2;
|
|
2080
1986
|
}
|
|
2081
1987
|
|
|
2082
1988
|
fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2120,8 +2026,14 @@ pub const Effects = struct {
|
|
|
2120
2026
|
}
|
|
2121
2027
|
|
|
2122
2028
|
// Body Slam can't paralyze a Normal type Pokémon
|
|
2123
|
-
|
|
2124
|
-
|
|
2029
|
+
const chance = !foe.active.types.includes(move.type) and if (showdown)
|
|
2030
|
+
battle.rng.chance(u8, @as(u8, if (move.effect == .ParalyzeChance1) 26 else 77), 256)
|
|
2031
|
+
else
|
|
2032
|
+
battle.rng.next() < 1 + (if (move.effect == .ParalyzeChance1)
|
|
2033
|
+
Gen12.percent(10)
|
|
2034
|
+
else
|
|
2035
|
+
Gen12.percent(30));
|
|
2036
|
+
if (!chance) return;
|
|
2125
2037
|
|
|
2126
2038
|
foe_stored.status = Status.init(.PAR);
|
|
2127
2039
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
@@ -2151,7 +2063,7 @@ pub const Effects = struct {
|
|
|
2151
2063
|
return log.fail(foe_ident, if (Status.is(foe_stored.status, .PSN))
|
|
2152
2064
|
if (!showdown)
|
|
2153
2065
|
.Poison
|
|
2154
|
-
else if (toxic ==
|
|
2066
|
+
else if (toxic == foe.active.volatiles.Toxic)
|
|
2155
2067
|
if (toxic) .Toxic else .Poison
|
|
2156
2068
|
else
|
|
2157
2069
|
.None
|
|
@@ -2164,7 +2076,16 @@ pub const Effects = struct {
|
|
|
2164
2076
|
|
|
2165
2077
|
if (move.effect == .Poison) {
|
|
2166
2078
|
if (!showdown and !try checkHit(battle, player, move, log)) return;
|
|
2167
|
-
} else
|
|
2079
|
+
} else {
|
|
2080
|
+
const chance = if (showdown)
|
|
2081
|
+
battle.rng.chance(u8, @as(u8, if (move.effect == .PoisonChance1) 52 else 103), 256)
|
|
2082
|
+
else
|
|
2083
|
+
battle.rng.next() < 1 + (if (move.effect == .PoisonChance1)
|
|
2084
|
+
Gen12.percent(20)
|
|
2085
|
+
else
|
|
2086
|
+
Gen12.percent(40));
|
|
2087
|
+
if (!chance) return;
|
|
2088
|
+
}
|
|
2168
2089
|
|
|
2169
2090
|
foe_stored.status = Status.init(.PSN);
|
|
2170
2091
|
if (toxic) {
|
|
@@ -2178,8 +2099,10 @@ pub const Effects = struct {
|
|
|
2178
2099
|
|
|
2179
2100
|
fn rage(battle: anytype, player: Player) !void {
|
|
2180
2101
|
var volatiles = &battle.side(player).active.volatiles;
|
|
2181
|
-
|
|
2102
|
+
if (volatiles.Rage) return;
|
|
2182
2103
|
volatiles.Rage = true;
|
|
2104
|
+
assert(!volatiles.Bide);
|
|
2105
|
+
volatiles.state = 0;
|
|
2183
2106
|
}
|
|
2184
2107
|
|
|
2185
2108
|
fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
|
|
@@ -2231,7 +2154,16 @@ pub const Effects = struct {
|
|
|
2231
2154
|
}
|
|
2232
2155
|
foe.active.volatiles.Recharging = false;
|
|
2233
2156
|
|
|
2234
|
-
|
|
2157
|
+
const duration = @intCast(u3, if (showdown)
|
|
2158
|
+
battle.rng.range(u8, 1, 8)
|
|
2159
|
+
else loop: {
|
|
2160
|
+
while (true) {
|
|
2161
|
+
const r = battle.rng.next() & 7;
|
|
2162
|
+
if (r != 0) break :loop r;
|
|
2163
|
+
}
|
|
2164
|
+
});
|
|
2165
|
+
|
|
2166
|
+
foe_stored.status = Status.slp(duration);
|
|
2235
2167
|
try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
|
|
2236
2168
|
}
|
|
2237
2169
|
|
|
@@ -2239,7 +2171,7 @@ pub const Effects = struct {
|
|
|
2239
2171
|
try log.activate(battle.active(player), .Splash);
|
|
2240
2172
|
}
|
|
2241
2173
|
|
|
2242
|
-
fn substitute(battle: anytype, player: Player,
|
|
2174
|
+
fn substitute(battle: anytype, player: Player, log: anytype) !void {
|
|
2243
2175
|
var side = battle.side(player);
|
|
2244
2176
|
if (side.active.volatiles.Substitute) {
|
|
2245
2177
|
try log.fail(battle.active(player), .Substitute);
|
|
@@ -2264,18 +2196,14 @@ pub const Effects = struct {
|
|
|
2264
2196
|
side.active.volatiles.substitute = hp + 1;
|
|
2265
2197
|
side.active.volatiles.Substitute = true;
|
|
2266
2198
|
try log.start(battle.active(player), .Substitute);
|
|
2267
|
-
if (hp > 0)
|
|
2268
|
-
try log.damage(battle.active(player), side.stored(), .None);
|
|
2269
|
-
if (showdown and side.stored().hp == 0) residual.* = false;
|
|
2270
|
-
}
|
|
2199
|
+
if (hp > 0) try log.damage(battle.active(player), side.stored(), .None);
|
|
2271
2200
|
}
|
|
2272
2201
|
|
|
2273
2202
|
fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
2274
2203
|
if (!showdown or battle.side(player).last_selected_move == .Teleport) return;
|
|
2275
2204
|
|
|
2276
|
-
// Whirlwind/Roar should not roll to hit
|
|
2205
|
+
// Whirlwind/Roar should not roll to hit but Pokémon Showdown does anyway
|
|
2277
2206
|
_ = try checkHit(battle, player, move, log);
|
|
2278
|
-
battle.last_damage = 0;
|
|
2279
2207
|
}
|
|
2280
2208
|
|
|
2281
2209
|
fn thrashing(battle: anytype, player: Player) void {
|
|
@@ -2284,7 +2212,11 @@ pub const Effects = struct {
|
|
|
2284
2212
|
assert(!volatiles.Bide);
|
|
2285
2213
|
|
|
2286
2214
|
volatiles.Thrashing = true;
|
|
2287
|
-
volatiles.
|
|
2215
|
+
volatiles.state = 0;
|
|
2216
|
+
volatiles.attacks = @intCast(u3, if (showdown)
|
|
2217
|
+
battle.rng.range(u8, 2, 4)
|
|
2218
|
+
else
|
|
2219
|
+
(battle.rng.next() & 1) + 2);
|
|
2288
2220
|
}
|
|
2289
2221
|
|
|
2290
2222
|
fn transform(battle: anytype, player: Player, log: anytype) !void {
|
|
@@ -2316,7 +2248,7 @@ pub const Effects = struct {
|
|
|
2316
2248
|
try log.transform(battle.active(player), foe_ident);
|
|
2317
2249
|
}
|
|
2318
2250
|
|
|
2319
|
-
fn binding(battle: anytype, player: Player
|
|
2251
|
+
fn binding(battle: anytype, player: Player) void {
|
|
2320
2252
|
var side = battle.side(player);
|
|
2321
2253
|
var foe = battle.foe(player);
|
|
2322
2254
|
|
|
@@ -2324,11 +2256,9 @@ pub const Effects = struct {
|
|
|
2324
2256
|
side.active.volatiles.Binding = true;
|
|
2325
2257
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2326
2258
|
// (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
|
|
2327
|
-
if (!showdown)
|
|
2328
|
-
foe.active.volatiles.Recharging = false;
|
|
2329
|
-
} else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
|
|
2259
|
+
if (!showdown) foe.active.volatiles.Recharging = false;
|
|
2330
2260
|
|
|
2331
|
-
side.active.volatiles.attacks =
|
|
2261
|
+
side.active.volatiles.attacks = distribution(battle) - 1;
|
|
2332
2262
|
}
|
|
2333
2263
|
|
|
2334
2264
|
fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2355,11 +2285,9 @@ pub const Effects = struct {
|
|
|
2355
2285
|
return try log.fail(ident, .None);
|
|
2356
2286
|
}
|
|
2357
2287
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2358
|
-
const stat = unmodifiedStats(battle,
|
|
2288
|
+
const stat = unmodifiedStats(battle, player).atk;
|
|
2359
2289
|
stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2360
2290
|
try log.boost(ident, reason, n);
|
|
2361
|
-
// Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
|
|
2362
|
-
if (showdown and move.effect == .Rage) return;
|
|
2363
2291
|
},
|
|
2364
2292
|
.DefenseUp1, .DefenseUp2 => {
|
|
2365
2293
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
@@ -2375,7 +2303,7 @@ pub const Effects = struct {
|
|
|
2375
2303
|
return try log.fail(ident, .None);
|
|
2376
2304
|
}
|
|
2377
2305
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2378
|
-
const stat = unmodifiedStats(battle,
|
|
2306
|
+
const stat = unmodifiedStats(battle, player).def;
|
|
2379
2307
|
stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2380
2308
|
try log.boost(ident, .Defense, n);
|
|
2381
2309
|
},
|
|
@@ -2392,7 +2320,7 @@ pub const Effects = struct {
|
|
|
2392
2320
|
return try log.fail(ident, .None);
|
|
2393
2321
|
}
|
|
2394
2322
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2395
|
-
const stat = unmodifiedStats(battle,
|
|
2323
|
+
const stat = unmodifiedStats(battle, player).spe;
|
|
2396
2324
|
stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2397
2325
|
try log.boost(ident, .Speed, 2);
|
|
2398
2326
|
},
|
|
@@ -2412,7 +2340,7 @@ pub const Effects = struct {
|
|
|
2412
2340
|
return try log.fail(ident, .None);
|
|
2413
2341
|
}
|
|
2414
2342
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2415
|
-
const stat = unmodifiedStats(battle,
|
|
2343
|
+
const stat = unmodifiedStats(battle, player).spc;
|
|
2416
2344
|
stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2417
2345
|
try log.boost(ident, .SpecialAttack, n);
|
|
2418
2346
|
try log.boost(ident, .SpecialDefense, n);
|
|
@@ -2439,7 +2367,11 @@ pub const Effects = struct {
|
|
|
2439
2367
|
}
|
|
2440
2368
|
|
|
2441
2369
|
if (move.effect.isStatDownChance()) {
|
|
2442
|
-
if (
|
|
2370
|
+
const chance = if (showdown)
|
|
2371
|
+
battle.rng.chance(u8, 85, 256)
|
|
2372
|
+
else
|
|
2373
|
+
battle.rng.next() < Gen12.percent(33) + 1;
|
|
2374
|
+
if (!chance or foe.active.volatiles.Invulnerable) return;
|
|
2443
2375
|
} else if (!showdown and !try checkHit(battle, player, move, log)) {
|
|
2444
2376
|
return; // checkHit already checks for Invulnerable
|
|
2445
2377
|
}
|
|
@@ -2461,7 +2393,7 @@ pub const Effects = struct {
|
|
|
2461
2393
|
return try log.fail(foe_ident, .None);
|
|
2462
2394
|
}
|
|
2463
2395
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2464
|
-
const stat = unmodifiedStats(battle, foe).atk;
|
|
2396
|
+
const stat = unmodifiedStats(battle, player.foe()).atk;
|
|
2465
2397
|
stats.atk = @max(1, stat * mod[0] / mod[1]);
|
|
2466
2398
|
try log.boost(foe_ident, .Attack, -1);
|
|
2467
2399
|
},
|
|
@@ -2479,7 +2411,7 @@ pub const Effects = struct {
|
|
|
2479
2411
|
return try log.fail(foe_ident, .None);
|
|
2480
2412
|
}
|
|
2481
2413
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2482
|
-
const stat = unmodifiedStats(battle, foe).def;
|
|
2414
|
+
const stat = unmodifiedStats(battle, player.foe()).def;
|
|
2483
2415
|
stats.def = @max(1, stat * mod[0] / mod[1]);
|
|
2484
2416
|
try log.boost(foe_ident, .Defense, -@as(i8, n));
|
|
2485
2417
|
},
|
|
@@ -2496,7 +2428,7 @@ pub const Effects = struct {
|
|
|
2496
2428
|
return try log.fail(foe_ident, .None);
|
|
2497
2429
|
}
|
|
2498
2430
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2499
|
-
const stat = unmodifiedStats(battle, foe).spe;
|
|
2431
|
+
const stat = unmodifiedStats(battle, player.foe()).spe;
|
|
2500
2432
|
stats.spe = @max(1, stat * mod[0] / mod[1]);
|
|
2501
2433
|
try log.boost(foe_ident, .Speed, -1);
|
|
2502
2434
|
assert(boosts.spe >= -6);
|
|
@@ -2516,7 +2448,7 @@ pub const Effects = struct {
|
|
|
2516
2448
|
return try log.fail(foe_ident, .None);
|
|
2517
2449
|
}
|
|
2518
2450
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2519
|
-
const stat = unmodifiedStats(battle, foe).spc;
|
|
2451
|
+
const stat = unmodifiedStats(battle, player.foe()).spc;
|
|
2520
2452
|
stats.spc = @max(1, stat * mod[0] / mod[1]);
|
|
2521
2453
|
try log.boost(foe_ident, .SpecialAttack, -1);
|
|
2522
2454
|
try log.boost(foe_ident, .SpecialDefense, -1);
|
|
@@ -2535,7 +2467,8 @@ pub const Effects = struct {
|
|
|
2535
2467
|
}
|
|
2536
2468
|
};
|
|
2537
2469
|
|
|
2538
|
-
fn unmodifiedStats(battle: anytype,
|
|
2470
|
+
fn unmodifiedStats(battle: anytype, who: Player) *Stats(u16) {
|
|
2471
|
+
const side = battle.side(who);
|
|
2539
2472
|
if (!side.active.volatiles.Transform) return &side.stored().stats;
|
|
2540
2473
|
const id = ID.from(side.active.volatiles.transform);
|
|
2541
2474
|
return &battle.side(id.player).pokemon[id.id - 1].stats;
|
|
@@ -2549,7 +2482,7 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
|
|
|
2549
2482
|
}
|
|
2550
2483
|
}
|
|
2551
2484
|
|
|
2552
|
-
|
|
2485
|
+
fn isForced(active: ActivePokemon) bool {
|
|
2553
2486
|
return active.volatiles.Recharging or active.volatiles.Rage or
|
|
2554
2487
|
active.volatiles.Thrashing or active.volatiles.Charging;
|
|
2555
2488
|
}
|
|
@@ -2581,9 +2514,13 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
|
|
|
2581
2514
|
volatiles.LeechSeed = false;
|
|
2582
2515
|
try log.end(ident, .LeechSeed);
|
|
2583
2516
|
}
|
|
2584
|
-
if (
|
|
2517
|
+
if (volatiles.Toxic) {
|
|
2585
2518
|
volatiles.Toxic = false;
|
|
2586
|
-
// volatiles.toxic is left unchanged, except on Pokémon Showdown which clears it
|
|
2519
|
+
// volatiles.toxic is left unchanged, except on Pokémon Showdown which clears it
|
|
2520
|
+
if (showdown) {
|
|
2521
|
+
volatiles.toxic = 0;
|
|
2522
|
+
try log.end(ident, .Toxic);
|
|
2523
|
+
}
|
|
2587
2524
|
}
|
|
2588
2525
|
if (volatiles.LightScreen) {
|
|
2589
2526
|
volatiles.LightScreen = false;
|
|
@@ -2593,148 +2530,70 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
|
|
|
2593
2530
|
volatiles.Reflect = false;
|
|
2594
2531
|
try log.end(ident, .Reflect);
|
|
2595
2532
|
}
|
|
2596
|
-
if (showdown and volatiles.Toxic) {
|
|
2597
|
-
volatiles.Toxic = false;
|
|
2598
|
-
// Pokémon Showdown erroneously clears the toxic counter
|
|
2599
|
-
volatiles.toxic = 0;
|
|
2600
|
-
try log.end(ident, .Toxic);
|
|
2601
|
-
}
|
|
2602
2533
|
}
|
|
2603
2534
|
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
return std.math.rotl(u8, battle.rng.next(), 3) < chance;
|
|
2613
|
-
}
|
|
2614
|
-
|
|
2615
|
-
inline fn damage(battle: anytype) u8 {
|
|
2616
|
-
if (showdown) return battle.rng.range(u8, 217, 256);
|
|
2617
|
-
while (true) {
|
|
2618
|
-
const r = std.math.rotr(u8, battle.rng.next(), 1);
|
|
2619
|
-
if (r >= 217) return r;
|
|
2535
|
+
fn disabled(side: *Side, ident: ID, log: anytype) !bool {
|
|
2536
|
+
if (side.active.volatiles.disabled_move != 0) {
|
|
2537
|
+
// A Pokémon that transforms after being disabled may end up with less move slots
|
|
2538
|
+
const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
|
|
2539
|
+
if (m != .None and m == side.last_selected_move) {
|
|
2540
|
+
side.active.volatiles.Charging = false;
|
|
2541
|
+
try log.disabled(ident, side.last_selected_move);
|
|
2542
|
+
return true;
|
|
2620
2543
|
}
|
|
2621
2544
|
}
|
|
2545
|
+
return false;
|
|
2546
|
+
}
|
|
2622
2547
|
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
inline fn confusionDuration(battle: anytype) u3 {
|
|
2629
|
-
if (showdown) return @intCast(u3, battle.rng.range(u8, 2, 6));
|
|
2630
|
-
return @intCast(u3, (battle.rng.next() & 3) + 2);
|
|
2631
|
-
}
|
|
2632
|
-
|
|
2633
|
-
inline fn confused(battle: anytype) bool {
|
|
2634
|
-
if (showdown) return !battle.rng.chance(u8, 128, 256);
|
|
2635
|
-
return battle.rng.next() >= Gen12.percent(50) + 1;
|
|
2636
|
-
}
|
|
2637
|
-
|
|
2638
|
-
inline fn paralyzed(battle: anytype) bool {
|
|
2639
|
-
if (showdown) return battle.rng.chance(u8, 63, 256);
|
|
2640
|
-
return battle.rng.next() < Gen12.percent(25);
|
|
2641
|
-
}
|
|
2642
|
-
|
|
2643
|
-
inline fn confusionChance(battle: anytype) bool {
|
|
2644
|
-
if (showdown) return battle.rng.chance(u8, 25, 256);
|
|
2645
|
-
return battle.rng.next() < Gen12.percent(10);
|
|
2646
|
-
}
|
|
2548
|
+
fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
|
|
2549
|
+
var volatiles = &active.volatiles;
|
|
2550
|
+
assert(volatiles.Thrashing);
|
|
2551
|
+
if (volatiles.attacks > 0) return false;
|
|
2647
2552
|
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2553
|
+
volatiles.Thrashing = false;
|
|
2554
|
+
volatiles.Confusion = true;
|
|
2555
|
+
volatiles.confusion = @intCast(u3, if (showdown)
|
|
2556
|
+
battle.rng.range(u8, 2, 6)
|
|
2557
|
+
else
|
|
2558
|
+
(battle.rng.next() & 3) + 2);
|
|
2652
2559
|
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
return battle.rng.next() < 1 + (if (low) Gen12.percent(20) else Gen12.percent(40));
|
|
2656
|
-
}
|
|
2560
|
+
return true;
|
|
2561
|
+
}
|
|
2657
2562
|
|
|
2658
|
-
|
|
2659
|
-
if (showdown) return @intCast(u3, battle.rng.range(u8, 1, 8));
|
|
2660
|
-
while (true) {
|
|
2661
|
-
const r = battle.rng.next() & 7;
|
|
2662
|
-
if (r != 0) return @intCast(u3, r);
|
|
2663
|
-
}
|
|
2664
|
-
}
|
|
2563
|
+
const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
|
|
2665
2564
|
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2565
|
+
fn distribution(battle: anytype) u3 {
|
|
2566
|
+
if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
|
|
2567
|
+
const r = (battle.rng.next() & 3);
|
|
2568
|
+
return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
|
|
2569
|
+
}
|
|
2670
2570
|
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
if (
|
|
2686
|
-
r -= 1;
|
|
2687
|
-
if (r == 0) break;
|
|
2688
|
-
}
|
|
2571
|
+
fn randomMoveSlot(rand: anytype, moves: []MoveSlot, check_pp: u4) u4 {
|
|
2572
|
+
if (showdown) {
|
|
2573
|
+
if (check_pp == 0) {
|
|
2574
|
+
var i: usize = moves.len;
|
|
2575
|
+
while (i > 0) {
|
|
2576
|
+
i -= 1;
|
|
2577
|
+
if (moves[i].id != .None) return rand.range(u4, 0, @intCast(u4, i + 1)) + 1;
|
|
2578
|
+
}
|
|
2579
|
+
} else {
|
|
2580
|
+
var r = rand.range(u4, 0, @intCast(u4, check_pp)) + 1;
|
|
2581
|
+
var i: usize = 0;
|
|
2582
|
+
while (i < moves.len and r > 0) : (i += 1) {
|
|
2583
|
+
if (moves[i].pp > 0) {
|
|
2584
|
+
r -= 1;
|
|
2585
|
+
if (r == 0) break;
|
|
2689
2586
|
}
|
|
2690
|
-
return @intCast(u4, i + 1);
|
|
2691
2587
|
}
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
while (true) {
|
|
2695
|
-
const r = @intCast(u4, battle.rng.next() & 3);
|
|
2696
|
-
if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
|
|
2697
|
-
}
|
|
2698
|
-
}
|
|
2699
|
-
|
|
2700
|
-
inline fn disableDuration(battle: anytype) u4 {
|
|
2701
|
-
if (showdown) return @intCast(u4, battle.rng.range(u8, 1, 9));
|
|
2702
|
-
return @intCast(u4, (battle.rng.next() & 7) + 1);
|
|
2703
|
-
}
|
|
2704
|
-
|
|
2705
|
-
inline fn metronome(battle: anytype) Move {
|
|
2706
|
-
if (showdown) {
|
|
2707
|
-
const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
|
|
2708
|
-
const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
|
|
2709
|
-
return @intToEnum(Move, r + mod);
|
|
2710
|
-
}
|
|
2711
|
-
while (true) {
|
|
2712
|
-
const r = battle.rng.next();
|
|
2713
|
-
if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
|
|
2714
|
-
if (r >= @enumToInt(Move.Struggle)) continue;
|
|
2715
|
-
return @intToEnum(Move, r);
|
|
2588
|
+
return @intCast(u4, i + 1);
|
|
2716
2589
|
}
|
|
2717
2590
|
}
|
|
2718
2591
|
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
const r = battle.rng.next();
|
|
2723
|
-
if (r < max) return r;
|
|
2724
|
-
}
|
|
2725
|
-
}
|
|
2726
|
-
|
|
2727
|
-
inline fn distribution(battle: anytype) u3 {
|
|
2728
|
-
if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
|
|
2729
|
-
const r = (battle.rng.next() & 3);
|
|
2730
|
-
return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
|
|
2592
|
+
while (true) {
|
|
2593
|
+
const r = @intCast(u4, rand.next() & 3);
|
|
2594
|
+
if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
|
|
2731
2595
|
}
|
|
2732
|
-
|
|
2733
|
-
inline fn unboost(battle: anytype) bool {
|
|
2734
|
-
if (showdown) return battle.rng.chance(u8, 85, 256);
|
|
2735
|
-
return battle.rng.next() < Gen12.percent(33) + 1;
|
|
2736
|
-
}
|
|
2737
|
-
};
|
|
2596
|
+
}
|
|
2738
2597
|
|
|
2739
2598
|
test "RNG agreement" {
|
|
2740
2599
|
if (!showdown) return;
|
|
@@ -2834,11 +2693,9 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
2834
2693
|
while (slot <= 4) : (slot += 1) {
|
|
2835
2694
|
const m = active.moves[slot - 1];
|
|
2836
2695
|
if (m.id == .None) break;
|
|
2837
|
-
if (m.id ==
|
|
2838
|
-
// Pokémon Showdown displays Struggle if limited to Bide
|
|
2839
|
-
const
|
|
2840
|
-
m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);
|
|
2841
|
-
const s = if (struggle) 0 else slot;
|
|
2696
|
+
if (m.id == side.last_selected_move) {
|
|
2697
|
+
// Pokémon Showdown displays Struggle if limited to Bide when out of PP
|
|
2698
|
+
const s = if (m.pp == 0 and m.id == .Bide) 0 else slot;
|
|
2842
2699
|
out[n] = .{ .type = .Move, .data = s };
|
|
2843
2700
|
n += 1;
|
|
2844
2701
|
return n;
|