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