@pkmn/engine 0.1.0-dev.02b7f810 → 0.1.0-dev.14e79917
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 +2 -2
- package/build/pkg/addon.js +5 -5
- package/build/pkg/addon.js.map +1 -1
- 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 +22 -27
- package/package.json +16 -15
- package/src/bin/install-pkmn-engine +3 -3
- 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 +90 -69
- package/src/lib/gen1/data.zig +19 -17
- package/src/lib/gen1/helpers.zig +4 -15
- package/src/lib/gen1/mechanics.zig +374 -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 {
|
|
@@ -219,9 +237,9 @@ fn saveMove(battle: anytype, player: Player, choice: ?Choice) void {
|
|
|
219
237
|
|
|
220
238
|
side.last_selected_move = move.id;
|
|
221
239
|
if (player == .P1) {
|
|
222
|
-
battle.
|
|
240
|
+
battle.last_moves.p1_index = @intCast(u3, c.data);
|
|
223
241
|
} else {
|
|
224
|
-
battle.
|
|
242
|
+
battle.last_moves.p2_index = @intCast(u3, c.data);
|
|
225
243
|
}
|
|
226
244
|
}
|
|
227
245
|
}
|
|
@@ -241,9 +259,9 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, log: anyty
|
|
|
241
259
|
side.order[slot - 1] = out;
|
|
242
260
|
|
|
243
261
|
if (player == .P1) {
|
|
244
|
-
battle.
|
|
262
|
+
battle.last_moves.p1_index = 1;
|
|
245
263
|
} else {
|
|
246
|
-
battle.
|
|
264
|
+
battle.last_moves.p2_index = 1;
|
|
247
265
|
}
|
|
248
266
|
|
|
249
267
|
side.last_used_move = .None;
|
|
@@ -300,13 +318,11 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
|
|
|
300
318
|
const spe2 = battle.side(.P2).active.stats.spe;
|
|
301
319
|
if (spe1 == spe2) {
|
|
302
320
|
// Pokémon Showdown's beforeTurnCallback shenanigans
|
|
303
|
-
if (showdown and m1 == .Counter and m2 == .Counter)
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
battle.rng.range(u8, 0, 2) == 0
|
|
307
|
-
else
|
|
308
|
-
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
|
+
}
|
|
309
324
|
|
|
325
|
+
const p1 = Rolls.speedTie(battle);
|
|
310
326
|
if (!showdown) return if (p1) .P1 else .P2;
|
|
311
327
|
|
|
312
328
|
// Pokémon Showdown's "lockedmove" volatile's onBeforeTurn uses BattleQueue#changeAction,
|
|
@@ -316,7 +332,7 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
|
|
|
316
332
|
const t1 = battle.side(.P1).active.volatiles.Thrashing;
|
|
317
333
|
const t2 = battle.side(.P2).active.volatiles.Thrashing;
|
|
318
334
|
// If *both* sides are thrashing it really should be another speed tie, but we've patched
|
|
319
|
-
// 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
|
|
320
336
|
// won the original coin flip
|
|
321
337
|
if (t1 and t2) return .P1;
|
|
322
338
|
return if (p1) if (t1 and !t2) .P2 else .P1 else if (t2 and !t1) .P1 else .P2;
|
|
@@ -329,9 +345,13 @@ fn doTurn(
|
|
|
329
345
|
battle: anytype,
|
|
330
346
|
player: Player,
|
|
331
347
|
player_choice: Choice,
|
|
348
|
+
player_rewrap: bool,
|
|
349
|
+
player_skip: bool,
|
|
332
350
|
player_from: ?Move,
|
|
333
351
|
foe_player: Player,
|
|
334
352
|
foe_choice: Choice,
|
|
353
|
+
foe_rewrap: bool,
|
|
354
|
+
foe_skip: bool,
|
|
335
355
|
foe_from: ?Move,
|
|
336
356
|
log: anytype,
|
|
337
357
|
) !?Result {
|
|
@@ -339,7 +359,16 @@ fn doTurn(
|
|
|
339
359
|
|
|
340
360
|
var residual = true;
|
|
341
361
|
var replace = battle.side(player).stored().hp == 0;
|
|
342
|
-
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;
|
|
343
372
|
if (!replace) {
|
|
344
373
|
if (player_choice.type != .Switch) {
|
|
345
374
|
if (try checkFaint(battle, foe_player, log)) |r| return r;
|
|
@@ -350,7 +379,16 @@ fn doTurn(
|
|
|
350
379
|
|
|
351
380
|
residual = true;
|
|
352
381
|
replace = battle.side(foe_player).stored().hp == 0;
|
|
353
|
-
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;
|
|
354
392
|
if (!replace) {
|
|
355
393
|
if (foe_choice.type != .Switch) {
|
|
356
394
|
if (try checkFaint(battle, player, log)) |r| return r;
|
|
@@ -373,6 +411,8 @@ fn executeMove(
|
|
|
373
411
|
battle: anytype,
|
|
374
412
|
player: Player,
|
|
375
413
|
choice: Choice,
|
|
414
|
+
rewrap: bool,
|
|
415
|
+
skip: bool,
|
|
376
416
|
from: ?Move,
|
|
377
417
|
residual: *bool,
|
|
378
418
|
log: anytype,
|
|
@@ -384,9 +424,10 @@ fn executeMove(
|
|
|
384
424
|
return null;
|
|
385
425
|
}
|
|
386
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
|
|
387
430
|
if (side.last_selected_move == .SKIP_TURN) {
|
|
388
|
-
// Pokémon Showdown overwrites the SKIP_TURN sentinel with its botched move select,
|
|
389
|
-
// Binding instead gets handled in beforeMove after sleep and freeze
|
|
390
431
|
assert(!showdown);
|
|
391
432
|
if (battle.foe(player).active.volatiles.Binding) {
|
|
392
433
|
try log.cant(battle.active(player), .Bound);
|
|
@@ -405,9 +446,9 @@ fn executeMove(
|
|
|
405
446
|
// choice.data == 0 only happens with Struggle on Pokémon Showdown
|
|
406
447
|
assert(!showdown);
|
|
407
448
|
mslot = @intCast(u4, if (player == .P1)
|
|
408
|
-
battle.
|
|
449
|
+
battle.last_moves.p1_index
|
|
409
450
|
else
|
|
410
|
-
battle.
|
|
451
|
+
battle.last_moves.p2_index);
|
|
411
452
|
const stored = side.stored();
|
|
412
453
|
// GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
|
|
413
454
|
auto = side.last_selected_move == .HyperBeam or side.active.volatiles.Bide or
|
|
@@ -429,14 +470,14 @@ fn executeMove(
|
|
|
429
470
|
// Incorrect mslot with Pokémon Showdown choice semantics so we need to recover from index
|
|
430
471
|
assert(mslot == 1);
|
|
431
472
|
mslot = @intCast(u4, if (player == .P1)
|
|
432
|
-
battle.
|
|
473
|
+
battle.last_moves.p1_index
|
|
433
474
|
else
|
|
434
|
-
battle.
|
|
475
|
+
battle.last_moves.p2_index);
|
|
435
476
|
}
|
|
436
477
|
|
|
437
478
|
var skip_can = false;
|
|
438
479
|
var skip_pp = false;
|
|
439
|
-
switch (try beforeMove(battle, player, from, residual, log)) {
|
|
480
|
+
switch (try beforeMove(battle, player, skip, from, residual, log)) {
|
|
440
481
|
.done => return null,
|
|
441
482
|
.skip_can => skip_can = true,
|
|
442
483
|
.skip_pp => skip_pp = true,
|
|
@@ -448,14 +489,15 @@ fn executeMove(
|
|
|
448
489
|
return null;
|
|
449
490
|
}
|
|
450
491
|
|
|
451
|
-
return doMove(battle, player, mslot, auto, residual, log);
|
|
492
|
+
return doMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
452
493
|
}
|
|
453
494
|
|
|
454
|
-
const BeforeMove =
|
|
495
|
+
const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
|
|
455
496
|
|
|
456
497
|
fn beforeMove(
|
|
457
498
|
battle: anytype,
|
|
458
499
|
player: Player,
|
|
500
|
+
skip: bool,
|
|
459
501
|
from: ?Move,
|
|
460
502
|
residual: *bool,
|
|
461
503
|
log: anytype,
|
|
@@ -487,13 +529,15 @@ fn beforeMove(
|
|
|
487
529
|
return .done;
|
|
488
530
|
}
|
|
489
531
|
|
|
490
|
-
if (foe.active.volatiles.Binding) {
|
|
532
|
+
if (skip or foe.active.volatiles.Binding) {
|
|
491
533
|
try log.cant(ident, .Bound);
|
|
492
534
|
return .done;
|
|
493
535
|
}
|
|
494
536
|
|
|
495
|
-
if (
|
|
496
|
-
|
|
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;
|
|
497
541
|
try log.cant(ident, .Flinch);
|
|
498
542
|
return .done;
|
|
499
543
|
}
|
|
@@ -514,13 +558,6 @@ fn beforeMove(
|
|
|
514
558
|
// Pokémon Showdown's disable condition has a single onBeforeMove handler
|
|
515
559
|
if (showdown and try disabled(side, ident, log)) return .done;
|
|
516
560
|
|
|
517
|
-
// Pokémon Showdown checks for Flinch *after* instead of before
|
|
518
|
-
if (showdown and volatiles.Flinch) {
|
|
519
|
-
volatiles.Flinch = false;
|
|
520
|
-
try log.cant(ident, .Flinch);
|
|
521
|
-
return .done;
|
|
522
|
-
}
|
|
523
|
-
|
|
524
561
|
// This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
|
|
525
562
|
// before the side had a selected a move - we simply need to assume this leads to a desync
|
|
526
563
|
if (side.last_selected_move == .None) {
|
|
@@ -538,12 +575,7 @@ fn beforeMove(
|
|
|
538
575
|
} else {
|
|
539
576
|
try log.activate(ident, .Confusion);
|
|
540
577
|
|
|
541
|
-
|
|
542
|
-
!battle.rng.chance(u8, 128, 256)
|
|
543
|
-
else
|
|
544
|
-
battle.rng.next() >= Gen12.percent(50) + 1;
|
|
545
|
-
|
|
546
|
-
if (confused) {
|
|
578
|
+
if (Rolls.confused(battle)) {
|
|
547
579
|
assert(!volatiles.MultiHit);
|
|
548
580
|
if (!volatiles.Rage) volatiles.state = 0;
|
|
549
581
|
volatiles.Bide = false;
|
|
@@ -573,7 +605,7 @@ fn beforeMove(
|
|
|
573
605
|
const uncapped = battle.last_damage;
|
|
574
606
|
// Skipping adjustDamage / randomizeDamage / checkHit
|
|
575
607
|
_ = try applyDamage(battle, player, target, .Confusion, log);
|
|
576
|
-
// Pokémon Showdown thinks that
|
|
608
|
+
// Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
|
|
577
609
|
if (showdown) battle.last_damage = uncapped;
|
|
578
610
|
|
|
579
611
|
return .done;
|
|
@@ -583,28 +615,28 @@ fn beforeMove(
|
|
|
583
615
|
|
|
584
616
|
if (!showdown and try disabled(side, ident, log)) return .done;
|
|
585
617
|
|
|
586
|
-
if (Status.is(stored.status, .PAR)) {
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
volatiles.Thrashing = false;
|
|
596
|
-
volatiles.Charging = false;
|
|
597
|
-
volatiles.Binding = false;
|
|
598
|
-
// GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
|
|
599
|
-
try log.cant(ident, .Paralysis);
|
|
600
|
-
return .done;
|
|
601
|
-
}
|
|
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;
|
|
602
627
|
}
|
|
603
628
|
|
|
604
629
|
if (volatiles.Bide) {
|
|
605
630
|
assert(!volatiles.Thrashing and !volatiles.Rage);
|
|
606
631
|
|
|
607
|
-
|
|
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
|
+
}
|
|
608
640
|
|
|
609
641
|
assert(volatiles.attacks > 0);
|
|
610
642
|
|
|
@@ -648,8 +680,13 @@ fn beforeMove(
|
|
|
648
680
|
try log.start(battle.active(player), .ConfusionSilent);
|
|
649
681
|
}
|
|
650
682
|
try log.move(ident, side.last_selected_move, battle.active(player.foe()), from);
|
|
651
|
-
if (showdown
|
|
652
|
-
|
|
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
|
+
}
|
|
653
690
|
}
|
|
654
691
|
return .skip_can;
|
|
655
692
|
}
|
|
@@ -664,7 +701,11 @@ fn beforeMove(
|
|
|
664
701
|
side.last_selected_move,
|
|
665
702
|
);
|
|
666
703
|
if (showdown or battle.last_damage != 0) {
|
|
704
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
667
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
|
+
}
|
|
668
709
|
}
|
|
669
710
|
return .done;
|
|
670
711
|
}
|
|
@@ -696,6 +737,7 @@ fn canMove(
|
|
|
696
737
|
side.active.volatiles.Invulnerable = false;
|
|
697
738
|
} else if (move.effect == .Charge) {
|
|
698
739
|
try log.move(player_ident, side.last_selected_move, .{}, from);
|
|
740
|
+
setCounterable(battle, player, side, move);
|
|
699
741
|
try Effects.charge(battle, player, log);
|
|
700
742
|
return false;
|
|
701
743
|
}
|
|
@@ -712,6 +754,7 @@ fn canMove(
|
|
|
712
754
|
// regrettable, but this information is more "nice to have" than required
|
|
713
755
|
const f = if (from != null and from.? == .None) null else from;
|
|
714
756
|
try log.move(player_ident, side.last_selected_move, battle.active(target), f);
|
|
757
|
+
setCounterable(battle, player, side, move);
|
|
715
758
|
|
|
716
759
|
if (move.effect.onBegin()) {
|
|
717
760
|
try onBegin(battle, player, move, mslot, residual, log);
|
|
@@ -722,12 +765,27 @@ fn canMove(
|
|
|
722
765
|
Effects.thrashing(battle, player);
|
|
723
766
|
} else if (!showdown and move.effect == .Binding) {
|
|
724
767
|
// Pokémon Showdown handles this after hit/miss checks and damage calculation
|
|
725
|
-
Effects.binding(battle, player);
|
|
768
|
+
Effects.binding(battle, player, false);
|
|
726
769
|
}
|
|
727
770
|
|
|
728
771
|
return true;
|
|
729
772
|
}
|
|
730
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
|
+
|
|
731
789
|
fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
|
|
732
790
|
if (side.last_selected_move == .Struggle) return;
|
|
733
791
|
|
|
@@ -763,6 +821,7 @@ fn doMove(
|
|
|
763
821
|
battle: anytype,
|
|
764
822
|
player: Player,
|
|
765
823
|
mslot: u4,
|
|
824
|
+
rewrap: bool,
|
|
766
825
|
auto: bool,
|
|
767
826
|
residual: *bool,
|
|
768
827
|
log: anytype,
|
|
@@ -854,9 +913,9 @@ fn doMove(
|
|
|
854
913
|
|
|
855
914
|
if (!showdown or !miss) {
|
|
856
915
|
if (move.effect == .MirrorMove) {
|
|
857
|
-
return mirrorMove(battle, player, mslot, auto, residual, log);
|
|
916
|
+
return mirrorMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
858
917
|
} else if (move.effect == .Metronome) {
|
|
859
|
-
return metronome(battle, player, mslot, auto, residual, log);
|
|
918
|
+
return metronome(battle, player, mslot, rewrap, auto, residual, log);
|
|
860
919
|
} else if (move.effect.onEnd()) {
|
|
861
920
|
try onEnd(battle, player, move, log);
|
|
862
921
|
return null;
|
|
@@ -871,7 +930,7 @@ fn doMove(
|
|
|
871
930
|
move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
|
|
872
931
|
if (ohko) {
|
|
873
932
|
try log.immune(foe_ident, .OHKO);
|
|
874
|
-
} else if (immune and !invulnerable) {
|
|
933
|
+
} else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
|
|
875
934
|
try log.immune(foe_ident, .None);
|
|
876
935
|
} else if (mist) {
|
|
877
936
|
if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
|
|
@@ -937,11 +996,17 @@ fn doMove(
|
|
|
937
996
|
} else if (showdown) {
|
|
938
997
|
// This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
|
|
939
998
|
if (move.effect == .Binding) {
|
|
940
|
-
Effects.binding(battle, player);
|
|
999
|
+
Effects.binding(battle, player, rewrap);
|
|
941
1000
|
if (immune) {
|
|
942
1001
|
battle.last_damage = 0;
|
|
1002
|
+
assert(foe.stored().hp > 0);
|
|
943
1003
|
// Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
|
|
944
|
-
|
|
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
|
+
}
|
|
945
1010
|
return null;
|
|
946
1011
|
}
|
|
947
1012
|
}
|
|
@@ -992,8 +1057,7 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
|
|
|
992
1057
|
chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
|
|
993
1058
|
chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
|
|
994
1059
|
|
|
995
|
-
|
|
996
|
-
return std.math.rotl(u8, battle.rng.next(), 3) < chance;
|
|
1060
|
+
return Rolls.criticalHit(battle, chance);
|
|
997
1061
|
}
|
|
998
1062
|
|
|
999
1063
|
fn calcDamage(
|
|
@@ -1097,16 +1161,7 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
|
1097
1161
|
|
|
1098
1162
|
fn randomizeDamage(battle: anytype) void {
|
|
1099
1163
|
if (battle.last_damage <= 1) return;
|
|
1100
|
-
|
|
1101
|
-
const random = if (showdown)
|
|
1102
|
-
battle.rng.range(u8, 217, 256)
|
|
1103
|
-
else loop: {
|
|
1104
|
-
while (true) {
|
|
1105
|
-
const r = std.math.rotr(u8, battle.rng.next(), 1);
|
|
1106
|
-
if (r >= 217) break :loop r;
|
|
1107
|
-
}
|
|
1108
|
-
};
|
|
1109
|
-
|
|
1164
|
+
const random = Rolls.damage(battle);
|
|
1110
1165
|
battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
|
|
1111
1166
|
}
|
|
1112
1167
|
|
|
@@ -1124,16 +1179,9 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
|
|
|
1124
1179
|
// GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
|
|
1125
1180
|
.Psywave => power: {
|
|
1126
1181
|
const max = @intCast(u8, @as(u16, side.stored().level) * 3 / 2);
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
// GLITCH: Psywave infinite glitch loop
|
|
1131
|
-
if (max <= 1) return Result.Error;
|
|
1132
|
-
while (true) {
|
|
1133
|
-
const r = battle.rng.next();
|
|
1134
|
-
if (r < max) break :power r;
|
|
1135
|
-
}
|
|
1136
|
-
}
|
|
1182
|
+
// GLITCH: Psywave infinite glitch loop
|
|
1183
|
+
if (!showdown and max <= 1) return Result.Error;
|
|
1184
|
+
break :power Rolls.psywave(battle, max);
|
|
1137
1185
|
},
|
|
1138
1186
|
else => unreachable,
|
|
1139
1187
|
};
|
|
@@ -1155,19 +1203,17 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
|
|
|
1155
1203
|
return null;
|
|
1156
1204
|
}
|
|
1157
1205
|
|
|
1158
|
-
// Pretend
|
|
1159
|
-
const foe_last_used_move =
|
|
1160
|
-
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
|
|
1161
1207
|
const foe_last_selected_move =
|
|
1162
1208
|
Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
|
|
1163
|
-
.
|
|
1209
|
+
.Splash
|
|
1164
1210
|
else
|
|
1165
1211
|
foe.last_selected_move);
|
|
1166
1212
|
|
|
1167
|
-
const used =
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1213
|
+
const used = (if (player.foe() == .P1)
|
|
1214
|
+
battle.last_moves.p1_counterable
|
|
1215
|
+
else
|
|
1216
|
+
battle.last_moves.p2_counterable) != 0;
|
|
1171
1217
|
|
|
1172
1218
|
const selected = foe_last_selected_move.bp > 0 and
|
|
1173
1219
|
foe.last_selected_move != .Counter and
|
|
@@ -1186,12 +1232,14 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
|
|
|
1186
1232
|
return null;
|
|
1187
1233
|
}
|
|
1188
1234
|
|
|
1189
|
-
battle.last_damage
|
|
1235
|
+
battle.last_damage *|= 2;
|
|
1190
1236
|
|
|
1191
1237
|
// Pokémon Showdown calls checkHit before Counter
|
|
1192
1238
|
if (!showdown and !try checkHit(battle, player, move, log)) return null;
|
|
1193
1239
|
|
|
1240
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1194
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);
|
|
1195
1243
|
return null;
|
|
1196
1244
|
}
|
|
1197
1245
|
|
|
@@ -1235,6 +1283,7 @@ fn mirrorMove(
|
|
|
1235
1283
|
battle: anytype,
|
|
1236
1284
|
player: Player,
|
|
1237
1285
|
mslot: u4,
|
|
1286
|
+
rewrap: bool,
|
|
1238
1287
|
auto: bool,
|
|
1239
1288
|
residual: *bool,
|
|
1240
1289
|
log: anytype,
|
|
@@ -1247,41 +1296,30 @@ fn mirrorMove(
|
|
|
1247
1296
|
if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
|
|
1248
1297
|
try log.fail(battle.active(player), .None);
|
|
1249
1298
|
return null;
|
|
1299
|
+
} else if (!showdown or foe.last_used_move != .Struggle) {
|
|
1300
|
+
incrementPP(side, mslot);
|
|
1250
1301
|
}
|
|
1251
1302
|
|
|
1252
|
-
incrementPP(side, mslot);
|
|
1253
|
-
|
|
1254
1303
|
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
|
|
1255
|
-
return doMove(battle, player, mslot, auto, residual, log);
|
|
1304
|
+
return doMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
1256
1305
|
}
|
|
1257
1306
|
|
|
1258
1307
|
fn metronome(
|
|
1259
1308
|
battle: anytype,
|
|
1260
1309
|
player: Player,
|
|
1261
1310
|
mslot: u4,
|
|
1311
|
+
rewrap: bool,
|
|
1262
1312
|
auto: bool,
|
|
1263
1313
|
residual: *bool,
|
|
1264
1314
|
log: anytype,
|
|
1265
1315
|
) !?Result {
|
|
1266
1316
|
var side = battle.side(player);
|
|
1267
1317
|
|
|
1268
|
-
side.last_selected_move =
|
|
1269
|
-
const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
|
|
1270
|
-
const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
|
|
1271
|
-
break :blk @intToEnum(Move, r + mod);
|
|
1272
|
-
} else loop: {
|
|
1273
|
-
while (true) {
|
|
1274
|
-
const r = battle.rng.next();
|
|
1275
|
-
if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
|
|
1276
|
-
if (r >= @enumToInt(Move.Struggle)) continue;
|
|
1277
|
-
break :loop @intToEnum(Move, r);
|
|
1278
|
-
}
|
|
1279
|
-
};
|
|
1280
|
-
|
|
1318
|
+
side.last_selected_move = Rolls.metronome(battle);
|
|
1281
1319
|
incrementPP(side, mslot);
|
|
1282
1320
|
|
|
1283
1321
|
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
|
|
1284
|
-
return doMove(battle, player, mslot, auto, residual, log);
|
|
1322
|
+
return doMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
1285
1323
|
}
|
|
1286
1324
|
|
|
1287
1325
|
fn checkHit(battle: anytype, player: Player, move: Move.Data, log: anytype) !bool {
|
|
@@ -1352,10 +1390,7 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
|
|
|
1352
1390
|
if (overwrite) side.active.volatiles.state = accuracy;
|
|
1353
1391
|
|
|
1354
1392
|
// GLITCH: max accuracy is 255 so 1/256 chance of miss
|
|
1355
|
-
break :miss
|
|
1356
|
-
!battle.rng.chance(u8, @intCast(u8, accuracy), 256)
|
|
1357
|
-
else
|
|
1358
|
-
battle.rng.next() >= accuracy;
|
|
1393
|
+
break :miss !Rolls.hit(battle, accuracy);
|
|
1359
1394
|
};
|
|
1360
1395
|
|
|
1361
1396
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1491,11 +1526,7 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
|
|
|
1491
1526
|
stored.hp -= amount;
|
|
1492
1527
|
|
|
1493
1528
|
// As above, Pokémon Showdown uses damageOf but its not relevant
|
|
1494
|
-
if (amount > 0)
|
|
1495
|
-
try log.damage(ident, stored, .LeechSeed);
|
|
1496
|
-
// Pokémon Showdown erroneously updates last damage with uncapped Leech Seed damage
|
|
1497
|
-
if (showdown) battle.last_damage = damage;
|
|
1498
|
-
}
|
|
1529
|
+
if (amount > 0) try log.damage(ident, stored, .LeechSeed);
|
|
1499
1530
|
|
|
1500
1531
|
const before = foe_stored.hp;
|
|
1501
1532
|
// Uncapped damage is added back to the foe
|
|
@@ -1509,13 +1540,13 @@ fn endTurn(battle: anytype, log: anytype) @TypeOf(log).Error!Result {
|
|
|
1509
1540
|
assert(!battle.side(.P1).active.volatiles.MultiHit);
|
|
1510
1541
|
assert(!battle.side(.P2).active.volatiles.MultiHit);
|
|
1511
1542
|
|
|
1543
|
+
battle.turn += 1;
|
|
1544
|
+
|
|
1512
1545
|
if (showdown and options.ebc and checkEBC(battle)) {
|
|
1513
1546
|
try log.tie();
|
|
1514
1547
|
return Result.Tie;
|
|
1515
1548
|
}
|
|
1516
1549
|
|
|
1517
|
-
battle.turn += 1;
|
|
1518
|
-
|
|
1519
1550
|
if (showdown and battle.turn >= 1000) {
|
|
1520
1551
|
try log.tie();
|
|
1521
1552
|
return Result.Tie;
|
|
@@ -1582,6 +1613,42 @@ fn checkEBC(battle: anytype) bool {
|
|
|
1582
1613
|
return false;
|
|
1583
1614
|
}
|
|
1584
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
|
+
|
|
1585
1652
|
inline fn onBegin(
|
|
1586
1653
|
battle: anytype,
|
|
1587
1654
|
player: Player,
|
|
@@ -1672,10 +1739,7 @@ pub const Effects = struct {
|
|
|
1672
1739
|
side.active.volatiles.Bide = true;
|
|
1673
1740
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1674
1741
|
side.active.volatiles.state = 0;
|
|
1675
|
-
side.active.volatiles.attacks =
|
|
1676
|
-
battle.rng.range(u4, 2, 4)
|
|
1677
|
-
else
|
|
1678
|
-
(battle.rng.next() & 1) + 2);
|
|
1742
|
+
side.active.volatiles.attacks = Rolls.bideThrashDuration(battle);
|
|
1679
1743
|
|
|
1680
1744
|
try log.start(battle.active(player), .Bide);
|
|
1681
1745
|
}
|
|
@@ -1697,14 +1761,8 @@ pub const Effects = struct {
|
|
|
1697
1761
|
return;
|
|
1698
1762
|
}
|
|
1699
1763
|
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
else
|
|
1703
|
-
battle.rng.next() < 1 + (if (move.effect == .BurnChance1)
|
|
1704
|
-
Gen12.percent(10)
|
|
1705
|
-
else
|
|
1706
|
-
Gen12.percent(30));
|
|
1707
|
-
if (!chance) return;
|
|
1764
|
+
if (foe.active.types.includes(move.type)) return;
|
|
1765
|
+
if (!Rolls.secondaryChance(battle, move.effect == .BurnChance1)) return;
|
|
1708
1766
|
|
|
1709
1767
|
foe_stored.status = Status.init(.BRN);
|
|
1710
1768
|
foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
|
|
@@ -1728,11 +1786,7 @@ pub const Effects = struct {
|
|
|
1728
1786
|
const sub = foe.active.volatiles.Substitute;
|
|
1729
1787
|
|
|
1730
1788
|
if (move.effect == .ConfusionChance) {
|
|
1731
|
-
|
|
1732
|
-
battle.rng.chance(u8, 25, 256)
|
|
1733
|
-
else
|
|
1734
|
-
battle.rng.next() < Gen12.percent(10);
|
|
1735
|
-
if (!chance) return;
|
|
1789
|
+
if (!Rolls.confusionChance(battle)) return;
|
|
1736
1790
|
} else {
|
|
1737
1791
|
if (showdown) {
|
|
1738
1792
|
if (!try checkHit(battle, player, move, log)) {
|
|
@@ -1751,10 +1805,7 @@ pub const Effects = struct {
|
|
|
1751
1805
|
|
|
1752
1806
|
if (foe.active.volatiles.Confusion) return;
|
|
1753
1807
|
foe.active.volatiles.Confusion = true;
|
|
1754
|
-
foe.active.volatiles.confusion =
|
|
1755
|
-
battle.rng.range(u8, 2, 6)
|
|
1756
|
-
else
|
|
1757
|
-
(battle.rng.next() & 3) + 2);
|
|
1808
|
+
foe.active.volatiles.confusion = Rolls.confusionDuration(battle);
|
|
1758
1809
|
|
|
1759
1810
|
try log.start(battle.active(player.foe()), .Confusion);
|
|
1760
1811
|
}
|
|
@@ -1804,11 +1855,8 @@ pub const Effects = struct {
|
|
|
1804
1855
|
return Result.Error;
|
|
1805
1856
|
}
|
|
1806
1857
|
|
|
1807
|
-
volatiles.disabled_move = @intCast(u3,
|
|
1808
|
-
volatiles.disabled_duration =
|
|
1809
|
-
battle.rng.range(u8, 1, 9)
|
|
1810
|
-
else
|
|
1811
|
-
(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);
|
|
1812
1860
|
|
|
1813
1861
|
try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
|
|
1814
1862
|
return null;
|
|
@@ -1847,15 +1895,7 @@ pub const Effects = struct {
|
|
|
1847
1895
|
var volatiles = &battle.foe(player).active.volatiles;
|
|
1848
1896
|
|
|
1849
1897
|
if (volatiles.Substitute) return;
|
|
1850
|
-
|
|
1851
|
-
const chance = if (showdown)
|
|
1852
|
-
battle.rng.chance(u8, @as(u8, if (move.effect == .FlinchChance1) 26 else 77), 256)
|
|
1853
|
-
else
|
|
1854
|
-
battle.rng.next() < 1 + (if (move.effect == .FlinchChance1)
|
|
1855
|
-
Gen12.percent(10)
|
|
1856
|
-
else
|
|
1857
|
-
Gen12.percent(30));
|
|
1858
|
-
if (!chance) return;
|
|
1898
|
+
if (!Rolls.secondaryChance(battle, move.effect == .FlinchChance1)) return;
|
|
1859
1899
|
|
|
1860
1900
|
volatiles.Flinch = true;
|
|
1861
1901
|
volatiles.Recharging = false;
|
|
@@ -1881,12 +1921,8 @@ pub const Effects = struct {
|
|
|
1881
1921
|
return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
|
|
1882
1922
|
}
|
|
1883
1923
|
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
else
|
|
1887
|
-
battle.rng.next() < 1 + Gen12.percent(10);
|
|
1888
|
-
if (!chance) return;
|
|
1889
|
-
|
|
1924
|
+
if (foe.active.types.includes(move.type)) return;
|
|
1925
|
+
if (!Rolls.secondaryChance(battle, true)) return;
|
|
1890
1926
|
// Freeze Clause Mod
|
|
1891
1927
|
if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
1892
1928
|
|
|
@@ -2023,15 +2059,15 @@ pub const Effects = struct {
|
|
|
2023
2059
|
}
|
|
2024
2060
|
break :has_mimic false;
|
|
2025
2061
|
};
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
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;
|
|
2029
2065
|
return try log.fail(battle.active(player.foe()), .None);
|
|
2030
2066
|
}
|
|
2031
2067
|
}
|
|
2032
2068
|
if (!try checkHit(battle, player, move, log)) return;
|
|
2033
2069
|
|
|
2034
|
-
const rslot =
|
|
2070
|
+
const rslot = Rolls.moveSlot(battle, &foe.active.moves, 0);
|
|
2035
2071
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
2036
2072
|
|
|
2037
2073
|
try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
|
|
@@ -2052,7 +2088,8 @@ pub const Effects = struct {
|
|
|
2052
2088
|
assert(!side.active.volatiles.MultiHit);
|
|
2053
2089
|
side.active.volatiles.MultiHit = true;
|
|
2054
2090
|
|
|
2055
|
-
side.active.volatiles.attacks =
|
|
2091
|
+
side.active.volatiles.attacks =
|
|
2092
|
+
if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
|
|
2056
2093
|
}
|
|
2057
2094
|
|
|
2058
2095
|
fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2096,14 +2133,8 @@ pub const Effects = struct {
|
|
|
2096
2133
|
}
|
|
2097
2134
|
|
|
2098
2135
|
// Body Slam can't paralyze a Normal type Pokémon
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
else
|
|
2102
|
-
battle.rng.next() < 1 + (if (move.effect == .ParalyzeChance1)
|
|
2103
|
-
Gen12.percent(10)
|
|
2104
|
-
else
|
|
2105
|
-
Gen12.percent(30));
|
|
2106
|
-
if (!chance) return;
|
|
2136
|
+
if (foe.active.types.includes(move.type)) return;
|
|
2137
|
+
if (!Rolls.secondaryChance(battle, move.effect == .ParalyzeChance1)) return;
|
|
2107
2138
|
|
|
2108
2139
|
foe_stored.status = Status.init(.PAR);
|
|
2109
2140
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
@@ -2146,16 +2177,7 @@ pub const Effects = struct {
|
|
|
2146
2177
|
|
|
2147
2178
|
if (move.effect == .Poison) {
|
|
2148
2179
|
if (!showdown and !try checkHit(battle, player, move, log)) return;
|
|
2149
|
-
} else
|
|
2150
|
-
const chance = if (showdown)
|
|
2151
|
-
battle.rng.chance(u8, @as(u8, if (move.effect == .PoisonChance1) 52 else 103), 256)
|
|
2152
|
-
else
|
|
2153
|
-
battle.rng.next() < 1 + (if (move.effect == .PoisonChance1)
|
|
2154
|
-
Gen12.percent(20)
|
|
2155
|
-
else
|
|
2156
|
-
Gen12.percent(40));
|
|
2157
|
-
if (!chance) return;
|
|
2158
|
-
}
|
|
2180
|
+
} else if (!Rolls.poisonChance(battle, move.effect == .PoisonChance1)) return;
|
|
2159
2181
|
|
|
2160
2182
|
foe_stored.status = Status.init(.PSN);
|
|
2161
2183
|
if (toxic) {
|
|
@@ -2177,6 +2199,9 @@ pub const Effects = struct {
|
|
|
2177
2199
|
var side = battle.side(player);
|
|
2178
2200
|
var stored = side.stored();
|
|
2179
2201
|
|
|
2202
|
+
assert(showdown or battle.last_damage > 0);
|
|
2203
|
+
if (showdown and battle.last_damage == 0) return;
|
|
2204
|
+
|
|
2180
2205
|
const damage = @intCast(i16, @max(battle.last_damage /
|
|
2181
2206
|
@as(u8, if (side.last_selected_move == .Struggle) 2 else 4), 1));
|
|
2182
2207
|
stored.hp = @intCast(u16, @max(@intCast(i16, stored.hp) - damage, 0));
|
|
@@ -2222,16 +2247,7 @@ pub const Effects = struct {
|
|
|
2222
2247
|
}
|
|
2223
2248
|
foe.active.volatiles.Recharging = false;
|
|
2224
2249
|
|
|
2225
|
-
|
|
2226
|
-
battle.rng.range(u8, 1, 8)
|
|
2227
|
-
else loop: {
|
|
2228
|
-
while (true) {
|
|
2229
|
-
const r = battle.rng.next() & 7;
|
|
2230
|
-
if (r != 0) break :loop r;
|
|
2231
|
-
}
|
|
2232
|
-
});
|
|
2233
|
-
|
|
2234
|
-
foe_stored.status = Status.slp(duration);
|
|
2250
|
+
foe_stored.status = Status.slp(Rolls.sleepDuration(battle));
|
|
2235
2251
|
try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
|
|
2236
2252
|
}
|
|
2237
2253
|
|
|
@@ -2284,10 +2300,7 @@ pub const Effects = struct {
|
|
|
2284
2300
|
assert(!volatiles.Bide);
|
|
2285
2301
|
|
|
2286
2302
|
volatiles.Thrashing = true;
|
|
2287
|
-
volatiles.attacks =
|
|
2288
|
-
battle.rng.range(u8, 2, 4)
|
|
2289
|
-
else
|
|
2290
|
-
(battle.rng.next() & 1) + 2);
|
|
2303
|
+
volatiles.attacks = Rolls.bideThrashDuration(battle);
|
|
2291
2304
|
}
|
|
2292
2305
|
|
|
2293
2306
|
fn transform(battle: anytype, player: Player, log: anytype) !void {
|
|
@@ -2319,7 +2332,7 @@ pub const Effects = struct {
|
|
|
2319
2332
|
try log.transform(battle.active(player), foe_ident);
|
|
2320
2333
|
}
|
|
2321
2334
|
|
|
2322
|
-
fn binding(battle: anytype, player: Player) void {
|
|
2335
|
+
fn binding(battle: anytype, player: Player, rewrap: bool) void {
|
|
2323
2336
|
var side = battle.side(player);
|
|
2324
2337
|
var foe = battle.foe(player);
|
|
2325
2338
|
|
|
@@ -2327,9 +2340,11 @@ pub const Effects = struct {
|
|
|
2327
2340
|
side.active.volatiles.Binding = true;
|
|
2328
2341
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2329
2342
|
// (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
|
|
2330
|
-
if (!showdown)
|
|
2343
|
+
if (!showdown) {
|
|
2344
|
+
foe.active.volatiles.Recharging = false;
|
|
2345
|
+
} else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
|
|
2331
2346
|
|
|
2332
|
-
side.active.volatiles.attacks = distribution(battle) - 1;
|
|
2347
|
+
side.active.volatiles.attacks = Rolls.distribution(battle) - 1;
|
|
2333
2348
|
}
|
|
2334
2349
|
|
|
2335
2350
|
fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2356,7 +2371,7 @@ pub const Effects = struct {
|
|
|
2356
2371
|
return try log.fail(ident, .None);
|
|
2357
2372
|
}
|
|
2358
2373
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2359
|
-
const stat = unmodifiedStats(battle,
|
|
2374
|
+
const stat = unmodifiedStats(battle, side).atk;
|
|
2360
2375
|
stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2361
2376
|
try log.boost(ident, reason, n);
|
|
2362
2377
|
// Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
|
|
@@ -2376,7 +2391,7 @@ pub const Effects = struct {
|
|
|
2376
2391
|
return try log.fail(ident, .None);
|
|
2377
2392
|
}
|
|
2378
2393
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2379
|
-
const stat = unmodifiedStats(battle,
|
|
2394
|
+
const stat = unmodifiedStats(battle, side).def;
|
|
2380
2395
|
stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2381
2396
|
try log.boost(ident, .Defense, n);
|
|
2382
2397
|
},
|
|
@@ -2393,7 +2408,7 @@ pub const Effects = struct {
|
|
|
2393
2408
|
return try log.fail(ident, .None);
|
|
2394
2409
|
}
|
|
2395
2410
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2396
|
-
const stat = unmodifiedStats(battle,
|
|
2411
|
+
const stat = unmodifiedStats(battle, side).spe;
|
|
2397
2412
|
stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2398
2413
|
try log.boost(ident, .Speed, 2);
|
|
2399
2414
|
},
|
|
@@ -2413,7 +2428,7 @@ pub const Effects = struct {
|
|
|
2413
2428
|
return try log.fail(ident, .None);
|
|
2414
2429
|
}
|
|
2415
2430
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2416
|
-
const stat = unmodifiedStats(battle,
|
|
2431
|
+
const stat = unmodifiedStats(battle, side).spc;
|
|
2417
2432
|
stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2418
2433
|
try log.boost(ident, .SpecialAttack, n);
|
|
2419
2434
|
try log.boost(ident, .SpecialDefense, n);
|
|
@@ -2434,28 +2449,24 @@ pub const Effects = struct {
|
|
|
2434
2449
|
fn unboost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
2435
2450
|
var foe = battle.foe(player);
|
|
2436
2451
|
const foe_ident = battle.active(player.foe());
|
|
2452
|
+
const secondary = move.effect.isStatDownChance();
|
|
2437
2453
|
|
|
2438
|
-
if (foe.active.volatiles.Substitute)
|
|
2439
|
-
return if (!move.effect.isStatDownChance()) log.fail(foe_ident, .None);
|
|
2440
|
-
}
|
|
2454
|
+
if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(foe_ident, .None);
|
|
2441
2455
|
|
|
2442
|
-
if (
|
|
2443
|
-
|
|
2444
|
-
battle.rng.chance(u8, 85, 256)
|
|
2445
|
-
else
|
|
2446
|
-
battle.rng.next() < Gen12.percent(33) + 1;
|
|
2447
|
-
if (!chance or foe.active.volatiles.Invulnerable) return;
|
|
2456
|
+
if (secondary) {
|
|
2457
|
+
if (!Rolls.unboost(battle) or foe.active.volatiles.Invulnerable) return;
|
|
2448
2458
|
} else if (!showdown and !try checkHit(battle, player, move, log)) {
|
|
2449
2459
|
return; // checkHit already checks for Invulnerable
|
|
2450
2460
|
}
|
|
2451
2461
|
|
|
2452
2462
|
var stats = &foe.active.stats;
|
|
2453
2463
|
var boosts = &foe.active.boosts;
|
|
2464
|
+
const fail = showdown or !secondary;
|
|
2454
2465
|
|
|
2455
2466
|
switch (move.effect) {
|
|
2456
2467
|
.AttackDown1, .AttackDownChance => {
|
|
2457
2468
|
assert(boosts.atk >= -6 and boosts.atk <= 6);
|
|
2458
|
-
if (boosts.atk == -6) return try log.fail(foe_ident, .None);
|
|
2469
|
+
if (boosts.atk == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2459
2470
|
boosts.atk = @intCast(i4, @max(-6, @as(i8, boosts.atk) - 1));
|
|
2460
2471
|
if (stats.atk == 1) {
|
|
2461
2472
|
boosts.atk += 1;
|
|
@@ -2466,13 +2477,13 @@ pub const Effects = struct {
|
|
|
2466
2477
|
return try log.fail(foe_ident, .None);
|
|
2467
2478
|
}
|
|
2468
2479
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2469
|
-
const stat = unmodifiedStats(battle,
|
|
2480
|
+
const stat = unmodifiedStats(battle, foe).atk;
|
|
2470
2481
|
stats.atk = @max(1, stat * mod[0] / mod[1]);
|
|
2471
2482
|
try log.boost(foe_ident, .Attack, -1);
|
|
2472
2483
|
},
|
|
2473
2484
|
.DefenseDown1, .DefenseDown2, .DefenseDownChance => {
|
|
2474
2485
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
2475
|
-
if (boosts.def == -6) return try log.fail(foe_ident, .None);
|
|
2486
|
+
if (boosts.def == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2476
2487
|
const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
|
|
2477
2488
|
boosts.def = @intCast(i4, @max(-6, @as(i8, boosts.def) - n));
|
|
2478
2489
|
if (stats.def == 1) {
|
|
@@ -2484,13 +2495,13 @@ pub const Effects = struct {
|
|
|
2484
2495
|
return try log.fail(foe_ident, .None);
|
|
2485
2496
|
}
|
|
2486
2497
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2487
|
-
const stat = unmodifiedStats(battle,
|
|
2498
|
+
const stat = unmodifiedStats(battle, foe).def;
|
|
2488
2499
|
stats.def = @max(1, stat * mod[0] / mod[1]);
|
|
2489
2500
|
try log.boost(foe_ident, .Defense, -@as(i8, n));
|
|
2490
2501
|
},
|
|
2491
2502
|
.SpeedDown1, .SpeedDownChance => {
|
|
2492
2503
|
assert(boosts.spe >= -6 and boosts.spe <= 6);
|
|
2493
|
-
if (boosts.spe == -6) return try log.fail(foe_ident, .None);
|
|
2504
|
+
if (boosts.spe == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2494
2505
|
boosts.spe = @intCast(i4, @max(-6, @as(i8, boosts.spe) - 1));
|
|
2495
2506
|
if (stats.spe == 1) {
|
|
2496
2507
|
boosts.spe += 1;
|
|
@@ -2501,14 +2512,14 @@ pub const Effects = struct {
|
|
|
2501
2512
|
return try log.fail(foe_ident, .None);
|
|
2502
2513
|
}
|
|
2503
2514
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2504
|
-
const stat = unmodifiedStats(battle,
|
|
2515
|
+
const stat = unmodifiedStats(battle, foe).spe;
|
|
2505
2516
|
stats.spe = @max(1, stat * mod[0] / mod[1]);
|
|
2506
2517
|
try log.boost(foe_ident, .Speed, -1);
|
|
2507
2518
|
assert(boosts.spe >= -6);
|
|
2508
2519
|
},
|
|
2509
2520
|
.SpecialDownChance => {
|
|
2510
2521
|
assert(boosts.spc >= -6 and boosts.spc <= 6);
|
|
2511
|
-
if (boosts.spc == -6) return try log.fail(foe_ident, .None);
|
|
2522
|
+
if (boosts.spc == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2512
2523
|
boosts.spc = @intCast(i4, @max(-6, @as(i8, boosts.spc) - 1));
|
|
2513
2524
|
if (stats.spc == 1) {
|
|
2514
2525
|
boosts.spc += 1;
|
|
@@ -2521,14 +2532,14 @@ pub const Effects = struct {
|
|
|
2521
2532
|
return try log.fail(foe_ident, .None);
|
|
2522
2533
|
}
|
|
2523
2534
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2524
|
-
const stat = unmodifiedStats(battle,
|
|
2535
|
+
const stat = unmodifiedStats(battle, foe).spc;
|
|
2525
2536
|
stats.spc = @max(1, stat * mod[0] / mod[1]);
|
|
2526
2537
|
try log.boost(foe_ident, .SpecialAttack, -1);
|
|
2527
2538
|
try log.boost(foe_ident, .SpecialDefense, -1);
|
|
2528
2539
|
},
|
|
2529
2540
|
.AccuracyDown1 => {
|
|
2530
2541
|
assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
|
|
2531
|
-
if (boosts.accuracy == -6) return try log.fail(foe_ident, .None);
|
|
2542
|
+
if (boosts.accuracy == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2532
2543
|
boosts.accuracy = @intCast(i4, @max(-6, @as(i8, boosts.accuracy) - 1));
|
|
2533
2544
|
try log.boost(foe_ident, .Accuracy, -1);
|
|
2534
2545
|
},
|
|
@@ -2540,8 +2551,7 @@ pub const Effects = struct {
|
|
|
2540
2551
|
}
|
|
2541
2552
|
};
|
|
2542
2553
|
|
|
2543
|
-
fn unmodifiedStats(battle: anytype,
|
|
2544
|
-
const side = battle.side(who);
|
|
2554
|
+
fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
|
|
2545
2555
|
if (!side.active.volatiles.Transform) return &side.stored().stats;
|
|
2546
2556
|
const id = ID.from(side.active.volatiles.transform);
|
|
2547
2557
|
return &battle.side(id.player).pokemon[id.id - 1].stats;
|
|
@@ -2607,80 +2617,140 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
|
|
|
2607
2617
|
}
|
|
2608
2618
|
}
|
|
2609
2619
|
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2620
|
+
pub const Rolls = struct {
|
|
2621
|
+
inline fn speedTie(battle: anytype) bool {
|
|
2622
|
+
if (showdown) return battle.rng.range(u8, 0, 2) == 0;
|
|
2623
|
+
return battle.rng.next() < Gen12.percent(50) + 1;
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2626
|
+
inline fn criticalHit(battle: anytype, chance: u16) bool {
|
|
2627
|
+
if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
|
|
2628
|
+
return std.math.rotl(u8, battle.rng.next(), 3) < chance;
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2631
|
+
inline fn damage(battle: anytype) u8 {
|
|
2632
|
+
if (showdown) return battle.rng.range(u8, 217, 256);
|
|
2633
|
+
while (true) {
|
|
2634
|
+
const r = std.math.rotr(u8, battle.rng.next(), 1);
|
|
2635
|
+
if (r >= 217) return r;
|
|
2623
2636
|
}
|
|
2624
2637
|
}
|
|
2625
|
-
return false;
|
|
2626
|
-
}
|
|
2627
2638
|
|
|
2628
|
-
inline fn
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
try Effects.boost(battle, who, Move.get(.Rage), log);
|
|
2639
|
+
inline fn hit(battle: anytype, accuracy: u16) bool {
|
|
2640
|
+
if (showdown) return battle.rng.chance(u8, @intCast(u8, accuracy), 256);
|
|
2641
|
+
return battle.rng.next() < accuracy;
|
|
2632
2642
|
}
|
|
2633
|
-
}
|
|
2634
2643
|
|
|
2635
|
-
fn
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2644
|
+
inline fn confusionDuration(battle: anytype) u3 {
|
|
2645
|
+
if (showdown) return @intCast(u3, battle.rng.range(u8, 2, 6));
|
|
2646
|
+
return @intCast(u3, (battle.rng.next() & 3) + 2);
|
|
2647
|
+
}
|
|
2639
2648
|
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
else
|
|
2645
|
-
(battle.rng.next() & 3) + 2);
|
|
2649
|
+
inline fn confused(battle: anytype) bool {
|
|
2650
|
+
if (showdown) return !battle.rng.chance(u8, 128, 256);
|
|
2651
|
+
return battle.rng.next() >= Gen12.percent(50) + 1;
|
|
2652
|
+
}
|
|
2646
2653
|
|
|
2647
|
-
|
|
2648
|
-
|
|
2654
|
+
inline fn paralyzed(battle: anytype) bool {
|
|
2655
|
+
if (showdown) return battle.rng.chance(u8, 63, 256);
|
|
2656
|
+
return battle.rng.next() < Gen12.percent(25);
|
|
2657
|
+
}
|
|
2649
2658
|
|
|
2650
|
-
|
|
2659
|
+
inline fn confusionChance(battle: anytype) bool {
|
|
2660
|
+
if (showdown) return battle.rng.chance(u8, 25, 256);
|
|
2661
|
+
return battle.rng.next() < Gen12.percent(10);
|
|
2662
|
+
}
|
|
2651
2663
|
|
|
2652
|
-
fn
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
}
|
|
2664
|
+
inline fn secondaryChance(battle: anytype, low: bool) bool {
|
|
2665
|
+
if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 26 else 77), 256);
|
|
2666
|
+
return battle.rng.next() < 1 + (if (low) Gen12.percent(10) else Gen12.percent(30));
|
|
2667
|
+
}
|
|
2657
2668
|
|
|
2658
|
-
fn
|
|
2659
|
-
|
|
2660
|
-
if (
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2669
|
+
inline fn poisonChance(battle: anytype, low: bool) bool {
|
|
2670
|
+
if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 52 else 103), 256);
|
|
2671
|
+
return battle.rng.next() < 1 + (if (low) Gen12.percent(20) else Gen12.percent(40));
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
inline fn sleepDuration(battle: anytype) u3 {
|
|
2675
|
+
if (showdown) return @intCast(u3, battle.rng.range(u8, 1, 8));
|
|
2676
|
+
while (true) {
|
|
2677
|
+
const r = battle.rng.next() & 7;
|
|
2678
|
+
if (r != 0) return @intCast(u3, r);
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
|
|
2682
|
+
inline fn bideThrashDuration(battle: anytype) u3 {
|
|
2683
|
+
if (showdown) return @intCast(u3, battle.rng.range(u4, 2, 4));
|
|
2684
|
+
return @intCast(u3, (battle.rng.next() & 1) + 2);
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
fn moveSlot(battle: anytype, moves: []MoveSlot, check_pp: u4) u4 {
|
|
2688
|
+
if (showdown) {
|
|
2689
|
+
if (check_pp == 0) {
|
|
2690
|
+
var i: usize = moves.len;
|
|
2691
|
+
while (i > 0) {
|
|
2692
|
+
i -= 1;
|
|
2693
|
+
if (moves[i].id != .None) {
|
|
2694
|
+
return battle.rng.range(u4, 0, @intCast(u4, i + 1)) + 1;
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
} else {
|
|
2698
|
+
var r = battle.rng.range(u4, 0, @intCast(u4, check_pp)) + 1;
|
|
2699
|
+
var i: usize = 0;
|
|
2700
|
+
while (i < moves.len and r > 0) : (i += 1) {
|
|
2701
|
+
if (moves[i].pp > 0) {
|
|
2702
|
+
r -= 1;
|
|
2703
|
+
if (r == 0) break;
|
|
2704
|
+
}
|
|
2673
2705
|
}
|
|
2706
|
+
return @intCast(u4, i + 1);
|
|
2674
2707
|
}
|
|
2675
|
-
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2710
|
+
while (true) {
|
|
2711
|
+
const r = @intCast(u4, battle.rng.next() & 3);
|
|
2712
|
+
if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
|
|
2676
2713
|
}
|
|
2677
2714
|
}
|
|
2678
2715
|
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2716
|
+
inline fn disableDuration(battle: anytype) u4 {
|
|
2717
|
+
if (showdown) return @intCast(u4, battle.rng.range(u8, 1, 9));
|
|
2718
|
+
return @intCast(u4, (battle.rng.next() & 7) + 1);
|
|
2682
2719
|
}
|
|
2683
|
-
|
|
2720
|
+
|
|
2721
|
+
inline fn metronome(battle: anytype) Move {
|
|
2722
|
+
if (showdown) {
|
|
2723
|
+
const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
|
|
2724
|
+
const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
|
|
2725
|
+
return @intToEnum(Move, r + mod);
|
|
2726
|
+
}
|
|
2727
|
+
while (true) {
|
|
2728
|
+
const r = battle.rng.next();
|
|
2729
|
+
if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
|
|
2730
|
+
if (r >= @enumToInt(Move.Struggle)) continue;
|
|
2731
|
+
return @intToEnum(Move, r);
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
|
|
2735
|
+
inline fn psywave(battle: anytype, max: u8) u8 {
|
|
2736
|
+
if (showdown) return battle.rng.range(u8, 0, max);
|
|
2737
|
+
while (true) {
|
|
2738
|
+
const r = battle.rng.next();
|
|
2739
|
+
if (r < max) return r;
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
|
|
2743
|
+
inline fn distribution(battle: anytype) u3 {
|
|
2744
|
+
if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
|
|
2745
|
+
const r = (battle.rng.next() & 3);
|
|
2746
|
+
return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
|
|
2747
|
+
}
|
|
2748
|
+
|
|
2749
|
+
inline fn unboost(battle: anytype) bool {
|
|
2750
|
+
if (showdown) return battle.rng.chance(u8, 85, 256);
|
|
2751
|
+
return battle.rng.next() < Gen12.percent(33) + 1;
|
|
2752
|
+
}
|
|
2753
|
+
};
|
|
2684
2754
|
|
|
2685
2755
|
test "RNG agreement" {
|
|
2686
2756
|
if (!showdown) return;
|