@pkmn/engine 0.1.0-dev.24867658 → 0.1.0-dev.2ec67dba
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 +22 -50
- 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 -43
- package/build/pkg/gen1.js.map +1 -1
- package/build/pkg/index.d.ts +12 -15
- 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 +84 -117
- package/src/lib/gen1/data.zig +17 -19
- package/src/lib/gen1/helpers.zig +18 -3
- package/src/lib/gen1/mechanics.zig +316 -442
- 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;
|
|
@@ -318,11 +299,13 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
|
|
|
318
299
|
const spe2 = battle.side(.P2).active.stats.spe;
|
|
319
300
|
if (spe1 == spe2) {
|
|
320
301
|
// Pokémon Showdown's beforeTurnCallback shenanigans
|
|
321
|
-
if (showdown and
|
|
322
|
-
|
|
323
|
-
|
|
302
|
+
if (showdown and m1 == .Counter and m2 == .Counter) battle.rng.advance(1);
|
|
303
|
+
|
|
304
|
+
const p1 = if (showdown)
|
|
305
|
+
battle.rng.range(u8, 0, 2) == 0
|
|
306
|
+
else
|
|
307
|
+
battle.rng.next() < Gen12.percent(50) + 1;
|
|
324
308
|
|
|
325
|
-
const p1 = Rolls.speedTie(battle);
|
|
326
309
|
if (!showdown) return if (p1) .P1 else .P2;
|
|
327
310
|
|
|
328
311
|
// Pokémon Showdown's "lockedmove" volatile's onBeforeTurn uses BattleQueue#changeAction,
|
|
@@ -332,7 +315,7 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
|
|
|
332
315
|
const t1 = battle.side(.P1).active.volatiles.Thrashing;
|
|
333
316
|
const t2 = battle.side(.P2).active.volatiles.Thrashing;
|
|
334
317
|
// If *both* sides are thrashing it really should be another speed tie, but we've patched
|
|
335
|
-
// 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
|
|
336
319
|
// won the original coin flip
|
|
337
320
|
if (t1 and t2) return .P1;
|
|
338
321
|
return if (p1) if (t1 and !t2) .P2 else .P1 else if (t2 and !t1) .P1 else .P2;
|
|
@@ -345,13 +328,9 @@ fn doTurn(
|
|
|
345
328
|
battle: anytype,
|
|
346
329
|
player: Player,
|
|
347
330
|
player_choice: Choice,
|
|
348
|
-
player_rewrap: bool,
|
|
349
|
-
player_skip: bool,
|
|
350
331
|
player_from: ?Move,
|
|
351
332
|
foe_player: Player,
|
|
352
333
|
foe_choice: Choice,
|
|
353
|
-
foe_rewrap: bool,
|
|
354
|
-
foe_skip: bool,
|
|
355
334
|
foe_from: ?Move,
|
|
356
335
|
log: anytype,
|
|
357
336
|
) !?Result {
|
|
@@ -359,16 +338,7 @@ fn doTurn(
|
|
|
359
338
|
|
|
360
339
|
var residual = true;
|
|
361
340
|
var replace = battle.side(player).stored().hp == 0;
|
|
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;
|
|
341
|
+
if (try executeMove(battle, player, player_choice, player_from, &residual, log)) |r| return r;
|
|
372
342
|
if (!replace) {
|
|
373
343
|
if (player_choice.type != .Switch) {
|
|
374
344
|
if (try checkFaint(battle, foe_player, log)) |r| return r;
|
|
@@ -377,18 +347,8 @@ fn doTurn(
|
|
|
377
347
|
if (try checkFaint(battle, player, log)) |r| return r;
|
|
378
348
|
} else if (foe_choice.type == .Pass) return null;
|
|
379
349
|
|
|
380
|
-
residual = true;
|
|
381
350
|
replace = battle.side(foe_player).stored().hp == 0;
|
|
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;
|
|
351
|
+
if (try executeMove(battle, foe_player, foe_choice, foe_from, &residual, log)) |r| return r;
|
|
392
352
|
if (!replace) {
|
|
393
353
|
if (foe_choice.type != .Switch) {
|
|
394
354
|
if (try checkFaint(battle, player, log)) |r| return r;
|
|
@@ -411,8 +371,6 @@ fn executeMove(
|
|
|
411
371
|
battle: anytype,
|
|
412
372
|
player: Player,
|
|
413
373
|
choice: Choice,
|
|
414
|
-
rewrap: bool,
|
|
415
|
-
skip: bool,
|
|
416
374
|
from: ?Move,
|
|
417
375
|
residual: *bool,
|
|
418
376
|
log: anytype,
|
|
@@ -424,10 +382,9 @@ fn executeMove(
|
|
|
424
382
|
return null;
|
|
425
383
|
}
|
|
426
384
|
|
|
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
|
|
430
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
|
|
431
388
|
assert(!showdown);
|
|
432
389
|
if (battle.foe(player).active.volatiles.Binding) {
|
|
433
390
|
try log.cant(battle.active(player), .Bound);
|
|
@@ -446,9 +403,9 @@ fn executeMove(
|
|
|
446
403
|
// choice.data == 0 only happens with Struggle on Pokémon Showdown
|
|
447
404
|
assert(!showdown);
|
|
448
405
|
mslot = @intCast(u4, if (player == .P1)
|
|
449
|
-
battle.
|
|
406
|
+
battle.last_selected_indexes.p1
|
|
450
407
|
else
|
|
451
|
-
battle.
|
|
408
|
+
battle.last_selected_indexes.p2);
|
|
452
409
|
const stored = side.stored();
|
|
453
410
|
// GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
|
|
454
411
|
auto = side.last_selected_move == .HyperBeam or side.active.volatiles.Bide or
|
|
@@ -470,14 +427,14 @@ fn executeMove(
|
|
|
470
427
|
// Incorrect mslot with Pokémon Showdown choice semantics so we need to recover from index
|
|
471
428
|
assert(mslot == 1);
|
|
472
429
|
mslot = @intCast(u4, if (player == .P1)
|
|
473
|
-
battle.
|
|
430
|
+
battle.last_selected_indexes.p1
|
|
474
431
|
else
|
|
475
|
-
battle.
|
|
432
|
+
battle.last_selected_indexes.p2);
|
|
476
433
|
}
|
|
477
434
|
|
|
478
435
|
var skip_can = false;
|
|
479
436
|
var skip_pp = false;
|
|
480
|
-
switch (try beforeMove(battle, player,
|
|
437
|
+
switch (try beforeMove(battle, player, from, log)) {
|
|
481
438
|
.done => return null,
|
|
482
439
|
.skip_can => skip_can = true,
|
|
483
440
|
.skip_pp => skip_pp = true,
|
|
@@ -489,19 +446,12 @@ fn executeMove(
|
|
|
489
446
|
return null;
|
|
490
447
|
}
|
|
491
448
|
|
|
492
|
-
return doMove(battle, player, mslot,
|
|
449
|
+
return doMove(battle, player, mslot, auto, residual, log);
|
|
493
450
|
}
|
|
494
451
|
|
|
495
|
-
const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
|
|
452
|
+
const BeforeMove = union(enum) { done, skip_can, skip_pp, ok, err };
|
|
496
453
|
|
|
497
|
-
fn beforeMove(
|
|
498
|
-
battle: anytype,
|
|
499
|
-
player: Player,
|
|
500
|
-
skip: bool,
|
|
501
|
-
from: ?Move,
|
|
502
|
-
residual: *bool,
|
|
503
|
-
log: anytype,
|
|
504
|
-
) !BeforeMove {
|
|
454
|
+
fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !BeforeMove {
|
|
505
455
|
var side = battle.side(player);
|
|
506
456
|
const foe = battle.foe(player);
|
|
507
457
|
var active = &side.active;
|
|
@@ -529,15 +479,13 @@ fn beforeMove(
|
|
|
529
479
|
return .done;
|
|
530
480
|
}
|
|
531
481
|
|
|
532
|
-
if (
|
|
482
|
+
if (foe.active.volatiles.Binding) {
|
|
533
483
|
try log.cant(ident, .Bound);
|
|
534
484
|
return .done;
|
|
535
485
|
}
|
|
536
486
|
|
|
537
|
-
if (volatiles.Flinch) {
|
|
538
|
-
|
|
539
|
-
// Pokémon can sometimes flinch multiple times from the same original hit
|
|
540
|
-
if (!showdown) volatiles.Flinch = false;
|
|
487
|
+
if (!showdown and volatiles.Flinch) {
|
|
488
|
+
volatiles.Flinch = false;
|
|
541
489
|
try log.cant(ident, .Flinch);
|
|
542
490
|
return .done;
|
|
543
491
|
}
|
|
@@ -558,6 +506,13 @@ fn beforeMove(
|
|
|
558
506
|
// Pokémon Showdown's disable condition has a single onBeforeMove handler
|
|
559
507
|
if (showdown and try disabled(side, ident, log)) return .done;
|
|
560
508
|
|
|
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
|
+
|
|
561
516
|
// This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
|
|
562
517
|
// before the side had a selected a move - we simply need to assume this leads to a desync
|
|
563
518
|
if (side.last_selected_move == .None) {
|
|
@@ -575,9 +530,14 @@ fn beforeMove(
|
|
|
575
530
|
} else {
|
|
576
531
|
try log.activate(ident, .Confusion);
|
|
577
532
|
|
|
578
|
-
if (
|
|
533
|
+
const confused = if (showdown)
|
|
534
|
+
!battle.rng.chance(u8, 128, 256)
|
|
535
|
+
else
|
|
536
|
+
battle.rng.next() >= Gen12.percent(50) + 1;
|
|
537
|
+
|
|
538
|
+
if (confused) {
|
|
579
539
|
assert(!volatiles.MultiHit);
|
|
580
|
-
|
|
540
|
+
volatiles.state = 0;
|
|
581
541
|
volatiles.Bide = false;
|
|
582
542
|
volatiles.Thrashing = false;
|
|
583
543
|
volatiles.MultiHit = false;
|
|
@@ -601,12 +561,8 @@ fn beforeMove(
|
|
|
601
561
|
player
|
|
602
562
|
else
|
|
603
563
|
player.foe();
|
|
604
|
-
|
|
605
|
-
const uncapped = battle.last_damage;
|
|
606
564
|
// Skipping adjustDamage / randomizeDamage / checkHit
|
|
607
565
|
_ = try applyDamage(battle, player, target, .Confusion, log);
|
|
608
|
-
// Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
|
|
609
|
-
if (showdown) battle.last_damage = uncapped;
|
|
610
566
|
|
|
611
567
|
return .done;
|
|
612
568
|
}
|
|
@@ -615,28 +571,28 @@ fn beforeMove(
|
|
|
615
571
|
|
|
616
572
|
if (!showdown and try disabled(side, ident, log)) return .done;
|
|
617
573
|
|
|
618
|
-
if (Status.is(stored.status, .PAR)
|
|
619
|
-
if (
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
574
|
+
if (Status.is(stored.status, .PAR)) {
|
|
575
|
+
const paralyzed = if (showdown)
|
|
576
|
+
battle.rng.chance(u8, 63, 256)
|
|
577
|
+
else
|
|
578
|
+
battle.rng.next() < Gen12.percent(25);
|
|
579
|
+
|
|
580
|
+
if (paralyzed) {
|
|
581
|
+
volatiles.state = 0;
|
|
582
|
+
volatiles.Bide = false;
|
|
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
|
+
}
|
|
627
590
|
}
|
|
628
591
|
|
|
629
592
|
if (volatiles.Bide) {
|
|
630
593
|
assert(!volatiles.Thrashing and !volatiles.Rage);
|
|
631
594
|
|
|
632
|
-
|
|
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
|
-
}
|
|
595
|
+
volatiles.state +%= battle.last_damage;
|
|
640
596
|
|
|
641
597
|
assert(volatiles.attacks > 0);
|
|
642
598
|
|
|
@@ -657,18 +613,7 @@ fn beforeMove(
|
|
|
657
613
|
return .done;
|
|
658
614
|
}
|
|
659
615
|
|
|
660
|
-
const sub = showdown and foe.active.volatiles.Substitute;
|
|
661
616
|
_ = 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
|
-
|
|
672
617
|
return .done;
|
|
673
618
|
}
|
|
674
619
|
|
|
@@ -680,13 +625,8 @@ fn beforeMove(
|
|
|
680
625
|
try log.start(battle.active(player), .ConfusionSilent);
|
|
681
626
|
}
|
|
682
627
|
try log.move(ident, side.last_selected_move, battle.active(player.foe()), from);
|
|
683
|
-
if (showdown) {
|
|
684
|
-
|
|
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
|
-
}
|
|
628
|
+
if (showdown and handleThrashing(battle, active)) {
|
|
629
|
+
try log.start(battle.active(player), .ConfusionSilent);
|
|
690
630
|
}
|
|
691
631
|
return .skip_can;
|
|
692
632
|
}
|
|
@@ -701,11 +641,7 @@ fn beforeMove(
|
|
|
701
641
|
side.last_selected_move,
|
|
702
642
|
);
|
|
703
643
|
if (showdown or battle.last_damage != 0) {
|
|
704
|
-
const sub = showdown and foe.active.volatiles.Substitute;
|
|
705
644
|
_ = 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
|
-
}
|
|
709
645
|
}
|
|
710
646
|
return .done;
|
|
711
647
|
}
|
|
@@ -737,7 +673,6 @@ fn canMove(
|
|
|
737
673
|
side.active.volatiles.Invulnerable = false;
|
|
738
674
|
} else if (move.effect == .Charge) {
|
|
739
675
|
try log.move(player_ident, side.last_selected_move, .{}, from);
|
|
740
|
-
setCounterable(battle, player, side, move);
|
|
741
676
|
try Effects.charge(battle, player, log);
|
|
742
677
|
return false;
|
|
743
678
|
}
|
|
@@ -754,7 +689,6 @@ fn canMove(
|
|
|
754
689
|
// regrettable, but this information is more "nice to have" than required
|
|
755
690
|
const f = if (from != null and from.? == .None) null else from;
|
|
756
691
|
try log.move(player_ident, side.last_selected_move, battle.active(target), f);
|
|
757
|
-
setCounterable(battle, player, side, move);
|
|
758
692
|
|
|
759
693
|
if (move.effect.onBegin()) {
|
|
760
694
|
try onBegin(battle, player, move, mslot, residual, log);
|
|
@@ -765,27 +699,12 @@ fn canMove(
|
|
|
765
699
|
Effects.thrashing(battle, player);
|
|
766
700
|
} else if (!showdown and move.effect == .Binding) {
|
|
767
701
|
// Pokémon Showdown handles this after hit/miss checks and damage calculation
|
|
768
|
-
Effects.binding(battle, player
|
|
702
|
+
Effects.binding(battle, player);
|
|
769
703
|
}
|
|
770
704
|
|
|
771
705
|
return true;
|
|
772
706
|
}
|
|
773
707
|
|
|
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
|
-
|
|
789
708
|
fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
|
|
790
709
|
if (side.last_selected_move == .Struggle) return;
|
|
791
710
|
|
|
@@ -821,7 +740,6 @@ fn doMove(
|
|
|
821
740
|
battle: anytype,
|
|
822
741
|
player: Player,
|
|
823
742
|
mslot: u4,
|
|
824
|
-
rewrap: bool,
|
|
825
743
|
auto: bool,
|
|
826
744
|
residual: *bool,
|
|
827
745
|
log: anytype,
|
|
@@ -852,21 +770,16 @@ fn doMove(
|
|
|
852
770
|
(@enumToInt(move.type.effectiveness(foe.active.types.type1)) == 0 or
|
|
853
771
|
@enumToInt(move.type.effectiveness(foe.active.types.type2)) == 0);
|
|
854
772
|
if (immune and move.effect != .Binding) break :miss true;
|
|
855
|
-
if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe)
|
|
856
|
-
battle.last_damage = 0;
|
|
857
|
-
break :miss true;
|
|
858
|
-
}
|
|
773
|
+
if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) break :miss true;
|
|
859
774
|
break :miss move.target != .Self and !moveHit(battle, player, move, &immune, &mist);
|
|
860
775
|
};
|
|
861
776
|
assert(!immune or miss or (showdown and move.effect == .Binding));
|
|
862
777
|
|
|
863
|
-
var late = showdown and move.effect != .Explode;
|
|
864
778
|
const skip = status or immune;
|
|
865
779
|
if ((!showdown or (!skip or counter)) and !miss) blk: {
|
|
866
780
|
if (showdown and move.effect.isMulti()) {
|
|
867
781
|
Effects.multiHit(battle, player, move);
|
|
868
782
|
hits = side.active.volatiles.attacks;
|
|
869
|
-
late = false;
|
|
870
783
|
}
|
|
871
784
|
|
|
872
785
|
// Cartridge rolls for crit even for moves that can't crit (Counter/Metronome/status/OHKO)
|
|
@@ -900,7 +813,7 @@ fn doMove(
|
|
|
900
813
|
}
|
|
901
814
|
|
|
902
815
|
// Due to control flow shenanigans we need to clear last_damage for Pokémon Showdown
|
|
903
|
-
if (showdown and skip) battle.last_damage = 0;
|
|
816
|
+
if (showdown and skip and move.effect != .Bide) battle.last_damage = 0;
|
|
904
817
|
|
|
905
818
|
miss = if (showdown or skip)
|
|
906
819
|
miss
|
|
@@ -913,9 +826,9 @@ fn doMove(
|
|
|
913
826
|
|
|
914
827
|
if (!showdown or !miss) {
|
|
915
828
|
if (move.effect == .MirrorMove) {
|
|
916
|
-
return mirrorMove(battle, player, mslot,
|
|
829
|
+
return mirrorMove(battle, player, mslot, auto, residual, log);
|
|
917
830
|
} else if (move.effect == .Metronome) {
|
|
918
|
-
return metronome(battle, player, mslot,
|
|
831
|
+
return metronome(battle, player, mslot, auto, residual, log);
|
|
919
832
|
} else if (move.effect.onEnd()) {
|
|
920
833
|
try onEnd(battle, player, move, log);
|
|
921
834
|
return null;
|
|
@@ -930,7 +843,7 @@ fn doMove(
|
|
|
930
843
|
move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
|
|
931
844
|
if (ohko) {
|
|
932
845
|
try log.immune(foe_ident, .OHKO);
|
|
933
|
-
} else if (immune and !invulnerable
|
|
846
|
+
} else if (immune and !invulnerable) {
|
|
934
847
|
try log.immune(foe_ident, .None);
|
|
935
848
|
} else if (mist) {
|
|
936
849
|
if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
|
|
@@ -947,9 +860,15 @@ fn doMove(
|
|
|
947
860
|
if (showdown and side.stored().hp == 0) residual.* = false;
|
|
948
861
|
} else if (move.effect == .Explode) {
|
|
949
862
|
try Effects.explode(battle, player);
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
863
|
+
if (foe.active.volatiles.Rage and foe.active.boosts.atk < 6) {
|
|
864
|
+
try Effects.boost(battle, player.foe(), Move.get(.Rage), log);
|
|
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
|
+
}
|
|
953
872
|
}
|
|
954
873
|
return null;
|
|
955
874
|
}
|
|
@@ -962,9 +881,6 @@ fn doMove(
|
|
|
962
881
|
hits = side.active.volatiles.attacks;
|
|
963
882
|
}
|
|
964
883
|
|
|
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
|
-
|
|
968
884
|
var nullified = false;
|
|
969
885
|
var hit: u4 = 0;
|
|
970
886
|
while (hit < hits) {
|
|
@@ -980,7 +896,9 @@ fn doMove(
|
|
|
980
896
|
if (hit == 0 and ohko) try log.ohko();
|
|
981
897
|
hit += 1;
|
|
982
898
|
if (foe.stored().hp == 0) break;
|
|
983
|
-
if (
|
|
899
|
+
if (foe.active.volatiles.Rage and foe.active.boosts.atk < 6) {
|
|
900
|
+
try Effects.boost(battle, player.foe(), Move.get(.Rage), log);
|
|
901
|
+
}
|
|
984
902
|
// If the substitute breaks during a multi-hit attack, the attack ends
|
|
985
903
|
if (nullified) break;
|
|
986
904
|
}
|
|
@@ -996,17 +914,11 @@ fn doMove(
|
|
|
996
914
|
} else if (showdown) {
|
|
997
915
|
// This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
|
|
998
916
|
if (move.effect == .Binding) {
|
|
999
|
-
Effects.binding(battle, player
|
|
917
|
+
Effects.binding(battle, player);
|
|
1000
918
|
if (immune) {
|
|
1001
919
|
battle.last_damage = 0;
|
|
1002
|
-
assert(foe.stored().hp > 0);
|
|
1003
920
|
// Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
|
|
1004
|
-
|
|
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
|
-
}
|
|
921
|
+
try log.damage(battle.active(player.foe()), foe.stored(), .None);
|
|
1010
922
|
return null;
|
|
1011
923
|
}
|
|
1012
924
|
}
|
|
@@ -1023,9 +935,6 @@ fn doMove(
|
|
|
1023
935
|
|
|
1024
936
|
if (foe.stored().hp == 0) return null;
|
|
1025
937
|
|
|
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
|
-
|
|
1029
938
|
if (!move.effect.isSpecial()) {
|
|
1030
939
|
// On the cartridge Rage is not considered to be "special" and thus gets executed for a
|
|
1031
940
|
// second time here (after being executed in the "always happens" block above) but that
|
|
@@ -1037,9 +946,7 @@ fn doMove(
|
|
|
1037
946
|
if (move.effect == .Twineedle) {
|
|
1038
947
|
if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), log);
|
|
1039
948
|
} else if (move.effect == .Disable) {
|
|
1040
|
-
|
|
1041
|
-
if (showdown) try buildRage(battle, player.foe(), log);
|
|
1042
|
-
return result;
|
|
949
|
+
return Effects.disable(battle, player, move, log);
|
|
1043
950
|
} else {
|
|
1044
951
|
try moveEffect(battle, player, move, log);
|
|
1045
952
|
}
|
|
@@ -1057,7 +964,8 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
|
|
|
1057
964
|
chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
|
|
1058
965
|
chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
|
|
1059
966
|
|
|
1060
|
-
return
|
|
967
|
+
if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
|
|
968
|
+
return std.math.rotl(u8, battle.rng.next(), 3) < chance;
|
|
1061
969
|
}
|
|
1062
970
|
|
|
1063
971
|
fn calcDamage(
|
|
@@ -1161,37 +1069,49 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
|
1161
1069
|
|
|
1162
1070
|
fn randomizeDamage(battle: anytype) void {
|
|
1163
1071
|
if (battle.last_damage <= 1) return;
|
|
1164
|
-
|
|
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
|
+
|
|
1165
1082
|
battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
|
|
1166
1083
|
}
|
|
1167
1084
|
|
|
1168
1085
|
fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
|
|
1169
1086
|
const side = battle.side(player);
|
|
1170
|
-
const foe = battle.foe(player);
|
|
1171
1087
|
|
|
1172
1088
|
if (!try checkHit(battle, player, move, log)) return null;
|
|
1173
1089
|
|
|
1174
1090
|
battle.last_damage = switch (side.last_selected_move) {
|
|
1175
|
-
.SuperFang => @max(foe.stored().hp / 2, 1),
|
|
1091
|
+
.SuperFang => @max(battle.foe(player).stored().hp / 2, 1),
|
|
1176
1092
|
.SeismicToss, .NightShade => side.stored().level,
|
|
1177
1093
|
.SonicBoom => 20,
|
|
1178
1094
|
.DragonRage => 40,
|
|
1179
1095
|
// GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
|
|
1180
1096
|
.Psywave => power: {
|
|
1181
1097
|
const max = @intCast(u8, @as(u16, side.stored().level) * 3 / 2);
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1098
|
+
if (showdown) {
|
|
1099
|
+
break :power battle.rng.range(u8, 0, max);
|
|
1100
|
+
} else {
|
|
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
|
+
}
|
|
1185
1108
|
},
|
|
1186
1109
|
else => unreachable,
|
|
1187
1110
|
};
|
|
1188
1111
|
|
|
1189
1112
|
if (battle.last_damage == 0) return if (showdown) null else Result.Error;
|
|
1190
1113
|
|
|
1191
|
-
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1192
1114
|
_ = 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
|
-
|
|
1195
1115
|
return null;
|
|
1196
1116
|
}
|
|
1197
1117
|
|
|
@@ -1203,17 +1123,19 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
|
|
|
1203
1123
|
return null;
|
|
1204
1124
|
}
|
|
1205
1125
|
|
|
1206
|
-
// Pretend
|
|
1126
|
+
// Pretend Counter was used as a stand-in when no move has been used to fail below with 0 BP
|
|
1127
|
+
const foe_last_used_move =
|
|
1128
|
+
Move.get(if (foe.last_used_move == .None) .Counter else foe.last_used_move);
|
|
1207
1129
|
const foe_last_selected_move =
|
|
1208
1130
|
Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
|
|
1209
|
-
.
|
|
1131
|
+
.Counter
|
|
1210
1132
|
else
|
|
1211
1133
|
foe.last_selected_move);
|
|
1212
1134
|
|
|
1213
|
-
const used =
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1135
|
+
const used = foe_last_used_move.bp > 0 and
|
|
1136
|
+
foe.last_used_move != .Counter and
|
|
1137
|
+
(foe_last_used_move.type == .Normal or
|
|
1138
|
+
foe_last_used_move.type == .Fighting);
|
|
1217
1139
|
|
|
1218
1140
|
const selected = foe_last_selected_move.bp > 0 and
|
|
1219
1141
|
foe.last_selected_move != .Counter and
|
|
@@ -1232,14 +1154,12 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
|
|
|
1232
1154
|
return null;
|
|
1233
1155
|
}
|
|
1234
1156
|
|
|
1235
|
-
battle.last_damage
|
|
1157
|
+
battle.last_damage = if (battle.last_damage > 0x7FFF) 0xFFFF else battle.last_damage * 2;
|
|
1236
1158
|
|
|
1237
1159
|
// Pokémon Showdown calls checkHit before Counter
|
|
1238
1160
|
if (!showdown and !try checkHit(battle, player, move, log)) return null;
|
|
1239
1161
|
|
|
1240
|
-
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1241
1162
|
_ = 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);
|
|
1243
1163
|
return null;
|
|
1244
1164
|
}
|
|
1245
1165
|
|
|
@@ -1283,7 +1203,6 @@ fn mirrorMove(
|
|
|
1283
1203
|
battle: anytype,
|
|
1284
1204
|
player: Player,
|
|
1285
1205
|
mslot: u4,
|
|
1286
|
-
rewrap: bool,
|
|
1287
1206
|
auto: bool,
|
|
1288
1207
|
residual: *bool,
|
|
1289
1208
|
log: anytype,
|
|
@@ -1296,30 +1215,41 @@ fn mirrorMove(
|
|
|
1296
1215
|
if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
|
|
1297
1216
|
try log.fail(battle.active(player), .None);
|
|
1298
1217
|
return null;
|
|
1299
|
-
} else if (!showdown or foe.last_used_move != .Struggle) {
|
|
1300
|
-
incrementPP(side, mslot);
|
|
1301
1218
|
}
|
|
1302
1219
|
|
|
1220
|
+
incrementPP(side, mslot);
|
|
1221
|
+
|
|
1303
1222
|
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
|
|
1304
|
-
return doMove(battle, player, mslot,
|
|
1223
|
+
return doMove(battle, player, mslot, auto, residual, log);
|
|
1305
1224
|
}
|
|
1306
1225
|
|
|
1307
1226
|
fn metronome(
|
|
1308
1227
|
battle: anytype,
|
|
1309
1228
|
player: Player,
|
|
1310
1229
|
mslot: u4,
|
|
1311
|
-
rewrap: bool,
|
|
1312
1230
|
auto: bool,
|
|
1313
1231
|
residual: *bool,
|
|
1314
1232
|
log: anytype,
|
|
1315
1233
|
) !?Result {
|
|
1316
1234
|
var side = battle.side(player);
|
|
1317
1235
|
|
|
1318
|
-
side.last_selected_move =
|
|
1236
|
+
side.last_selected_move = if (showdown) blk: {
|
|
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
|
+
|
|
1319
1249
|
incrementPP(side, mslot);
|
|
1320
1250
|
|
|
1321
1251
|
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
|
|
1322
|
-
return doMove(battle, player, mslot,
|
|
1252
|
+
return doMove(battle, player, mslot, auto, residual, log);
|
|
1323
1253
|
}
|
|
1324
1254
|
|
|
1325
1255
|
fn checkHit(battle: anytype, player: Player, move: Move.Data, log: anytype) !bool {
|
|
@@ -1373,24 +1303,23 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
|
|
|
1373
1303
|
}
|
|
1374
1304
|
|
|
1375
1305
|
// GLITCH: Thrash / Petal Dance / Rage get their accuracy overwritten on subsequent hits
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
var accuracy = if (overwritten) state else @as(u16, Gen12.percent(move.accuracy));
|
|
1306
|
+
var state = &side.active.volatiles.state;
|
|
1307
|
+
const overwritten = (move.effect == .Thrashing or move.effect == .Rage) and state.* > 0;
|
|
1308
|
+
assert(!overwritten or (0 < state.* and state.* <= 255 and !side.active.volatiles.Bide));
|
|
1309
|
+
var accuracy = if (overwritten) state.* else @as(u16, Gen12.percent(move.accuracy));
|
|
1382
1310
|
var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
|
|
1383
1311
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1384
1312
|
boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
|
|
1385
1313
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1386
1314
|
accuracy = @min(255, @max(1, accuracy));
|
|
1387
1315
|
|
|
1388
|
-
|
|
1389
|
-
if (showdown) overwrite = side.active.volatiles.Rage or side.active.volatiles.Thrashing;
|
|
1390
|
-
if (overwrite) side.active.volatiles.state = accuracy;
|
|
1316
|
+
if (move.effect == .Thrashing or move.effect == .Rage) state.* = accuracy;
|
|
1391
1317
|
|
|
1392
1318
|
// GLITCH: max accuracy is 255 so 1/256 chance of miss
|
|
1393
|
-
break :miss
|
|
1319
|
+
break :miss if (showdown)
|
|
1320
|
+
!battle.rng.chance(u8, @intCast(u8, accuracy), 256)
|
|
1321
|
+
else
|
|
1322
|
+
battle.rng.next() >= accuracy;
|
|
1394
1323
|
};
|
|
1395
1324
|
|
|
1396
1325
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1505,15 +1434,6 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
|
|
|
1505
1434
|
}
|
|
1506
1435
|
|
|
1507
1436
|
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
|
-
|
|
1517
1437
|
var damage = @max(stored.stats.hp / 16, 1);
|
|
1518
1438
|
|
|
1519
1439
|
// GLITCH: Leech Seed + Toxic glitch
|
|
@@ -1525,6 +1445,10 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
|
|
|
1525
1445
|
const amount = @min(damage, stored.hp);
|
|
1526
1446
|
stored.hp -= amount;
|
|
1527
1447
|
|
|
1448
|
+
var foe = battle.foe(player);
|
|
1449
|
+
var foe_stored = foe.stored();
|
|
1450
|
+
const foe_ident = battle.active(player.foe());
|
|
1451
|
+
|
|
1528
1452
|
// As above, Pokémon Showdown uses damageOf but its not relevant
|
|
1529
1453
|
if (amount > 0) try log.damage(ident, stored, .LeechSeed);
|
|
1530
1454
|
|
|
@@ -1540,13 +1464,13 @@ fn endTurn(battle: anytype, log: anytype) @TypeOf(log).Error!Result {
|
|
|
1540
1464
|
assert(!battle.side(.P1).active.volatiles.MultiHit);
|
|
1541
1465
|
assert(!battle.side(.P2).active.volatiles.MultiHit);
|
|
1542
1466
|
|
|
1543
|
-
battle.turn += 1;
|
|
1544
|
-
|
|
1545
1467
|
if (showdown and options.ebc and checkEBC(battle)) {
|
|
1546
1468
|
try log.tie();
|
|
1547
1469
|
return Result.Tie;
|
|
1548
1470
|
}
|
|
1549
1471
|
|
|
1472
|
+
battle.turn += 1;
|
|
1473
|
+
|
|
1550
1474
|
if (showdown and battle.turn >= 1000) {
|
|
1551
1475
|
try log.tie();
|
|
1552
1476
|
return Result.Tie;
|
|
@@ -1613,42 +1537,6 @@ fn checkEBC(battle: anytype) bool {
|
|
|
1613
1537
|
return false;
|
|
1614
1538
|
}
|
|
1615
1539
|
|
|
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
|
-
|
|
1652
1540
|
inline fn onBegin(
|
|
1653
1541
|
battle: anytype,
|
|
1654
1542
|
player: Player,
|
|
@@ -1739,7 +1627,10 @@ pub const Effects = struct {
|
|
|
1739
1627
|
side.active.volatiles.Bide = true;
|
|
1740
1628
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1741
1629
|
side.active.volatiles.state = 0;
|
|
1742
|
-
side.active.volatiles.attacks =
|
|
1630
|
+
side.active.volatiles.attacks = @intCast(u3, if (showdown)
|
|
1631
|
+
battle.rng.range(u4, 2, 4)
|
|
1632
|
+
else
|
|
1633
|
+
(battle.rng.next() & 1) + 2);
|
|
1743
1634
|
|
|
1744
1635
|
try log.start(battle.active(player), .Bide);
|
|
1745
1636
|
}
|
|
@@ -1761,8 +1652,14 @@ pub const Effects = struct {
|
|
|
1761
1652
|
return;
|
|
1762
1653
|
}
|
|
1763
1654
|
|
|
1764
|
-
|
|
1765
|
-
|
|
1655
|
+
const chance = !foe.active.types.includes(move.type) and if (showdown)
|
|
1656
|
+
battle.rng.chance(u8, @as(u8, if (move.effect == .BurnChance1) 26 else 77), 256)
|
|
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;
|
|
1766
1663
|
|
|
1767
1664
|
foe_stored.status = Status.init(.BRN);
|
|
1768
1665
|
foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
|
|
@@ -1786,7 +1683,11 @@ pub const Effects = struct {
|
|
|
1786
1683
|
const sub = foe.active.volatiles.Substitute;
|
|
1787
1684
|
|
|
1788
1685
|
if (move.effect == .ConfusionChance) {
|
|
1789
|
-
if (
|
|
1686
|
+
const chance = if (showdown)
|
|
1687
|
+
battle.rng.chance(u8, 25, 256)
|
|
1688
|
+
else
|
|
1689
|
+
battle.rng.next() < Gen12.percent(10);
|
|
1690
|
+
if (!chance) return;
|
|
1790
1691
|
} else {
|
|
1791
1692
|
if (showdown) {
|
|
1792
1693
|
if (!try checkHit(battle, player, move, log)) {
|
|
@@ -1805,7 +1706,10 @@ pub const Effects = struct {
|
|
|
1805
1706
|
|
|
1806
1707
|
if (foe.active.volatiles.Confusion) return;
|
|
1807
1708
|
foe.active.volatiles.Confusion = true;
|
|
1808
|
-
foe.active.volatiles.confusion =
|
|
1709
|
+
foe.active.volatiles.confusion = @intCast(u3, if (showdown)
|
|
1710
|
+
battle.rng.range(u8, 2, 6)
|
|
1711
|
+
else
|
|
1712
|
+
(battle.rng.next() & 3) + 2);
|
|
1809
1713
|
|
|
1810
1714
|
try log.start(battle.active(player.foe()), .Confusion);
|
|
1811
1715
|
}
|
|
@@ -1855,8 +1759,11 @@ pub const Effects = struct {
|
|
|
1855
1759
|
return Result.Error;
|
|
1856
1760
|
}
|
|
1857
1761
|
|
|
1858
|
-
volatiles.disabled_move = @intCast(u3,
|
|
1859
|
-
volatiles.disabled_duration =
|
|
1762
|
+
volatiles.disabled_move = @intCast(u3, randomMoveSlot(&battle.rng, &foe.active.moves, n));
|
|
1763
|
+
volatiles.disabled_duration = @intCast(u4, if (showdown)
|
|
1764
|
+
battle.rng.range(u8, 1, 9)
|
|
1765
|
+
else
|
|
1766
|
+
(battle.rng.next() & 7) + 1);
|
|
1860
1767
|
|
|
1861
1768
|
try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
|
|
1862
1769
|
return null;
|
|
@@ -1868,7 +1775,7 @@ pub const Effects = struct {
|
|
|
1868
1775
|
|
|
1869
1776
|
if (battle.last_damage == 0) {
|
|
1870
1777
|
assert(showdown);
|
|
1871
|
-
|
|
1778
|
+
return;
|
|
1872
1779
|
}
|
|
1873
1780
|
|
|
1874
1781
|
const drain = @max(battle.last_damage / 2, 1);
|
|
@@ -1895,7 +1802,15 @@ pub const Effects = struct {
|
|
|
1895
1802
|
var volatiles = &battle.foe(player).active.volatiles;
|
|
1896
1803
|
|
|
1897
1804
|
if (volatiles.Substitute) return;
|
|
1898
|
-
|
|
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;
|
|
1899
1814
|
|
|
1900
1815
|
volatiles.Flinch = true;
|
|
1901
1816
|
volatiles.Recharging = false;
|
|
@@ -1921,8 +1836,12 @@ pub const Effects = struct {
|
|
|
1921
1836
|
return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
|
|
1922
1837
|
}
|
|
1923
1838
|
|
|
1924
|
-
|
|
1925
|
-
|
|
1839
|
+
const chance = !foe.active.types.includes(move.type) and if (showdown)
|
|
1840
|
+
battle.rng.chance(u8, 26, 256)
|
|
1841
|
+
else
|
|
1842
|
+
battle.rng.next() < 1 + Gen12.percent(10);
|
|
1843
|
+
if (!chance) return;
|
|
1844
|
+
|
|
1926
1845
|
// Freeze Clause Mod
|
|
1927
1846
|
if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
1928
1847
|
|
|
@@ -2059,15 +1978,15 @@ pub const Effects = struct {
|
|
|
2059
1978
|
}
|
|
2060
1979
|
break :has_mimic false;
|
|
2061
1980
|
};
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
1981
|
+
// If the foe is Invulnerable we still want to fall through to checkHit to be able to
|
|
1982
|
+
// trigger |-miss| instead of |-fail|
|
|
1983
|
+
if (!has_mimic and !foe.active.volatiles.Invulnerable) {
|
|
2065
1984
|
return try log.fail(battle.active(player.foe()), .None);
|
|
2066
1985
|
}
|
|
2067
1986
|
}
|
|
2068
1987
|
if (!try checkHit(battle, player, move, log)) return;
|
|
2069
1988
|
|
|
2070
|
-
const rslot =
|
|
1989
|
+
const rslot = randomMoveSlot(&battle.rng, &foe.active.moves, 0);
|
|
2071
1990
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
2072
1991
|
|
|
2073
1992
|
try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
|
|
@@ -2088,8 +2007,7 @@ pub const Effects = struct {
|
|
|
2088
2007
|
assert(!side.active.volatiles.MultiHit);
|
|
2089
2008
|
side.active.volatiles.MultiHit = true;
|
|
2090
2009
|
|
|
2091
|
-
side.active.volatiles.attacks =
|
|
2092
|
-
if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
|
|
2010
|
+
side.active.volatiles.attacks = if (move.effect == .MultiHit) distribution(battle) else 2;
|
|
2093
2011
|
}
|
|
2094
2012
|
|
|
2095
2013
|
fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2133,8 +2051,14 @@ pub const Effects = struct {
|
|
|
2133
2051
|
}
|
|
2134
2052
|
|
|
2135
2053
|
// Body Slam can't paralyze a Normal type Pokémon
|
|
2136
|
-
|
|
2137
|
-
|
|
2054
|
+
const chance = !foe.active.types.includes(move.type) and if (showdown)
|
|
2055
|
+
battle.rng.chance(u8, @as(u8, if (move.effect == .ParalyzeChance1) 26 else 77), 256)
|
|
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;
|
|
2138
2062
|
|
|
2139
2063
|
foe_stored.status = Status.init(.PAR);
|
|
2140
2064
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
@@ -2177,7 +2101,16 @@ pub const Effects = struct {
|
|
|
2177
2101
|
|
|
2178
2102
|
if (move.effect == .Poison) {
|
|
2179
2103
|
if (!showdown and !try checkHit(battle, player, move, log)) return;
|
|
2180
|
-
} else
|
|
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
|
+
}
|
|
2181
2114
|
|
|
2182
2115
|
foe_stored.status = Status.init(.PSN);
|
|
2183
2116
|
if (toxic) {
|
|
@@ -2191,8 +2124,10 @@ pub const Effects = struct {
|
|
|
2191
2124
|
|
|
2192
2125
|
fn rage(battle: anytype, player: Player) !void {
|
|
2193
2126
|
var volatiles = &battle.side(player).active.volatiles;
|
|
2194
|
-
|
|
2127
|
+
if (volatiles.Rage) return;
|
|
2195
2128
|
volatiles.Rage = true;
|
|
2129
|
+
assert(!volatiles.Bide);
|
|
2130
|
+
volatiles.state = 0;
|
|
2196
2131
|
}
|
|
2197
2132
|
|
|
2198
2133
|
fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
|
|
@@ -2244,7 +2179,16 @@ pub const Effects = struct {
|
|
|
2244
2179
|
}
|
|
2245
2180
|
foe.active.volatiles.Recharging = false;
|
|
2246
2181
|
|
|
2247
|
-
|
|
2182
|
+
const duration = @intCast(u3, if (showdown)
|
|
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);
|
|
2248
2192
|
try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
|
|
2249
2193
|
}
|
|
2250
2194
|
|
|
@@ -2286,9 +2230,8 @@ pub const Effects = struct {
|
|
|
2286
2230
|
fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
2287
2231
|
if (!showdown or battle.side(player).last_selected_move == .Teleport) return;
|
|
2288
2232
|
|
|
2289
|
-
// Whirlwind/Roar should not roll to hit
|
|
2233
|
+
// Whirlwind/Roar should not roll to hit but Pokémon Showdown does anyway
|
|
2290
2234
|
_ = try checkHit(battle, player, move, log);
|
|
2291
|
-
battle.last_damage = 0;
|
|
2292
2235
|
}
|
|
2293
2236
|
|
|
2294
2237
|
fn thrashing(battle: anytype, player: Player) void {
|
|
@@ -2297,7 +2240,11 @@ pub const Effects = struct {
|
|
|
2297
2240
|
assert(!volatiles.Bide);
|
|
2298
2241
|
|
|
2299
2242
|
volatiles.Thrashing = true;
|
|
2300
|
-
volatiles.
|
|
2243
|
+
volatiles.state = 0;
|
|
2244
|
+
volatiles.attacks = @intCast(u3, if (showdown)
|
|
2245
|
+
battle.rng.range(u8, 2, 4)
|
|
2246
|
+
else
|
|
2247
|
+
(battle.rng.next() & 1) + 2);
|
|
2301
2248
|
}
|
|
2302
2249
|
|
|
2303
2250
|
fn transform(battle: anytype, player: Player, log: anytype) !void {
|
|
@@ -2329,7 +2276,7 @@ pub const Effects = struct {
|
|
|
2329
2276
|
try log.transform(battle.active(player), foe_ident);
|
|
2330
2277
|
}
|
|
2331
2278
|
|
|
2332
|
-
fn binding(battle: anytype, player: Player
|
|
2279
|
+
fn binding(battle: anytype, player: Player) void {
|
|
2333
2280
|
var side = battle.side(player);
|
|
2334
2281
|
var foe = battle.foe(player);
|
|
2335
2282
|
|
|
@@ -2337,11 +2284,9 @@ pub const Effects = struct {
|
|
|
2337
2284
|
side.active.volatiles.Binding = true;
|
|
2338
2285
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2339
2286
|
// (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
|
|
2340
|
-
if (!showdown)
|
|
2341
|
-
foe.active.volatiles.Recharging = false;
|
|
2342
|
-
} else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
|
|
2287
|
+
if (!showdown) foe.active.volatiles.Recharging = false;
|
|
2343
2288
|
|
|
2344
|
-
side.active.volatiles.attacks =
|
|
2289
|
+
side.active.volatiles.attacks = distribution(battle) - 1;
|
|
2345
2290
|
}
|
|
2346
2291
|
|
|
2347
2292
|
fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2368,11 +2313,9 @@ pub const Effects = struct {
|
|
|
2368
2313
|
return try log.fail(ident, .None);
|
|
2369
2314
|
}
|
|
2370
2315
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2371
|
-
const stat = unmodifiedStats(battle,
|
|
2316
|
+
const stat = unmodifiedStats(battle, player).atk;
|
|
2372
2317
|
stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2373
2318
|
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;
|
|
2376
2319
|
},
|
|
2377
2320
|
.DefenseUp1, .DefenseUp2 => {
|
|
2378
2321
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
@@ -2388,7 +2331,7 @@ pub const Effects = struct {
|
|
|
2388
2331
|
return try log.fail(ident, .None);
|
|
2389
2332
|
}
|
|
2390
2333
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2391
|
-
const stat = unmodifiedStats(battle,
|
|
2334
|
+
const stat = unmodifiedStats(battle, player).def;
|
|
2392
2335
|
stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2393
2336
|
try log.boost(ident, .Defense, n);
|
|
2394
2337
|
},
|
|
@@ -2405,7 +2348,7 @@ pub const Effects = struct {
|
|
|
2405
2348
|
return try log.fail(ident, .None);
|
|
2406
2349
|
}
|
|
2407
2350
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2408
|
-
const stat = unmodifiedStats(battle,
|
|
2351
|
+
const stat = unmodifiedStats(battle, player).spe;
|
|
2409
2352
|
stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2410
2353
|
try log.boost(ident, .Speed, 2);
|
|
2411
2354
|
},
|
|
@@ -2425,7 +2368,7 @@ pub const Effects = struct {
|
|
|
2425
2368
|
return try log.fail(ident, .None);
|
|
2426
2369
|
}
|
|
2427
2370
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2428
|
-
const stat = unmodifiedStats(battle,
|
|
2371
|
+
const stat = unmodifiedStats(battle, player).spc;
|
|
2429
2372
|
stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2430
2373
|
try log.boost(ident, .SpecialAttack, n);
|
|
2431
2374
|
try log.boost(ident, .SpecialDefense, n);
|
|
@@ -2452,7 +2395,11 @@ pub const Effects = struct {
|
|
|
2452
2395
|
}
|
|
2453
2396
|
|
|
2454
2397
|
if (move.effect.isStatDownChance()) {
|
|
2455
|
-
if (
|
|
2398
|
+
const chance = if (showdown)
|
|
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;
|
|
2456
2403
|
} else if (!showdown and !try checkHit(battle, player, move, log)) {
|
|
2457
2404
|
return; // checkHit already checks for Invulnerable
|
|
2458
2405
|
}
|
|
@@ -2474,7 +2421,7 @@ pub const Effects = struct {
|
|
|
2474
2421
|
return try log.fail(foe_ident, .None);
|
|
2475
2422
|
}
|
|
2476
2423
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2477
|
-
const stat = unmodifiedStats(battle, foe).atk;
|
|
2424
|
+
const stat = unmodifiedStats(battle, player.foe()).atk;
|
|
2478
2425
|
stats.atk = @max(1, stat * mod[0] / mod[1]);
|
|
2479
2426
|
try log.boost(foe_ident, .Attack, -1);
|
|
2480
2427
|
},
|
|
@@ -2492,7 +2439,7 @@ pub const Effects = struct {
|
|
|
2492
2439
|
return try log.fail(foe_ident, .None);
|
|
2493
2440
|
}
|
|
2494
2441
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2495
|
-
const stat = unmodifiedStats(battle, foe).def;
|
|
2442
|
+
const stat = unmodifiedStats(battle, player.foe()).def;
|
|
2496
2443
|
stats.def = @max(1, stat * mod[0] / mod[1]);
|
|
2497
2444
|
try log.boost(foe_ident, .Defense, -@as(i8, n));
|
|
2498
2445
|
},
|
|
@@ -2509,7 +2456,7 @@ pub const Effects = struct {
|
|
|
2509
2456
|
return try log.fail(foe_ident, .None);
|
|
2510
2457
|
}
|
|
2511
2458
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2512
|
-
const stat = unmodifiedStats(battle, foe).spe;
|
|
2459
|
+
const stat = unmodifiedStats(battle, player.foe()).spe;
|
|
2513
2460
|
stats.spe = @max(1, stat * mod[0] / mod[1]);
|
|
2514
2461
|
try log.boost(foe_ident, .Speed, -1);
|
|
2515
2462
|
assert(boosts.spe >= -6);
|
|
@@ -2529,7 +2476,7 @@ pub const Effects = struct {
|
|
|
2529
2476
|
return try log.fail(foe_ident, .None);
|
|
2530
2477
|
}
|
|
2531
2478
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2532
|
-
const stat = unmodifiedStats(battle, foe).spc;
|
|
2479
|
+
const stat = unmodifiedStats(battle, player.foe()).spc;
|
|
2533
2480
|
stats.spc = @max(1, stat * mod[0] / mod[1]);
|
|
2534
2481
|
try log.boost(foe_ident, .SpecialAttack, -1);
|
|
2535
2482
|
try log.boost(foe_ident, .SpecialDefense, -1);
|
|
@@ -2548,7 +2495,8 @@ pub const Effects = struct {
|
|
|
2548
2495
|
}
|
|
2549
2496
|
};
|
|
2550
2497
|
|
|
2551
|
-
fn unmodifiedStats(battle: anytype,
|
|
2498
|
+
fn unmodifiedStats(battle: anytype, who: Player) *Stats(u16) {
|
|
2499
|
+
const side = battle.side(who);
|
|
2552
2500
|
if (!side.active.volatiles.Transform) return &side.stored().stats;
|
|
2553
2501
|
const id = ID.from(side.active.volatiles.transform);
|
|
2554
2502
|
return &battle.side(id.player).pokemon[id.id - 1].stats;
|
|
@@ -2562,7 +2510,7 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
|
|
|
2562
2510
|
}
|
|
2563
2511
|
}
|
|
2564
2512
|
|
|
2565
|
-
|
|
2513
|
+
fn isForced(active: ActivePokemon) bool {
|
|
2566
2514
|
return active.volatiles.Recharging or active.volatiles.Rage or
|
|
2567
2515
|
active.volatiles.Thrashing or active.volatiles.Charging;
|
|
2568
2516
|
}
|
|
@@ -2614,140 +2562,68 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
|
|
|
2614
2562
|
}
|
|
2615
2563
|
}
|
|
2616
2564
|
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
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;
|
|
2565
|
+
fn disabled(side: *Side, ident: ID, log: anytype) !bool {
|
|
2566
|
+
if (side.active.volatiles.disabled_move != 0) {
|
|
2567
|
+
// A Pokémon that transforms after being disabled may end up with less move slots
|
|
2568
|
+
const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
|
|
2569
|
+
if (m != .None and m == side.last_selected_move) {
|
|
2570
|
+
side.active.volatiles.Charging = false;
|
|
2571
|
+
try log.disabled(ident, side.last_selected_move);
|
|
2572
|
+
return true;
|
|
2633
2573
|
}
|
|
2634
2574
|
}
|
|
2575
|
+
return false;
|
|
2576
|
+
}
|
|
2635
2577
|
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
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
|
-
}
|
|
2645
|
-
|
|
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
|
-
}
|
|
2650
|
-
|
|
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
|
-
}
|
|
2655
|
-
|
|
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
|
-
}
|
|
2578
|
+
fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
|
|
2579
|
+
var volatiles = &active.volatiles;
|
|
2580
|
+
assert(volatiles.Thrashing);
|
|
2581
|
+
if (volatiles.attacks > 0) return false;
|
|
2660
2582
|
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2583
|
+
volatiles.Thrashing = false;
|
|
2584
|
+
volatiles.Confusion = true;
|
|
2585
|
+
volatiles.confusion = @intCast(u3, if (showdown)
|
|
2586
|
+
battle.rng.range(u8, 2, 6)
|
|
2587
|
+
else
|
|
2588
|
+
(battle.rng.next() & 3) + 2);
|
|
2665
2589
|
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
return battle.rng.next() < 1 + (if (low) Gen12.percent(20) else Gen12.percent(40));
|
|
2669
|
-
}
|
|
2590
|
+
return true;
|
|
2591
|
+
}
|
|
2670
2592
|
|
|
2671
|
-
|
|
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
|
-
}
|
|
2593
|
+
const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
|
|
2678
2594
|
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2595
|
+
fn distribution(battle: anytype) u3 {
|
|
2596
|
+
if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
|
|
2597
|
+
const r = (battle.rng.next() & 3);
|
|
2598
|
+
return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
|
|
2599
|
+
}
|
|
2683
2600
|
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
if (
|
|
2699
|
-
r -= 1;
|
|
2700
|
-
if (r == 0) break;
|
|
2701
|
-
}
|
|
2601
|
+
fn randomMoveSlot(rand: anytype, moves: []MoveSlot, check_pp: u4) u4 {
|
|
2602
|
+
if (showdown) {
|
|
2603
|
+
if (check_pp == 0) {
|
|
2604
|
+
var i: usize = moves.len;
|
|
2605
|
+
while (i > 0) {
|
|
2606
|
+
i -= 1;
|
|
2607
|
+
if (moves[i].id != .None) return rand.range(u4, 0, @intCast(u4, i + 1)) + 1;
|
|
2608
|
+
}
|
|
2609
|
+
} else {
|
|
2610
|
+
var r = rand.range(u4, 0, @intCast(u4, check_pp)) + 1;
|
|
2611
|
+
var i: usize = 0;
|
|
2612
|
+
while (i < moves.len and r > 0) : (i += 1) {
|
|
2613
|
+
if (moves[i].pp > 0) {
|
|
2614
|
+
r -= 1;
|
|
2615
|
+
if (r == 0) break;
|
|
2702
2616
|
}
|
|
2703
|
-
return @intCast(u4, i + 1);
|
|
2704
2617
|
}
|
|
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;
|
|
2618
|
+
return @intCast(u4, i + 1);
|
|
2710
2619
|
}
|
|
2711
2620
|
}
|
|
2712
2621
|
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2622
|
+
while (true) {
|
|
2623
|
+
const r = @intCast(u4, rand.next() & 3);
|
|
2624
|
+
if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
|
|
2716
2625
|
}
|
|
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
|
-
};
|
|
2626
|
+
}
|
|
2751
2627
|
|
|
2752
2628
|
test "RNG agreement" {
|
|
2753
2629
|
if (!showdown) return;
|
|
@@ -2847,11 +2723,9 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
2847
2723
|
while (slot <= 4) : (slot += 1) {
|
|
2848
2724
|
const m = active.moves[slot - 1];
|
|
2849
2725
|
if (m.id == .None) break;
|
|
2850
|
-
if (m.id ==
|
|
2851
|
-
// Pokémon Showdown displays Struggle if limited to Bide
|
|
2852
|
-
const
|
|
2853
|
-
m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);
|
|
2854
|
-
const s = if (struggle) 0 else slot;
|
|
2726
|
+
if (m.id == side.last_selected_move) {
|
|
2727
|
+
// Pokémon Showdown displays Struggle if limited to Bide when out of PP
|
|
2728
|
+
const s = if (m.pp == 0 and m.id == .Bide) 0 else slot;
|
|
2855
2729
|
out[n] = .{ .type = .Move, .data = s };
|
|
2856
2730
|
n += 1;
|
|
2857
2731
|
return n;
|