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