@pkmn/engine 0.1.0-dev.9a0c07c3 → 0.1.0-dev.af1565df
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 -22
- package/build/pkg/addon.d.ts +7 -0
- package/build/pkg/addon.js +111 -0
- package/build/pkg/addon.js.map +1 -0
- package/build/pkg/data/layout.json +1 -1
- package/build/pkg/gen1.d.ts +5 -4
- package/build/pkg/gen1.js +43 -24
- package/build/pkg/gen1.js.map +1 -1
- package/build/pkg/index.d.ts +15 -12
- package/build/pkg/index.js.map +1 -1
- package/build/pkg/protocol.d.ts +4 -2
- package/build/pkg/protocol.js +4 -2
- package/build/pkg/protocol.js.map +1 -1
- package/build/test/display.js +31 -13
- package/build/test/display.js.map +1 -1
- package/build.zig +17 -22
- package/package.json +13 -9
- package/src/bin/install-pkmn-engine +3 -3
- package/src/bin/pkmn-debug +34 -7
- package/src/lib/binding/node.zig +5 -5
- package/src/lib/binding/wasm.zig +3 -3
- package/src/lib/common/debug.zig +1 -0
- package/src/lib/common/options.zig +3 -3
- package/src/lib/common/protocol.zig +38 -38
- package/src/lib/gen1/README.md +99 -71
- package/src/lib/gen1/data.zig +19 -17
- package/src/lib/gen1/helpers.zig +4 -15
- package/src/lib/gen1/mechanics.zig +378 -304
- package/src/lib/gen2/data.zig +4 -4
- package/src/lib/pkmn.zig +7 -7
|
@@ -56,30 +56,34 @@ const BOOSTS = &[_][2]u8{
|
|
|
56
56
|
|
|
57
57
|
const MAX_STAT_VALUE = 999;
|
|
58
58
|
|
|
59
|
+
const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
|
|
60
|
+
|
|
59
61
|
pub fn update(battle: anytype, c1: Choice, c2: Choice, log: anytype) !Result {
|
|
60
62
|
assert(c1.type != .Pass or c2.type != .Pass or battle.turn == 0);
|
|
61
63
|
if (battle.turn == 0) return start(battle, log);
|
|
62
64
|
|
|
65
|
+
var s1 = false;
|
|
66
|
+
var s2 = false;
|
|
63
67
|
var f1: ?Move = null;
|
|
64
68
|
var f2: ?Move = null;
|
|
65
69
|
|
|
66
|
-
if (selectMove(battle, .P1, c1, c2, &f1)) |r| return r;
|
|
67
|
-
if (selectMove(battle, .P2, c2, c1, &f2)) |r| return r;
|
|
70
|
+
if (selectMove(battle, .P1, c1, c2, &s1, &f1)) |r| return r;
|
|
71
|
+
if (selectMove(battle, .P2, c2, c1, &s2, &f2)) |r| return r;
|
|
72
|
+
|
|
73
|
+
var p1 = battle.side(.P1);
|
|
74
|
+
var p2 = battle.side(.P2);
|
|
75
|
+
|
|
76
|
+
var r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
|
|
77
|
+
var r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
|
|
68
78
|
|
|
69
79
|
if (turnOrder(battle, c1, c2) == .P1) {
|
|
70
|
-
if (try doTurn(battle, .P1, c1, f1, .P2, c2, f2, log)) |r| return r;
|
|
80
|
+
if (try doTurn(battle, .P1, c1, r1, s1, f1, .P2, c2, r2, s2, f2, log)) |r| return r;
|
|
71
81
|
} else {
|
|
72
|
-
if (try doTurn(battle, .P2, c2, f2, .P1, c1, f1, log)) |r| return r;
|
|
82
|
+
if (try doTurn(battle, .P2, c2, r2, s2, f2, .P1, c1, r1, s1, f1, log)) |r| return r;
|
|
73
83
|
}
|
|
74
84
|
|
|
75
|
-
|
|
76
|
-
if (
|
|
77
|
-
p1.active.volatiles.Binding = false;
|
|
78
|
-
}
|
|
79
|
-
var p2 = battle.side(.P2);
|
|
80
|
-
if (p2.active.volatiles.attacks == 0) {
|
|
81
|
-
p2.active.volatiles.Binding = false;
|
|
82
|
-
}
|
|
85
|
+
if (p1.active.volatiles.attacks == 0) p1.active.volatiles.Binding = false;
|
|
86
|
+
if (p2.active.volatiles.attacks == 0) p2.active.volatiles.Binding = false;
|
|
83
87
|
|
|
84
88
|
return endTurn(battle, log);
|
|
85
89
|
}
|
|
@@ -112,6 +116,7 @@ fn selectMove(
|
|
|
112
116
|
player: Player,
|
|
113
117
|
choice: Choice,
|
|
114
118
|
foe_choice: Choice,
|
|
119
|
+
skip_turn: *bool,
|
|
115
120
|
from: *?Move,
|
|
116
121
|
) ?Result {
|
|
117
122
|
if (choice.type == .Pass) return null;
|
|
@@ -124,17 +129,26 @@ fn selectMove(
|
|
|
124
129
|
(choice.type == .Move and choice.data == @boolToInt(showdown)));
|
|
125
130
|
|
|
126
131
|
// pre-battle menu
|
|
127
|
-
if (volatiles.Recharging)
|
|
132
|
+
if (volatiles.Recharging) {
|
|
133
|
+
if (showdown and battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
128
136
|
if (volatiles.Rage) {
|
|
129
137
|
from.* = side.last_used_move;
|
|
130
|
-
if (showdown)
|
|
138
|
+
if (showdown) {
|
|
139
|
+
if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
|
|
140
|
+
saveMove(battle, player, null);
|
|
141
|
+
}
|
|
131
142
|
return null;
|
|
132
143
|
}
|
|
133
144
|
// Pokémon Showdown removes Flinch at the end-of-turn in its residual handler
|
|
134
145
|
if (!showdown) volatiles.Flinch = false;
|
|
135
146
|
if (volatiles.Thrashing or volatiles.Charging) {
|
|
136
147
|
from.* = side.last_selected_move;
|
|
137
|
-
if (showdown)
|
|
148
|
+
if (showdown) {
|
|
149
|
+
if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
|
|
150
|
+
saveMove(battle, player, null);
|
|
151
|
+
}
|
|
138
152
|
return null;
|
|
139
153
|
}
|
|
140
154
|
|
|
@@ -144,7 +158,10 @@ fn selectMove(
|
|
|
144
158
|
// pre-move select
|
|
145
159
|
if (Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP) or volatiles.Bide) {
|
|
146
160
|
assert(showdown or choice.data == 0);
|
|
147
|
-
if (showdown)
|
|
161
|
+
if (showdown) {
|
|
162
|
+
if (volatiles.Bide and battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
|
|
163
|
+
saveMove(battle, player, choice);
|
|
164
|
+
}
|
|
148
165
|
return null;
|
|
149
166
|
}
|
|
150
167
|
if (volatiles.Binding) {
|
|
@@ -163,9 +180,9 @@ fn selectMove(
|
|
|
163
180
|
// GLITCH: https://glitchcity.wiki/Partial_trapping_move_Mirror_Move_link_battle_glitch
|
|
164
181
|
if (foe_choice.type == .Switch) {
|
|
165
182
|
const slot = if (player == .P1)
|
|
166
|
-
battle.
|
|
183
|
+
battle.last_moves.p1_index
|
|
167
184
|
else
|
|
168
|
-
battle.
|
|
185
|
+
battle.last_moves.p2_index;
|
|
169
186
|
const last = side.active.move(@intCast(u4, slot));
|
|
170
187
|
if (last.id == .Metronome) side.last_selected_move = last.id;
|
|
171
188
|
if (last.id == .MirrorMove) return Result.Error;
|
|
@@ -175,6 +192,7 @@ fn selectMove(
|
|
|
175
192
|
}
|
|
176
193
|
|
|
177
194
|
if (battle.foe(player).active.volatiles.Binding) {
|
|
195
|
+
skip_turn.* = true;
|
|
178
196
|
if (showdown) {
|
|
179
197
|
saveMove(battle, player, choice);
|
|
180
198
|
} else {
|
|
@@ -185,6 +203,7 @@ fn selectMove(
|
|
|
185
203
|
}
|
|
186
204
|
|
|
187
205
|
// move select
|
|
206
|
+
volatiles.state = 0;
|
|
188
207
|
if (choice.data == 0) {
|
|
189
208
|
const struggle = ok: {
|
|
190
209
|
for (side.active.moves, 0..) |move, i| {
|
|
@@ -218,9 +237,9 @@ fn saveMove(battle: anytype, player: Player, choice: ?Choice) void {
|
|
|
218
237
|
|
|
219
238
|
side.last_selected_move = move.id;
|
|
220
239
|
if (player == .P1) {
|
|
221
|
-
battle.
|
|
240
|
+
battle.last_moves.p1_index = @intCast(u3, c.data);
|
|
222
241
|
} else {
|
|
223
|
-
battle.
|
|
242
|
+
battle.last_moves.p2_index = @intCast(u3, c.data);
|
|
224
243
|
}
|
|
225
244
|
}
|
|
226
245
|
}
|
|
@@ -240,9 +259,9 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, log: anyty
|
|
|
240
259
|
side.order[slot - 1] = out;
|
|
241
260
|
|
|
242
261
|
if (player == .P1) {
|
|
243
|
-
battle.
|
|
262
|
+
battle.last_moves.p1_index = 1;
|
|
244
263
|
} else {
|
|
245
|
-
battle.
|
|
264
|
+
battle.last_moves.p2_index = 1;
|
|
246
265
|
}
|
|
247
266
|
|
|
248
267
|
side.last_used_move = .None;
|
|
@@ -299,13 +318,11 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
|
|
|
299
318
|
const spe2 = battle.side(.P2).active.stats.spe;
|
|
300
319
|
if (spe1 == spe2) {
|
|
301
320
|
// Pokémon Showdown's beforeTurnCallback shenanigans
|
|
302
|
-
if (showdown and m1 == .Counter and m2 == .Counter)
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
battle.rng.range(u8, 0, 2) == 0
|
|
306
|
-
else
|
|
307
|
-
battle.rng.next() < Gen12.percent(50) + 1;
|
|
321
|
+
if (showdown and !double_switch and m1 == .Counter and m2 == .Counter) {
|
|
322
|
+
battle.rng.advance(1);
|
|
323
|
+
}
|
|
308
324
|
|
|
325
|
+
const p1 = Rolls.speedTie(battle);
|
|
309
326
|
if (!showdown) return if (p1) .P1 else .P2;
|
|
310
327
|
|
|
311
328
|
// Pokémon Showdown's "lockedmove" volatile's onBeforeTurn uses BattleQueue#changeAction,
|
|
@@ -315,7 +332,7 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
|
|
|
315
332
|
const t1 = battle.side(.P1).active.volatiles.Thrashing;
|
|
316
333
|
const t2 = battle.side(.P2).active.volatiles.Thrashing;
|
|
317
334
|
// If *both* sides are thrashing it really should be another speed tie, but we've patched
|
|
318
|
-
// that out and enforce host ordering of events, so P1 just goes first
|
|
335
|
+
// that out and enforce host ordering of events, so P1 just goes first regardless of who
|
|
319
336
|
// won the original coin flip
|
|
320
337
|
if (t1 and t2) return .P1;
|
|
321
338
|
return if (p1) if (t1 and !t2) .P2 else .P1 else if (t2 and !t1) .P1 else .P2;
|
|
@@ -328,9 +345,13 @@ fn doTurn(
|
|
|
328
345
|
battle: anytype,
|
|
329
346
|
player: Player,
|
|
330
347
|
player_choice: Choice,
|
|
348
|
+
player_rewrap: bool,
|
|
349
|
+
player_skip: bool,
|
|
331
350
|
player_from: ?Move,
|
|
332
351
|
foe_player: Player,
|
|
333
352
|
foe_choice: Choice,
|
|
353
|
+
foe_rewrap: bool,
|
|
354
|
+
foe_skip: bool,
|
|
334
355
|
foe_from: ?Move,
|
|
335
356
|
log: anytype,
|
|
336
357
|
) !?Result {
|
|
@@ -338,7 +359,16 @@ fn doTurn(
|
|
|
338
359
|
|
|
339
360
|
var residual = true;
|
|
340
361
|
var replace = battle.side(player).stored().hp == 0;
|
|
341
|
-
if (try executeMove(
|
|
362
|
+
if (try executeMove(
|
|
363
|
+
battle,
|
|
364
|
+
player,
|
|
365
|
+
player_choice,
|
|
366
|
+
player_rewrap,
|
|
367
|
+
player_skip,
|
|
368
|
+
player_from,
|
|
369
|
+
&residual,
|
|
370
|
+
log,
|
|
371
|
+
)) |r| return r;
|
|
342
372
|
if (!replace) {
|
|
343
373
|
if (player_choice.type != .Switch) {
|
|
344
374
|
if (try checkFaint(battle, foe_player, log)) |r| return r;
|
|
@@ -349,7 +379,16 @@ fn doTurn(
|
|
|
349
379
|
|
|
350
380
|
residual = true;
|
|
351
381
|
replace = battle.side(foe_player).stored().hp == 0;
|
|
352
|
-
if (try executeMove(
|
|
382
|
+
if (try executeMove(
|
|
383
|
+
battle,
|
|
384
|
+
foe_player,
|
|
385
|
+
foe_choice,
|
|
386
|
+
foe_rewrap,
|
|
387
|
+
foe_skip,
|
|
388
|
+
foe_from,
|
|
389
|
+
&residual,
|
|
390
|
+
log,
|
|
391
|
+
)) |r| return r;
|
|
353
392
|
if (!replace) {
|
|
354
393
|
if (foe_choice.type != .Switch) {
|
|
355
394
|
if (try checkFaint(battle, player, log)) |r| return r;
|
|
@@ -372,6 +411,8 @@ fn executeMove(
|
|
|
372
411
|
battle: anytype,
|
|
373
412
|
player: Player,
|
|
374
413
|
choice: Choice,
|
|
414
|
+
rewrap: bool,
|
|
415
|
+
skip: bool,
|
|
375
416
|
from: ?Move,
|
|
376
417
|
residual: *bool,
|
|
377
418
|
log: anytype,
|
|
@@ -383,9 +424,10 @@ fn executeMove(
|
|
|
383
424
|
return null;
|
|
384
425
|
}
|
|
385
426
|
|
|
427
|
+
// This is the correct place to check for SKIP_TURN and abort early, however since Pokémon
|
|
428
|
+
// Showdown overwrites the SKIP_TURN sentinel with its botched move select we need to add an
|
|
429
|
+
// additional skip boolean to accomplish the same thing in the Binding check of BeforeMove
|
|
386
430
|
if (side.last_selected_move == .SKIP_TURN) {
|
|
387
|
-
// Pokémon Showdown overwrites the SKIP_TURN sentinel with its botched move select,
|
|
388
|
-
// Binding instead gets handled in beforeMove after sleep and freeze
|
|
389
431
|
assert(!showdown);
|
|
390
432
|
if (battle.foe(player).active.volatiles.Binding) {
|
|
391
433
|
try log.cant(battle.active(player), .Bound);
|
|
@@ -404,9 +446,9 @@ fn executeMove(
|
|
|
404
446
|
// choice.data == 0 only happens with Struggle on Pokémon Showdown
|
|
405
447
|
assert(!showdown);
|
|
406
448
|
mslot = @intCast(u4, if (player == .P1)
|
|
407
|
-
battle.
|
|
449
|
+
battle.last_moves.p1_index
|
|
408
450
|
else
|
|
409
|
-
battle.
|
|
451
|
+
battle.last_moves.p2_index);
|
|
410
452
|
const stored = side.stored();
|
|
411
453
|
// GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
|
|
412
454
|
auto = side.last_selected_move == .HyperBeam or side.active.volatiles.Bide or
|
|
@@ -428,14 +470,14 @@ fn executeMove(
|
|
|
428
470
|
// Incorrect mslot with Pokémon Showdown choice semantics so we need to recover from index
|
|
429
471
|
assert(mslot == 1);
|
|
430
472
|
mslot = @intCast(u4, if (player == .P1)
|
|
431
|
-
battle.
|
|
473
|
+
battle.last_moves.p1_index
|
|
432
474
|
else
|
|
433
|
-
battle.
|
|
475
|
+
battle.last_moves.p2_index);
|
|
434
476
|
}
|
|
435
477
|
|
|
436
478
|
var skip_can = false;
|
|
437
479
|
var skip_pp = false;
|
|
438
|
-
switch (try beforeMove(battle, player, from, residual, log)) {
|
|
480
|
+
switch (try beforeMove(battle, player, skip, from, residual, log)) {
|
|
439
481
|
.done => return null,
|
|
440
482
|
.skip_can => skip_can = true,
|
|
441
483
|
.skip_pp => skip_pp = true,
|
|
@@ -447,14 +489,15 @@ fn executeMove(
|
|
|
447
489
|
return null;
|
|
448
490
|
}
|
|
449
491
|
|
|
450
|
-
return doMove(battle, player, mslot, auto, residual, log);
|
|
492
|
+
return doMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
451
493
|
}
|
|
452
494
|
|
|
453
|
-
const BeforeMove =
|
|
495
|
+
const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
|
|
454
496
|
|
|
455
497
|
fn beforeMove(
|
|
456
498
|
battle: anytype,
|
|
457
499
|
player: Player,
|
|
500
|
+
skip: bool,
|
|
458
501
|
from: ?Move,
|
|
459
502
|
residual: *bool,
|
|
460
503
|
log: anytype,
|
|
@@ -486,13 +529,15 @@ fn beforeMove(
|
|
|
486
529
|
return .done;
|
|
487
530
|
}
|
|
488
531
|
|
|
489
|
-
if (foe.active.volatiles.Binding) {
|
|
532
|
+
if (skip or foe.active.volatiles.Binding) {
|
|
490
533
|
try log.cant(ident, .Bound);
|
|
491
534
|
return .done;
|
|
492
535
|
}
|
|
493
536
|
|
|
494
|
-
if (
|
|
495
|
-
|
|
537
|
+
if (volatiles.Flinch) {
|
|
538
|
+
// Pokémon Showdown doesn't clear Flinch until its imaginary "residual" phase, meaning
|
|
539
|
+
// Pokémon can sometimes flinch multiple times from the same original hit
|
|
540
|
+
if (!showdown) volatiles.Flinch = false;
|
|
496
541
|
try log.cant(ident, .Flinch);
|
|
497
542
|
return .done;
|
|
498
543
|
}
|
|
@@ -513,13 +558,6 @@ fn beforeMove(
|
|
|
513
558
|
// Pokémon Showdown's disable condition has a single onBeforeMove handler
|
|
514
559
|
if (showdown and try disabled(side, ident, log)) return .done;
|
|
515
560
|
|
|
516
|
-
// Pokémon Showdown checks for Flinch *after* instead of before
|
|
517
|
-
if (showdown and volatiles.Flinch) {
|
|
518
|
-
volatiles.Flinch = false;
|
|
519
|
-
try log.cant(ident, .Flinch);
|
|
520
|
-
return .done;
|
|
521
|
-
}
|
|
522
|
-
|
|
523
561
|
// This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
|
|
524
562
|
// before the side had a selected a move - we simply need to assume this leads to a desync
|
|
525
563
|
if (side.last_selected_move == .None) {
|
|
@@ -537,12 +575,7 @@ fn beforeMove(
|
|
|
537
575
|
} else {
|
|
538
576
|
try log.activate(ident, .Confusion);
|
|
539
577
|
|
|
540
|
-
|
|
541
|
-
!battle.rng.chance(u8, 128, 256)
|
|
542
|
-
else
|
|
543
|
-
battle.rng.next() >= Gen12.percent(50) + 1;
|
|
544
|
-
|
|
545
|
-
if (confused) {
|
|
578
|
+
if (Rolls.confused(battle)) {
|
|
546
579
|
assert(!volatiles.MultiHit);
|
|
547
580
|
if (!volatiles.Rage) volatiles.state = 0;
|
|
548
581
|
volatiles.Bide = false;
|
|
@@ -568,8 +601,12 @@ fn beforeMove(
|
|
|
568
601
|
player
|
|
569
602
|
else
|
|
570
603
|
player.foe();
|
|
604
|
+
|
|
605
|
+
const uncapped = battle.last_damage;
|
|
571
606
|
// Skipping adjustDamage / randomizeDamage / checkHit
|
|
572
607
|
_ = try applyDamage(battle, player, target, .Confusion, log);
|
|
608
|
+
// Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
|
|
609
|
+
if (showdown) battle.last_damage = uncapped;
|
|
573
610
|
|
|
574
611
|
return .done;
|
|
575
612
|
}
|
|
@@ -578,28 +615,28 @@ fn beforeMove(
|
|
|
578
615
|
|
|
579
616
|
if (!showdown and try disabled(side, ident, log)) return .done;
|
|
580
617
|
|
|
581
|
-
if (Status.is(stored.status, .PAR)) {
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
volatiles.Thrashing = false;
|
|
591
|
-
volatiles.Charging = false;
|
|
592
|
-
volatiles.Binding = false;
|
|
593
|
-
// GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
|
|
594
|
-
try log.cant(ident, .Paralysis);
|
|
595
|
-
return .done;
|
|
596
|
-
}
|
|
618
|
+
if (Status.is(stored.status, .PAR) and Rolls.paralyzed(battle)) {
|
|
619
|
+
if (!volatiles.Rage) volatiles.state = 0;
|
|
620
|
+
volatiles.Bide = false;
|
|
621
|
+
volatiles.Thrashing = false;
|
|
622
|
+
volatiles.Charging = false;
|
|
623
|
+
volatiles.Binding = false;
|
|
624
|
+
// GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
|
|
625
|
+
try log.cant(ident, .Paralysis);
|
|
626
|
+
return .done;
|
|
597
627
|
}
|
|
598
628
|
|
|
599
629
|
if (volatiles.Bide) {
|
|
600
630
|
assert(!volatiles.Thrashing and !volatiles.Rage);
|
|
601
631
|
|
|
602
|
-
|
|
632
|
+
if (showdown) {
|
|
633
|
+
// Pokémon Showdown doesn't implement Bide potentially overflowing in the event of
|
|
634
|
+
// OHKO-move damage, but we can fake this incorrect behavior by simply saturating the
|
|
635
|
+
// addition because 65535 is sufficient to faint any Pokémon anyway
|
|
636
|
+
volatiles.state +|= battle.last_damage;
|
|
637
|
+
} else {
|
|
638
|
+
volatiles.state +%= battle.last_damage;
|
|
639
|
+
}
|
|
603
640
|
|
|
604
641
|
assert(volatiles.attacks > 0);
|
|
605
642
|
|
|
@@ -643,8 +680,13 @@ fn beforeMove(
|
|
|
643
680
|
try log.start(battle.active(player), .ConfusionSilent);
|
|
644
681
|
}
|
|
645
682
|
try log.move(ident, side.last_selected_move, battle.active(player.foe()), from);
|
|
646
|
-
if (showdown
|
|
647
|
-
|
|
683
|
+
if (showdown) {
|
|
684
|
+
// This shouldn't actually set last_used_move, but Pokémon Showdown sets last
|
|
685
|
+
// used in useMove and doesn't have the notion of skipping canMove semantics
|
|
686
|
+
side.last_used_move = side.last_selected_move;
|
|
687
|
+
if (handleThrashing(battle, active)) {
|
|
688
|
+
try log.start(battle.active(player), .ConfusionSilent);
|
|
689
|
+
}
|
|
648
690
|
}
|
|
649
691
|
return .skip_can;
|
|
650
692
|
}
|
|
@@ -659,7 +701,11 @@ fn beforeMove(
|
|
|
659
701
|
side.last_selected_move,
|
|
660
702
|
);
|
|
661
703
|
if (showdown or battle.last_damage != 0) {
|
|
704
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
662
705
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
706
|
+
if (battle.foe(player).stored().hp > 0 and !sub) {
|
|
707
|
+
try buildRage(battle, player.foe(), log);
|
|
708
|
+
}
|
|
663
709
|
}
|
|
664
710
|
return .done;
|
|
665
711
|
}
|
|
@@ -691,6 +737,7 @@ fn canMove(
|
|
|
691
737
|
side.active.volatiles.Invulnerable = false;
|
|
692
738
|
} else if (move.effect == .Charge) {
|
|
693
739
|
try log.move(player_ident, side.last_selected_move, .{}, from);
|
|
740
|
+
setCounterable(battle, player, side, move);
|
|
694
741
|
try Effects.charge(battle, player, log);
|
|
695
742
|
return false;
|
|
696
743
|
}
|
|
@@ -707,6 +754,7 @@ fn canMove(
|
|
|
707
754
|
// regrettable, but this information is more "nice to have" than required
|
|
708
755
|
const f = if (from != null and from.? == .None) null else from;
|
|
709
756
|
try log.move(player_ident, side.last_selected_move, battle.active(target), f);
|
|
757
|
+
setCounterable(battle, player, side, move);
|
|
710
758
|
|
|
711
759
|
if (move.effect.onBegin()) {
|
|
712
760
|
try onBegin(battle, player, move, mslot, residual, log);
|
|
@@ -717,12 +765,27 @@ fn canMove(
|
|
|
717
765
|
Effects.thrashing(battle, player);
|
|
718
766
|
} else if (!showdown and move.effect == .Binding) {
|
|
719
767
|
// Pokémon Showdown handles this after hit/miss checks and damage calculation
|
|
720
|
-
Effects.binding(battle, player);
|
|
768
|
+
Effects.binding(battle, player, false);
|
|
721
769
|
}
|
|
722
770
|
|
|
723
771
|
return true;
|
|
724
772
|
}
|
|
725
773
|
|
|
774
|
+
fn setCounterable(battle: anytype, player: Player, side: *Side, move: Move.Data) void {
|
|
775
|
+
// The Counter desync is caused by the cartridge not calling GetCurrentMove until now, meaning
|
|
776
|
+
// in cases where an early return happens the data for a players last selected move does not get
|
|
777
|
+
// reloaded and HandleCounterMove actually bases its success/failure off of stale information.
|
|
778
|
+
// This boolean state we track here doesn't exist on the cartridge because it instead manifests
|
|
779
|
+
// as the desync results from actually having two separate battle states that subtly disagree
|
|
780
|
+
const counterable = side.last_selected_move != .Counter and move.bp > 0 and
|
|
781
|
+
(move.type == .Normal or move.type == .Fighting);
|
|
782
|
+
if (player == .P1) {
|
|
783
|
+
battle.last_moves.p1_counterable = @boolToInt(counterable);
|
|
784
|
+
} else {
|
|
785
|
+
battle.last_moves.p2_counterable = @boolToInt(counterable);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
726
789
|
fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
|
|
727
790
|
if (side.last_selected_move == .Struggle) return;
|
|
728
791
|
|
|
@@ -758,6 +821,7 @@ fn doMove(
|
|
|
758
821
|
battle: anytype,
|
|
759
822
|
player: Player,
|
|
760
823
|
mslot: u4,
|
|
824
|
+
rewrap: bool,
|
|
761
825
|
auto: bool,
|
|
762
826
|
residual: *bool,
|
|
763
827
|
log: anytype,
|
|
@@ -796,7 +860,7 @@ fn doMove(
|
|
|
796
860
|
};
|
|
797
861
|
assert(!immune or miss or (showdown and move.effect == .Binding));
|
|
798
862
|
|
|
799
|
-
var late = showdown;
|
|
863
|
+
var late = showdown and move.effect != .Explode;
|
|
800
864
|
const skip = status or immune;
|
|
801
865
|
if ((!showdown or (!skip or counter)) and !miss) blk: {
|
|
802
866
|
if (showdown and move.effect.isMulti()) {
|
|
@@ -849,9 +913,9 @@ fn doMove(
|
|
|
849
913
|
|
|
850
914
|
if (!showdown or !miss) {
|
|
851
915
|
if (move.effect == .MirrorMove) {
|
|
852
|
-
return mirrorMove(battle, player, mslot, auto, residual, log);
|
|
916
|
+
return mirrorMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
853
917
|
} else if (move.effect == .Metronome) {
|
|
854
|
-
return metronome(battle, player, mslot, auto, residual, log);
|
|
918
|
+
return metronome(battle, player, mslot, rewrap, auto, residual, log);
|
|
855
919
|
} else if (move.effect.onEnd()) {
|
|
856
920
|
try onEnd(battle, player, move, log);
|
|
857
921
|
return null;
|
|
@@ -866,7 +930,7 @@ fn doMove(
|
|
|
866
930
|
move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
|
|
867
931
|
if (ohko) {
|
|
868
932
|
try log.immune(foe_ident, .OHKO);
|
|
869
|
-
} else if (immune and !invulnerable) {
|
|
933
|
+
} else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
|
|
870
934
|
try log.immune(foe_ident, .None);
|
|
871
935
|
} else if (mist) {
|
|
872
936
|
if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
|
|
@@ -916,7 +980,7 @@ fn doMove(
|
|
|
916
980
|
if (hit == 0 and ohko) try log.ohko();
|
|
917
981
|
hit += 1;
|
|
918
982
|
if (foe.stored().hp == 0) break;
|
|
919
|
-
if (!late and !sub) try buildRage(battle, player.foe(), log);
|
|
983
|
+
if (!late and (!sub or move.effect == .Explode)) try buildRage(battle, player.foe(), log);
|
|
920
984
|
// If the substitute breaks during a multi-hit attack, the attack ends
|
|
921
985
|
if (nullified) break;
|
|
922
986
|
}
|
|
@@ -932,11 +996,17 @@ fn doMove(
|
|
|
932
996
|
} else if (showdown) {
|
|
933
997
|
// This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
|
|
934
998
|
if (move.effect == .Binding) {
|
|
935
|
-
Effects.binding(battle, player);
|
|
999
|
+
Effects.binding(battle, player, rewrap);
|
|
936
1000
|
if (immune) {
|
|
937
1001
|
battle.last_damage = 0;
|
|
1002
|
+
assert(foe.stored().hp > 0);
|
|
938
1003
|
// Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
|
|
939
|
-
|
|
1004
|
+
if (sub) {
|
|
1005
|
+
try log.activate(battle.active(player.foe()), .Substitute);
|
|
1006
|
+
} else {
|
|
1007
|
+
try log.damage(battle.active(player.foe()), foe.stored(), .None);
|
|
1008
|
+
try buildRage(battle, player.foe(), log);
|
|
1009
|
+
}
|
|
940
1010
|
return null;
|
|
941
1011
|
}
|
|
942
1012
|
}
|
|
@@ -987,8 +1057,7 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
|
|
|
987
1057
|
chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
|
|
988
1058
|
chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
|
|
989
1059
|
|
|
990
|
-
|
|
991
|
-
return std.math.rotl(u8, battle.rng.next(), 3) < chance;
|
|
1060
|
+
return Rolls.criticalHit(battle, chance);
|
|
992
1061
|
}
|
|
993
1062
|
|
|
994
1063
|
fn calcDamage(
|
|
@@ -1092,16 +1161,7 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
|
1092
1161
|
|
|
1093
1162
|
fn randomizeDamage(battle: anytype) void {
|
|
1094
1163
|
if (battle.last_damage <= 1) return;
|
|
1095
|
-
|
|
1096
|
-
const random = if (showdown)
|
|
1097
|
-
battle.rng.range(u8, 217, 256)
|
|
1098
|
-
else loop: {
|
|
1099
|
-
while (true) {
|
|
1100
|
-
const r = std.math.rotr(u8, battle.rng.next(), 1);
|
|
1101
|
-
if (r >= 217) break :loop r;
|
|
1102
|
-
}
|
|
1103
|
-
};
|
|
1104
|
-
|
|
1164
|
+
const random = Rolls.damage(battle);
|
|
1105
1165
|
battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
|
|
1106
1166
|
}
|
|
1107
1167
|
|
|
@@ -1119,16 +1179,9 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
|
|
|
1119
1179
|
// GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
|
|
1120
1180
|
.Psywave => power: {
|
|
1121
1181
|
const max = @intCast(u8, @as(u16, side.stored().level) * 3 / 2);
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
// GLITCH: Psywave infinite glitch loop
|
|
1126
|
-
if (max <= 1) return Result.Error;
|
|
1127
|
-
while (true) {
|
|
1128
|
-
const r = battle.rng.next();
|
|
1129
|
-
if (r < max) break :power r;
|
|
1130
|
-
}
|
|
1131
|
-
}
|
|
1182
|
+
// GLITCH: Psywave infinite glitch loop
|
|
1183
|
+
if (!showdown and max <= 1) return Result.Error;
|
|
1184
|
+
break :power Rolls.psywave(battle, max);
|
|
1132
1185
|
},
|
|
1133
1186
|
else => unreachable,
|
|
1134
1187
|
};
|
|
@@ -1150,19 +1203,17 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
|
|
|
1150
1203
|
return null;
|
|
1151
1204
|
}
|
|
1152
1205
|
|
|
1153
|
-
// Pretend
|
|
1154
|
-
const foe_last_used_move =
|
|
1155
|
-
Move.get(if (foe.last_used_move == .None) .Counter else foe.last_used_move);
|
|
1206
|
+
// Pretend Splash was used as a stand-in when no move has been used to fail below with 0 BP
|
|
1156
1207
|
const foe_last_selected_move =
|
|
1157
1208
|
Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
|
|
1158
|
-
.
|
|
1209
|
+
.Splash
|
|
1159
1210
|
else
|
|
1160
1211
|
foe.last_selected_move);
|
|
1161
1212
|
|
|
1162
|
-
const used =
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1213
|
+
const used = (if (player.foe() == .P1)
|
|
1214
|
+
battle.last_moves.p1_counterable
|
|
1215
|
+
else
|
|
1216
|
+
battle.last_moves.p2_counterable) != 0;
|
|
1166
1217
|
|
|
1167
1218
|
const selected = foe_last_selected_move.bp > 0 and
|
|
1168
1219
|
foe.last_selected_move != .Counter and
|
|
@@ -1181,12 +1232,14 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
|
|
|
1181
1232
|
return null;
|
|
1182
1233
|
}
|
|
1183
1234
|
|
|
1184
|
-
battle.last_damage
|
|
1235
|
+
battle.last_damage *|= 2;
|
|
1185
1236
|
|
|
1186
1237
|
// Pokémon Showdown calls checkHit before Counter
|
|
1187
1238
|
if (!showdown and !try checkHit(battle, player, move, log)) return null;
|
|
1188
1239
|
|
|
1240
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1189
1241
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
1242
|
+
if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
|
|
1190
1243
|
return null;
|
|
1191
1244
|
}
|
|
1192
1245
|
|
|
@@ -1230,6 +1283,7 @@ fn mirrorMove(
|
|
|
1230
1283
|
battle: anytype,
|
|
1231
1284
|
player: Player,
|
|
1232
1285
|
mslot: u4,
|
|
1286
|
+
rewrap: bool,
|
|
1233
1287
|
auto: bool,
|
|
1234
1288
|
residual: *bool,
|
|
1235
1289
|
log: anytype,
|
|
@@ -1242,41 +1296,30 @@ fn mirrorMove(
|
|
|
1242
1296
|
if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
|
|
1243
1297
|
try log.fail(battle.active(player), .None);
|
|
1244
1298
|
return null;
|
|
1299
|
+
} else if (!showdown or foe.last_used_move != .Struggle) {
|
|
1300
|
+
incrementPP(side, mslot);
|
|
1245
1301
|
}
|
|
1246
1302
|
|
|
1247
|
-
incrementPP(side, mslot);
|
|
1248
|
-
|
|
1249
1303
|
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
|
|
1250
|
-
return doMove(battle, player, mslot, auto, residual, log);
|
|
1304
|
+
return doMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
1251
1305
|
}
|
|
1252
1306
|
|
|
1253
1307
|
fn metronome(
|
|
1254
1308
|
battle: anytype,
|
|
1255
1309
|
player: Player,
|
|
1256
1310
|
mslot: u4,
|
|
1311
|
+
rewrap: bool,
|
|
1257
1312
|
auto: bool,
|
|
1258
1313
|
residual: *bool,
|
|
1259
1314
|
log: anytype,
|
|
1260
1315
|
) !?Result {
|
|
1261
1316
|
var side = battle.side(player);
|
|
1262
1317
|
|
|
1263
|
-
side.last_selected_move =
|
|
1264
|
-
const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
|
|
1265
|
-
const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
|
|
1266
|
-
break :blk @intToEnum(Move, r + mod);
|
|
1267
|
-
} else loop: {
|
|
1268
|
-
while (true) {
|
|
1269
|
-
const r = battle.rng.next();
|
|
1270
|
-
if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
|
|
1271
|
-
if (r >= @enumToInt(Move.Struggle)) continue;
|
|
1272
|
-
break :loop @intToEnum(Move, r);
|
|
1273
|
-
}
|
|
1274
|
-
};
|
|
1275
|
-
|
|
1318
|
+
side.last_selected_move = Rolls.metronome(battle);
|
|
1276
1319
|
incrementPP(side, mslot);
|
|
1277
1320
|
|
|
1278
1321
|
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
|
|
1279
|
-
return doMove(battle, player, mslot, auto, residual, log);
|
|
1322
|
+
return doMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
1280
1323
|
}
|
|
1281
1324
|
|
|
1282
1325
|
fn checkHit(battle: anytype, player: Player, move: Move.Data, log: anytype) !bool {
|
|
@@ -1330,23 +1373,24 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
|
|
|
1330
1373
|
}
|
|
1331
1374
|
|
|
1332
1375
|
// GLITCH: Thrash / Petal Dance / Rage get their accuracy overwritten on subsequent hits
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1376
|
+
const state = side.active.volatiles.state;
|
|
1377
|
+
var overwrite = move.effect == .Rage or move.effect == .Thrashing;
|
|
1378
|
+
const overwritten = overwrite and state > 0;
|
|
1379
|
+
assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
|
|
1380
|
+
|
|
1381
|
+
var accuracy = if (overwritten) state else @as(u16, Gen12.percent(move.accuracy));
|
|
1337
1382
|
var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
|
|
1338
1383
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1339
1384
|
boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
|
|
1340
1385
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1341
1386
|
accuracy = @min(255, @max(1, accuracy));
|
|
1342
1387
|
|
|
1343
|
-
|
|
1388
|
+
// Pokémon Showdown only overwrites if the volatile is present
|
|
1389
|
+
if (showdown) overwrite = side.active.volatiles.Rage or side.active.volatiles.Thrashing;
|
|
1390
|
+
if (overwrite) side.active.volatiles.state = accuracy;
|
|
1344
1391
|
|
|
1345
1392
|
// GLITCH: max accuracy is 255 so 1/256 chance of miss
|
|
1346
|
-
break :miss
|
|
1347
|
-
!battle.rng.chance(u8, @intCast(u8, accuracy), 256)
|
|
1348
|
-
else
|
|
1349
|
-
battle.rng.next() >= accuracy;
|
|
1393
|
+
break :miss !Rolls.hit(battle, accuracy);
|
|
1350
1394
|
};
|
|
1351
1395
|
|
|
1352
1396
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1478,9 +1522,6 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
|
|
|
1478
1522
|
damage *= volatiles.toxic;
|
|
1479
1523
|
}
|
|
1480
1524
|
|
|
1481
|
-
// Pokémon Showdown erroneously updates last damage with uncapped Leech Seed damage
|
|
1482
|
-
if (showdown) battle.last_damage = damage;
|
|
1483
|
-
|
|
1484
1525
|
const amount = @min(damage, stored.hp);
|
|
1485
1526
|
stored.hp -= amount;
|
|
1486
1527
|
|
|
@@ -1499,13 +1540,13 @@ fn endTurn(battle: anytype, log: anytype) @TypeOf(log).Error!Result {
|
|
|
1499
1540
|
assert(!battle.side(.P1).active.volatiles.MultiHit);
|
|
1500
1541
|
assert(!battle.side(.P2).active.volatiles.MultiHit);
|
|
1501
1542
|
|
|
1543
|
+
battle.turn += 1;
|
|
1544
|
+
|
|
1502
1545
|
if (showdown and options.ebc and checkEBC(battle)) {
|
|
1503
1546
|
try log.tie();
|
|
1504
1547
|
return Result.Tie;
|
|
1505
1548
|
}
|
|
1506
1549
|
|
|
1507
|
-
battle.turn += 1;
|
|
1508
|
-
|
|
1509
1550
|
if (showdown and battle.turn >= 1000) {
|
|
1510
1551
|
try log.tie();
|
|
1511
1552
|
return Result.Tie;
|
|
@@ -1572,6 +1613,42 @@ fn checkEBC(battle: anytype) bool {
|
|
|
1572
1613
|
return false;
|
|
1573
1614
|
}
|
|
1574
1615
|
|
|
1616
|
+
fn disabled(side: *Side, ident: ID, log: anytype) !bool {
|
|
1617
|
+
if (side.active.volatiles.disabled_move != 0) {
|
|
1618
|
+
// A Pokémon that transforms after being disabled may end up with less move slots
|
|
1619
|
+
const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
|
|
1620
|
+
// side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
|
|
1621
|
+
const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
|
|
1622
|
+
m
|
|
1623
|
+
else
|
|
1624
|
+
side.last_selected_move;
|
|
1625
|
+
if (m != .None and m == last) {
|
|
1626
|
+
side.active.volatiles.Charging = false;
|
|
1627
|
+
try log.disabled(ident, last);
|
|
1628
|
+
return true;
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
return false;
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
inline fn buildRage(battle: anytype, who: Player, log: anytype) !void {
|
|
1635
|
+
const side = battle.side(who);
|
|
1636
|
+
if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
|
|
1637
|
+
try Effects.boost(battle, who, Move.get(.Rage), log);
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
|
|
1642
|
+
var volatiles = &active.volatiles;
|
|
1643
|
+
assert(volatiles.Thrashing);
|
|
1644
|
+
if (volatiles.attacks > 0) return false;
|
|
1645
|
+
|
|
1646
|
+
volatiles.Thrashing = false;
|
|
1647
|
+
volatiles.Confusion = true;
|
|
1648
|
+
volatiles.confusion = Rolls.confusionDuration(battle);
|
|
1649
|
+
return true;
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1575
1652
|
inline fn onBegin(
|
|
1576
1653
|
battle: anytype,
|
|
1577
1654
|
player: Player,
|
|
@@ -1662,10 +1739,7 @@ pub const Effects = struct {
|
|
|
1662
1739
|
side.active.volatiles.Bide = true;
|
|
1663
1740
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1664
1741
|
side.active.volatiles.state = 0;
|
|
1665
|
-
side.active.volatiles.attacks =
|
|
1666
|
-
battle.rng.range(u4, 2, 4)
|
|
1667
|
-
else
|
|
1668
|
-
(battle.rng.next() & 1) + 2);
|
|
1742
|
+
side.active.volatiles.attacks = Rolls.bideThrashDuration(battle);
|
|
1669
1743
|
|
|
1670
1744
|
try log.start(battle.active(player), .Bide);
|
|
1671
1745
|
}
|
|
@@ -1687,14 +1761,8 @@ pub const Effects = struct {
|
|
|
1687
1761
|
return;
|
|
1688
1762
|
}
|
|
1689
1763
|
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
else
|
|
1693
|
-
battle.rng.next() < 1 + (if (move.effect == .BurnChance1)
|
|
1694
|
-
Gen12.percent(10)
|
|
1695
|
-
else
|
|
1696
|
-
Gen12.percent(30));
|
|
1697
|
-
if (!chance) return;
|
|
1764
|
+
if (foe.active.types.includes(move.type)) return;
|
|
1765
|
+
if (!Rolls.secondaryChance(battle, move.effect == .BurnChance1)) return;
|
|
1698
1766
|
|
|
1699
1767
|
foe_stored.status = Status.init(.BRN);
|
|
1700
1768
|
foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
|
|
@@ -1718,11 +1786,7 @@ pub const Effects = struct {
|
|
|
1718
1786
|
const sub = foe.active.volatiles.Substitute;
|
|
1719
1787
|
|
|
1720
1788
|
if (move.effect == .ConfusionChance) {
|
|
1721
|
-
|
|
1722
|
-
battle.rng.chance(u8, 25, 256)
|
|
1723
|
-
else
|
|
1724
|
-
battle.rng.next() < Gen12.percent(10);
|
|
1725
|
-
if (!chance) return;
|
|
1789
|
+
if (!Rolls.confusionChance(battle)) return;
|
|
1726
1790
|
} else {
|
|
1727
1791
|
if (showdown) {
|
|
1728
1792
|
if (!try checkHit(battle, player, move, log)) {
|
|
@@ -1741,10 +1805,7 @@ pub const Effects = struct {
|
|
|
1741
1805
|
|
|
1742
1806
|
if (foe.active.volatiles.Confusion) return;
|
|
1743
1807
|
foe.active.volatiles.Confusion = true;
|
|
1744
|
-
foe.active.volatiles.confusion =
|
|
1745
|
-
battle.rng.range(u8, 2, 6)
|
|
1746
|
-
else
|
|
1747
|
-
(battle.rng.next() & 3) + 2);
|
|
1808
|
+
foe.active.volatiles.confusion = Rolls.confusionDuration(battle);
|
|
1748
1809
|
|
|
1749
1810
|
try log.start(battle.active(player.foe()), .Confusion);
|
|
1750
1811
|
}
|
|
@@ -1794,11 +1855,8 @@ pub const Effects = struct {
|
|
|
1794
1855
|
return Result.Error;
|
|
1795
1856
|
}
|
|
1796
1857
|
|
|
1797
|
-
volatiles.disabled_move = @intCast(u3,
|
|
1798
|
-
volatiles.disabled_duration =
|
|
1799
|
-
battle.rng.range(u8, 1, 9)
|
|
1800
|
-
else
|
|
1801
|
-
(battle.rng.next() & 7) + 1);
|
|
1858
|
+
volatiles.disabled_move = @intCast(u3, Rolls.moveSlot(battle, &foe.active.moves, n));
|
|
1859
|
+
volatiles.disabled_duration = Rolls.disableDuration(battle);
|
|
1802
1860
|
|
|
1803
1861
|
try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
|
|
1804
1862
|
return null;
|
|
@@ -1810,7 +1868,7 @@ pub const Effects = struct {
|
|
|
1810
1868
|
|
|
1811
1869
|
if (battle.last_damage == 0) {
|
|
1812
1870
|
assert(showdown);
|
|
1813
|
-
return;
|
|
1871
|
+
if (!battle.foe(player).active.volatiles.Substitute) return;
|
|
1814
1872
|
}
|
|
1815
1873
|
|
|
1816
1874
|
const drain = @max(battle.last_damage / 2, 1);
|
|
@@ -1837,15 +1895,7 @@ pub const Effects = struct {
|
|
|
1837
1895
|
var volatiles = &battle.foe(player).active.volatiles;
|
|
1838
1896
|
|
|
1839
1897
|
if (volatiles.Substitute) return;
|
|
1840
|
-
|
|
1841
|
-
const chance = if (showdown)
|
|
1842
|
-
battle.rng.chance(u8, @as(u8, if (move.effect == .FlinchChance1) 26 else 77), 256)
|
|
1843
|
-
else
|
|
1844
|
-
battle.rng.next() < 1 + (if (move.effect == .FlinchChance1)
|
|
1845
|
-
Gen12.percent(10)
|
|
1846
|
-
else
|
|
1847
|
-
Gen12.percent(30));
|
|
1848
|
-
if (!chance) return;
|
|
1898
|
+
if (!Rolls.secondaryChance(battle, move.effect == .FlinchChance1)) return;
|
|
1849
1899
|
|
|
1850
1900
|
volatiles.Flinch = true;
|
|
1851
1901
|
volatiles.Recharging = false;
|
|
@@ -1871,12 +1921,8 @@ pub const Effects = struct {
|
|
|
1871
1921
|
return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
|
|
1872
1922
|
}
|
|
1873
1923
|
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
else
|
|
1877
|
-
battle.rng.next() < 1 + Gen12.percent(10);
|
|
1878
|
-
if (!chance) return;
|
|
1879
|
-
|
|
1924
|
+
if (foe.active.types.includes(move.type)) return;
|
|
1925
|
+
if (!Rolls.secondaryChance(battle, true)) return;
|
|
1880
1926
|
// Freeze Clause Mod
|
|
1881
1927
|
if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
1882
1928
|
|
|
@@ -2013,15 +2059,15 @@ pub const Effects = struct {
|
|
|
2013
2059
|
}
|
|
2014
2060
|
break :has_mimic false;
|
|
2015
2061
|
};
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2062
|
+
if (!has_mimic) {
|
|
2063
|
+
// Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
|
|
2064
|
+
if (!try checkHit(battle, player, move, log)) return;
|
|
2019
2065
|
return try log.fail(battle.active(player.foe()), .None);
|
|
2020
2066
|
}
|
|
2021
2067
|
}
|
|
2022
2068
|
if (!try checkHit(battle, player, move, log)) return;
|
|
2023
2069
|
|
|
2024
|
-
const rslot =
|
|
2070
|
+
const rslot = Rolls.moveSlot(battle, &foe.active.moves, 0);
|
|
2025
2071
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
2026
2072
|
|
|
2027
2073
|
try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
|
|
@@ -2042,7 +2088,8 @@ pub const Effects = struct {
|
|
|
2042
2088
|
assert(!side.active.volatiles.MultiHit);
|
|
2043
2089
|
side.active.volatiles.MultiHit = true;
|
|
2044
2090
|
|
|
2045
|
-
side.active.volatiles.attacks =
|
|
2091
|
+
side.active.volatiles.attacks =
|
|
2092
|
+
if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
|
|
2046
2093
|
}
|
|
2047
2094
|
|
|
2048
2095
|
fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2086,14 +2133,8 @@ pub const Effects = struct {
|
|
|
2086
2133
|
}
|
|
2087
2134
|
|
|
2088
2135
|
// Body Slam can't paralyze a Normal type Pokémon
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
else
|
|
2092
|
-
battle.rng.next() < 1 + (if (move.effect == .ParalyzeChance1)
|
|
2093
|
-
Gen12.percent(10)
|
|
2094
|
-
else
|
|
2095
|
-
Gen12.percent(30));
|
|
2096
|
-
if (!chance) return;
|
|
2136
|
+
if (foe.active.types.includes(move.type)) return;
|
|
2137
|
+
if (!Rolls.secondaryChance(battle, move.effect == .ParalyzeChance1)) return;
|
|
2097
2138
|
|
|
2098
2139
|
foe_stored.status = Status.init(.PAR);
|
|
2099
2140
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
@@ -2136,16 +2177,7 @@ pub const Effects = struct {
|
|
|
2136
2177
|
|
|
2137
2178
|
if (move.effect == .Poison) {
|
|
2138
2179
|
if (!showdown and !try checkHit(battle, player, move, log)) return;
|
|
2139
|
-
} else
|
|
2140
|
-
const chance = if (showdown)
|
|
2141
|
-
battle.rng.chance(u8, @as(u8, if (move.effect == .PoisonChance1) 52 else 103), 256)
|
|
2142
|
-
else
|
|
2143
|
-
battle.rng.next() < 1 + (if (move.effect == .PoisonChance1)
|
|
2144
|
-
Gen12.percent(20)
|
|
2145
|
-
else
|
|
2146
|
-
Gen12.percent(40));
|
|
2147
|
-
if (!chance) return;
|
|
2148
|
-
}
|
|
2180
|
+
} else if (!Rolls.poisonChance(battle, move.effect == .PoisonChance1)) return;
|
|
2149
2181
|
|
|
2150
2182
|
foe_stored.status = Status.init(.PSN);
|
|
2151
2183
|
if (toxic) {
|
|
@@ -2159,10 +2191,8 @@ pub const Effects = struct {
|
|
|
2159
2191
|
|
|
2160
2192
|
fn rage(battle: anytype, player: Player) !void {
|
|
2161
2193
|
var volatiles = &battle.side(player).active.volatiles;
|
|
2162
|
-
if (volatiles.Rage) return;
|
|
2163
|
-
volatiles.Rage = true;
|
|
2164
2194
|
assert(!volatiles.Bide);
|
|
2165
|
-
volatiles.
|
|
2195
|
+
volatiles.Rage = true;
|
|
2166
2196
|
}
|
|
2167
2197
|
|
|
2168
2198
|
fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
|
|
@@ -2214,16 +2244,7 @@ pub const Effects = struct {
|
|
|
2214
2244
|
}
|
|
2215
2245
|
foe.active.volatiles.Recharging = false;
|
|
2216
2246
|
|
|
2217
|
-
|
|
2218
|
-
battle.rng.range(u8, 1, 8)
|
|
2219
|
-
else loop: {
|
|
2220
|
-
while (true) {
|
|
2221
|
-
const r = battle.rng.next() & 7;
|
|
2222
|
-
if (r != 0) break :loop r;
|
|
2223
|
-
}
|
|
2224
|
-
});
|
|
2225
|
-
|
|
2226
|
-
foe_stored.status = Status.slp(duration);
|
|
2247
|
+
foe_stored.status = Status.slp(Rolls.sleepDuration(battle));
|
|
2227
2248
|
try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
|
|
2228
2249
|
}
|
|
2229
2250
|
|
|
@@ -2276,11 +2297,7 @@ pub const Effects = struct {
|
|
|
2276
2297
|
assert(!volatiles.Bide);
|
|
2277
2298
|
|
|
2278
2299
|
volatiles.Thrashing = true;
|
|
2279
|
-
volatiles.
|
|
2280
|
-
volatiles.attacks = @intCast(u3, if (showdown)
|
|
2281
|
-
battle.rng.range(u8, 2, 4)
|
|
2282
|
-
else
|
|
2283
|
-
(battle.rng.next() & 1) + 2);
|
|
2300
|
+
volatiles.attacks = Rolls.bideThrashDuration(battle);
|
|
2284
2301
|
}
|
|
2285
2302
|
|
|
2286
2303
|
fn transform(battle: anytype, player: Player, log: anytype) !void {
|
|
@@ -2312,7 +2329,7 @@ pub const Effects = struct {
|
|
|
2312
2329
|
try log.transform(battle.active(player), foe_ident);
|
|
2313
2330
|
}
|
|
2314
2331
|
|
|
2315
|
-
fn binding(battle: anytype, player: Player) void {
|
|
2332
|
+
fn binding(battle: anytype, player: Player, rewrap: bool) void {
|
|
2316
2333
|
var side = battle.side(player);
|
|
2317
2334
|
var foe = battle.foe(player);
|
|
2318
2335
|
|
|
@@ -2320,9 +2337,11 @@ pub const Effects = struct {
|
|
|
2320
2337
|
side.active.volatiles.Binding = true;
|
|
2321
2338
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2322
2339
|
// (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
|
|
2323
|
-
if (!showdown)
|
|
2340
|
+
if (!showdown) {
|
|
2341
|
+
foe.active.volatiles.Recharging = false;
|
|
2342
|
+
} else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
|
|
2324
2343
|
|
|
2325
|
-
side.active.volatiles.attacks = distribution(battle) - 1;
|
|
2344
|
+
side.active.volatiles.attacks = Rolls.distribution(battle) - 1;
|
|
2326
2345
|
}
|
|
2327
2346
|
|
|
2328
2347
|
fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2349,7 +2368,7 @@ pub const Effects = struct {
|
|
|
2349
2368
|
return try log.fail(ident, .None);
|
|
2350
2369
|
}
|
|
2351
2370
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2352
|
-
const stat = unmodifiedStats(battle,
|
|
2371
|
+
const stat = unmodifiedStats(battle, side).atk;
|
|
2353
2372
|
stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2354
2373
|
try log.boost(ident, reason, n);
|
|
2355
2374
|
// Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
|
|
@@ -2369,7 +2388,7 @@ pub const Effects = struct {
|
|
|
2369
2388
|
return try log.fail(ident, .None);
|
|
2370
2389
|
}
|
|
2371
2390
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2372
|
-
const stat = unmodifiedStats(battle,
|
|
2391
|
+
const stat = unmodifiedStats(battle, side).def;
|
|
2373
2392
|
stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2374
2393
|
try log.boost(ident, .Defense, n);
|
|
2375
2394
|
},
|
|
@@ -2386,7 +2405,7 @@ pub const Effects = struct {
|
|
|
2386
2405
|
return try log.fail(ident, .None);
|
|
2387
2406
|
}
|
|
2388
2407
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2389
|
-
const stat = unmodifiedStats(battle,
|
|
2408
|
+
const stat = unmodifiedStats(battle, side).spe;
|
|
2390
2409
|
stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2391
2410
|
try log.boost(ident, .Speed, 2);
|
|
2392
2411
|
},
|
|
@@ -2406,7 +2425,7 @@ pub const Effects = struct {
|
|
|
2406
2425
|
return try log.fail(ident, .None);
|
|
2407
2426
|
}
|
|
2408
2427
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2409
|
-
const stat = unmodifiedStats(battle,
|
|
2428
|
+
const stat = unmodifiedStats(battle, side).spc;
|
|
2410
2429
|
stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2411
2430
|
try log.boost(ident, .SpecialAttack, n);
|
|
2412
2431
|
try log.boost(ident, .SpecialDefense, n);
|
|
@@ -2433,11 +2452,7 @@ pub const Effects = struct {
|
|
|
2433
2452
|
}
|
|
2434
2453
|
|
|
2435
2454
|
if (move.effect.isStatDownChance()) {
|
|
2436
|
-
|
|
2437
|
-
battle.rng.chance(u8, 85, 256)
|
|
2438
|
-
else
|
|
2439
|
-
battle.rng.next() < Gen12.percent(33) + 1;
|
|
2440
|
-
if (!chance or foe.active.volatiles.Invulnerable) return;
|
|
2455
|
+
if (!Rolls.unboost(battle) or foe.active.volatiles.Invulnerable) return;
|
|
2441
2456
|
} else if (!showdown and !try checkHit(battle, player, move, log)) {
|
|
2442
2457
|
return; // checkHit already checks for Invulnerable
|
|
2443
2458
|
}
|
|
@@ -2459,7 +2474,7 @@ pub const Effects = struct {
|
|
|
2459
2474
|
return try log.fail(foe_ident, .None);
|
|
2460
2475
|
}
|
|
2461
2476
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2462
|
-
const stat = unmodifiedStats(battle,
|
|
2477
|
+
const stat = unmodifiedStats(battle, foe).atk;
|
|
2463
2478
|
stats.atk = @max(1, stat * mod[0] / mod[1]);
|
|
2464
2479
|
try log.boost(foe_ident, .Attack, -1);
|
|
2465
2480
|
},
|
|
@@ -2477,7 +2492,7 @@ pub const Effects = struct {
|
|
|
2477
2492
|
return try log.fail(foe_ident, .None);
|
|
2478
2493
|
}
|
|
2479
2494
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2480
|
-
const stat = unmodifiedStats(battle,
|
|
2495
|
+
const stat = unmodifiedStats(battle, foe).def;
|
|
2481
2496
|
stats.def = @max(1, stat * mod[0] / mod[1]);
|
|
2482
2497
|
try log.boost(foe_ident, .Defense, -@as(i8, n));
|
|
2483
2498
|
},
|
|
@@ -2494,7 +2509,7 @@ pub const Effects = struct {
|
|
|
2494
2509
|
return try log.fail(foe_ident, .None);
|
|
2495
2510
|
}
|
|
2496
2511
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2497
|
-
const stat = unmodifiedStats(battle,
|
|
2512
|
+
const stat = unmodifiedStats(battle, foe).spe;
|
|
2498
2513
|
stats.spe = @max(1, stat * mod[0] / mod[1]);
|
|
2499
2514
|
try log.boost(foe_ident, .Speed, -1);
|
|
2500
2515
|
assert(boosts.spe >= -6);
|
|
@@ -2514,7 +2529,7 @@ pub const Effects = struct {
|
|
|
2514
2529
|
return try log.fail(foe_ident, .None);
|
|
2515
2530
|
}
|
|
2516
2531
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2517
|
-
const stat = unmodifiedStats(battle,
|
|
2532
|
+
const stat = unmodifiedStats(battle, foe).spc;
|
|
2518
2533
|
stats.spc = @max(1, stat * mod[0] / mod[1]);
|
|
2519
2534
|
try log.boost(foe_ident, .SpecialAttack, -1);
|
|
2520
2535
|
try log.boost(foe_ident, .SpecialDefense, -1);
|
|
@@ -2533,8 +2548,7 @@ pub const Effects = struct {
|
|
|
2533
2548
|
}
|
|
2534
2549
|
};
|
|
2535
2550
|
|
|
2536
|
-
fn unmodifiedStats(battle: anytype,
|
|
2537
|
-
const side = battle.side(who);
|
|
2551
|
+
fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
|
|
2538
2552
|
if (!side.active.volatiles.Transform) return &side.stored().stats;
|
|
2539
2553
|
const id = ID.from(side.active.volatiles.transform);
|
|
2540
2554
|
return &battle.side(id.player).pokemon[id.id - 1].stats;
|
|
@@ -2600,80 +2614,140 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
|
|
|
2600
2614
|
}
|
|
2601
2615
|
}
|
|
2602
2616
|
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2617
|
+
pub const Rolls = struct {
|
|
2618
|
+
inline fn speedTie(battle: anytype) bool {
|
|
2619
|
+
if (showdown) return battle.rng.range(u8, 0, 2) == 0;
|
|
2620
|
+
return battle.rng.next() < Gen12.percent(50) + 1;
|
|
2621
|
+
}
|
|
2622
|
+
|
|
2623
|
+
inline fn criticalHit(battle: anytype, chance: u16) bool {
|
|
2624
|
+
if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
|
|
2625
|
+
return std.math.rotl(u8, battle.rng.next(), 3) < chance;
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2628
|
+
inline fn damage(battle: anytype) u8 {
|
|
2629
|
+
if (showdown) return battle.rng.range(u8, 217, 256);
|
|
2630
|
+
while (true) {
|
|
2631
|
+
const r = std.math.rotr(u8, battle.rng.next(), 1);
|
|
2632
|
+
if (r >= 217) return r;
|
|
2616
2633
|
}
|
|
2617
2634
|
}
|
|
2618
|
-
return false;
|
|
2619
|
-
}
|
|
2620
2635
|
|
|
2621
|
-
inline fn
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
try Effects.boost(battle, who, Move.get(.Rage), log);
|
|
2636
|
+
inline fn hit(battle: anytype, accuracy: u16) bool {
|
|
2637
|
+
if (showdown) return battle.rng.chance(u8, @intCast(u8, accuracy), 256);
|
|
2638
|
+
return battle.rng.next() < accuracy;
|
|
2625
2639
|
}
|
|
2626
|
-
}
|
|
2627
2640
|
|
|
2628
|
-
fn
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
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
|
+
}
|
|
2632
2645
|
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
else
|
|
2638
|
-
(battle.rng.next() & 3) + 2);
|
|
2646
|
+
inline fn confused(battle: anytype) bool {
|
|
2647
|
+
if (showdown) return !battle.rng.chance(u8, 128, 256);
|
|
2648
|
+
return battle.rng.next() >= Gen12.percent(50) + 1;
|
|
2649
|
+
}
|
|
2639
2650
|
|
|
2640
|
-
|
|
2641
|
-
|
|
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
|
+
}
|
|
2642
2655
|
|
|
2643
|
-
|
|
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
|
+
}
|
|
2644
2660
|
|
|
2645
|
-
fn
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
}
|
|
2661
|
+
inline fn secondaryChance(battle: anytype, low: bool) bool {
|
|
2662
|
+
if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 26 else 77), 256);
|
|
2663
|
+
return battle.rng.next() < 1 + (if (low) Gen12.percent(10) else Gen12.percent(30));
|
|
2664
|
+
}
|
|
2650
2665
|
|
|
2651
|
-
fn
|
|
2652
|
-
|
|
2653
|
-
if (
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
+
inline fn poisonChance(battle: anytype, low: bool) bool {
|
|
2667
|
+
if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 52 else 103), 256);
|
|
2668
|
+
return battle.rng.next() < 1 + (if (low) Gen12.percent(20) else Gen12.percent(40));
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2671
|
+
inline fn sleepDuration(battle: anytype) u3 {
|
|
2672
|
+
if (showdown) return @intCast(u3, battle.rng.range(u8, 1, 8));
|
|
2673
|
+
while (true) {
|
|
2674
|
+
const r = battle.rng.next() & 7;
|
|
2675
|
+
if (r != 0) return @intCast(u3, r);
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
inline fn bideThrashDuration(battle: anytype) u3 {
|
|
2680
|
+
if (showdown) return @intCast(u3, battle.rng.range(u4, 2, 4));
|
|
2681
|
+
return @intCast(u3, (battle.rng.next() & 1) + 2);
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
fn moveSlot(battle: anytype, moves: []MoveSlot, check_pp: u4) u4 {
|
|
2685
|
+
if (showdown) {
|
|
2686
|
+
if (check_pp == 0) {
|
|
2687
|
+
var i: usize = moves.len;
|
|
2688
|
+
while (i > 0) {
|
|
2689
|
+
i -= 1;
|
|
2690
|
+
if (moves[i].id != .None) {
|
|
2691
|
+
return battle.rng.range(u4, 0, @intCast(u4, i + 1)) + 1;
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
} else {
|
|
2695
|
+
var r = battle.rng.range(u4, 0, @intCast(u4, check_pp)) + 1;
|
|
2696
|
+
var i: usize = 0;
|
|
2697
|
+
while (i < moves.len and r > 0) : (i += 1) {
|
|
2698
|
+
if (moves[i].pp > 0) {
|
|
2699
|
+
r -= 1;
|
|
2700
|
+
if (r == 0) break;
|
|
2701
|
+
}
|
|
2666
2702
|
}
|
|
2703
|
+
return @intCast(u4, i + 1);
|
|
2667
2704
|
}
|
|
2668
|
-
|
|
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;
|
|
2669
2710
|
}
|
|
2670
2711
|
}
|
|
2671
2712
|
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
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);
|
|
2675
2716
|
}
|
|
2676
|
-
|
|
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
|
+
};
|
|
2677
2751
|
|
|
2678
2752
|
test "RNG agreement" {
|
|
2679
2753
|
if (!showdown) return;
|