@pkmn/engine 0.1.0-dev.d716e940 → 0.1.0-dev.fb5bf955
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 +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/test/display.js +13 -31
- package/build/test/display.js.map +1 -1
- package/build.zig +22 -17
- package/package.json +9 -10
- package/src/bin/install-pkmn-engine +3 -3
- package/src/bin/pkmn-debug +6 -29
- 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 -99
- package/src/lib/gen1/data.zig +17 -19
- package/src/lib/gen1/helpers.zig +14 -3
- package/src/lib/gen1/mechanics.zig +304 -378
- package/src/lib/gen2/data.zig +4 -4
- package/src/lib/pkmn.zig +7 -7
|
@@ -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,
|
|
@@ -860,7 +796,7 @@ fn doMove(
|
|
|
860
796
|
};
|
|
861
797
|
assert(!immune or miss or (showdown and move.effect == .Binding));
|
|
862
798
|
|
|
863
|
-
var late = showdown
|
|
799
|
+
var late = showdown;
|
|
864
800
|
const skip = status or immune;
|
|
865
801
|
if ((!showdown or (!skip or counter)) and !miss) blk: {
|
|
866
802
|
if (showdown and move.effect.isMulti()) {
|
|
@@ -913,9 +849,9 @@ fn doMove(
|
|
|
913
849
|
|
|
914
850
|
if (!showdown or !miss) {
|
|
915
851
|
if (move.effect == .MirrorMove) {
|
|
916
|
-
return mirrorMove(battle, player, mslot,
|
|
852
|
+
return mirrorMove(battle, player, mslot, auto, residual, log);
|
|
917
853
|
} else if (move.effect == .Metronome) {
|
|
918
|
-
return metronome(battle, player, mslot,
|
|
854
|
+
return metronome(battle, player, mslot, auto, residual, log);
|
|
919
855
|
} else if (move.effect.onEnd()) {
|
|
920
856
|
try onEnd(battle, player, move, log);
|
|
921
857
|
return null;
|
|
@@ -930,7 +866,7 @@ fn doMove(
|
|
|
930
866
|
move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
|
|
931
867
|
if (ohko) {
|
|
932
868
|
try log.immune(foe_ident, .OHKO);
|
|
933
|
-
} else if (immune and !invulnerable
|
|
869
|
+
} else if (immune and !invulnerable) {
|
|
934
870
|
try log.immune(foe_ident, .None);
|
|
935
871
|
} else if (mist) {
|
|
936
872
|
if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
|
|
@@ -980,7 +916,7 @@ fn doMove(
|
|
|
980
916
|
if (hit == 0 and ohko) try log.ohko();
|
|
981
917
|
hit += 1;
|
|
982
918
|
if (foe.stored().hp == 0) break;
|
|
983
|
-
if (!late and
|
|
919
|
+
if (!late and !sub) try buildRage(battle, player.foe(), log);
|
|
984
920
|
// If the substitute breaks during a multi-hit attack, the attack ends
|
|
985
921
|
if (nullified) break;
|
|
986
922
|
}
|
|
@@ -996,17 +932,11 @@ fn doMove(
|
|
|
996
932
|
} else if (showdown) {
|
|
997
933
|
// This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
|
|
998
934
|
if (move.effect == .Binding) {
|
|
999
|
-
Effects.binding(battle, player
|
|
935
|
+
Effects.binding(battle, player);
|
|
1000
936
|
if (immune) {
|
|
1001
937
|
battle.last_damage = 0;
|
|
1002
|
-
assert(foe.stored().hp > 0);
|
|
1003
938
|
// 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
|
-
}
|
|
939
|
+
try log.damage(battle.active(player.foe()), foe.stored(), .None);
|
|
1010
940
|
return null;
|
|
1011
941
|
}
|
|
1012
942
|
}
|
|
@@ -1057,7 +987,8 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
|
|
|
1057
987
|
chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
|
|
1058
988
|
chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
|
|
1059
989
|
|
|
1060
|
-
return
|
|
990
|
+
if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
|
|
991
|
+
return std.math.rotl(u8, battle.rng.next(), 3) < chance;
|
|
1061
992
|
}
|
|
1062
993
|
|
|
1063
994
|
fn calcDamage(
|
|
@@ -1161,7 +1092,16 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
|
1161
1092
|
|
|
1162
1093
|
fn randomizeDamage(battle: anytype) void {
|
|
1163
1094
|
if (battle.last_damage <= 1) return;
|
|
1164
|
-
|
|
1095
|
+
|
|
1096
|
+
const random = if (showdown)
|
|
1097
|
+
battle.rng.range(u8, 217, 256)
|
|
1098
|
+
else loop: {
|
|
1099
|
+
while (true) {
|
|
1100
|
+
const r = std.math.rotr(u8, battle.rng.next(), 1);
|
|
1101
|
+
if (r >= 217) break :loop r;
|
|
1102
|
+
}
|
|
1103
|
+
};
|
|
1104
|
+
|
|
1165
1105
|
battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
|
|
1166
1106
|
}
|
|
1167
1107
|
|
|
@@ -1179,9 +1119,16 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
|
|
|
1179
1119
|
// GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
|
|
1180
1120
|
.Psywave => power: {
|
|
1181
1121
|
const max = @intCast(u8, @as(u16, side.stored().level) * 3 / 2);
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1122
|
+
if (showdown) {
|
|
1123
|
+
break :power battle.rng.range(u8, 0, max);
|
|
1124
|
+
} else {
|
|
1125
|
+
// GLITCH: Psywave infinite glitch loop
|
|
1126
|
+
if (max <= 1) return Result.Error;
|
|
1127
|
+
while (true) {
|
|
1128
|
+
const r = battle.rng.next();
|
|
1129
|
+
if (r < max) break :power r;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1185
1132
|
},
|
|
1186
1133
|
else => unreachable,
|
|
1187
1134
|
};
|
|
@@ -1203,17 +1150,19 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
|
|
|
1203
1150
|
return null;
|
|
1204
1151
|
}
|
|
1205
1152
|
|
|
1206
|
-
// Pretend
|
|
1153
|
+
// Pretend Counter was used as a stand-in when no move has been used to fail below with 0 BP
|
|
1154
|
+
const foe_last_used_move =
|
|
1155
|
+
Move.get(if (foe.last_used_move == .None) .Counter else foe.last_used_move);
|
|
1207
1156
|
const foe_last_selected_move =
|
|
1208
1157
|
Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
|
|
1209
|
-
.
|
|
1158
|
+
.Counter
|
|
1210
1159
|
else
|
|
1211
1160
|
foe.last_selected_move);
|
|
1212
1161
|
|
|
1213
|
-
const used =
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1162
|
+
const used = foe_last_used_move.bp > 0 and
|
|
1163
|
+
foe.last_used_move != .Counter and
|
|
1164
|
+
(foe_last_used_move.type == .Normal or
|
|
1165
|
+
foe_last_used_move.type == .Fighting);
|
|
1217
1166
|
|
|
1218
1167
|
const selected = foe_last_selected_move.bp > 0 and
|
|
1219
1168
|
foe.last_selected_move != .Counter and
|
|
@@ -1232,14 +1181,12 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
|
|
|
1232
1181
|
return null;
|
|
1233
1182
|
}
|
|
1234
1183
|
|
|
1235
|
-
battle.last_damage
|
|
1184
|
+
battle.last_damage = if (battle.last_damage > 0x7FFF) 0xFFFF else battle.last_damage * 2;
|
|
1236
1185
|
|
|
1237
1186
|
// Pokémon Showdown calls checkHit before Counter
|
|
1238
1187
|
if (!showdown and !try checkHit(battle, player, move, log)) return null;
|
|
1239
1188
|
|
|
1240
|
-
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1241
1189
|
_ = 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
1190
|
return null;
|
|
1244
1191
|
}
|
|
1245
1192
|
|
|
@@ -1283,7 +1230,6 @@ fn mirrorMove(
|
|
|
1283
1230
|
battle: anytype,
|
|
1284
1231
|
player: Player,
|
|
1285
1232
|
mslot: u4,
|
|
1286
|
-
rewrap: bool,
|
|
1287
1233
|
auto: bool,
|
|
1288
1234
|
residual: *bool,
|
|
1289
1235
|
log: anytype,
|
|
@@ -1296,30 +1242,41 @@ fn mirrorMove(
|
|
|
1296
1242
|
if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
|
|
1297
1243
|
try log.fail(battle.active(player), .None);
|
|
1298
1244
|
return null;
|
|
1299
|
-
} else if (!showdown or foe.last_used_move != .Struggle) {
|
|
1300
|
-
incrementPP(side, mslot);
|
|
1301
1245
|
}
|
|
1302
1246
|
|
|
1247
|
+
incrementPP(side, mslot);
|
|
1248
|
+
|
|
1303
1249
|
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
|
|
1304
|
-
return doMove(battle, player, mslot,
|
|
1250
|
+
return doMove(battle, player, mslot, auto, residual, log);
|
|
1305
1251
|
}
|
|
1306
1252
|
|
|
1307
1253
|
fn metronome(
|
|
1308
1254
|
battle: anytype,
|
|
1309
1255
|
player: Player,
|
|
1310
1256
|
mslot: u4,
|
|
1311
|
-
rewrap: bool,
|
|
1312
1257
|
auto: bool,
|
|
1313
1258
|
residual: *bool,
|
|
1314
1259
|
log: anytype,
|
|
1315
1260
|
) !?Result {
|
|
1316
1261
|
var side = battle.side(player);
|
|
1317
1262
|
|
|
1318
|
-
side.last_selected_move =
|
|
1263
|
+
side.last_selected_move = if (showdown) blk: {
|
|
1264
|
+
const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
|
|
1265
|
+
const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
|
|
1266
|
+
break :blk @intToEnum(Move, r + mod);
|
|
1267
|
+
} else loop: {
|
|
1268
|
+
while (true) {
|
|
1269
|
+
const r = battle.rng.next();
|
|
1270
|
+
if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
|
|
1271
|
+
if (r >= @enumToInt(Move.Struggle)) continue;
|
|
1272
|
+
break :loop @intToEnum(Move, r);
|
|
1273
|
+
}
|
|
1274
|
+
};
|
|
1275
|
+
|
|
1319
1276
|
incrementPP(side, mslot);
|
|
1320
1277
|
|
|
1321
1278
|
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
|
|
1322
|
-
return doMove(battle, player, mslot,
|
|
1279
|
+
return doMove(battle, player, mslot, auto, residual, log);
|
|
1323
1280
|
}
|
|
1324
1281
|
|
|
1325
1282
|
fn checkHit(battle: anytype, player: Player, move: Move.Data, log: anytype) !bool {
|
|
@@ -1373,24 +1330,23 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
|
|
|
1373
1330
|
}
|
|
1374
1331
|
|
|
1375
1332
|
// 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));
|
|
1333
|
+
var state = &side.active.volatiles.state;
|
|
1334
|
+
const overwritten = (move.effect == .Thrashing or move.effect == .Rage) and state.* > 0;
|
|
1335
|
+
assert(!overwritten or (0 < state.* and state.* <= 255 and !side.active.volatiles.Bide));
|
|
1336
|
+
var accuracy = if (overwritten) state.* else @as(u16, Gen12.percent(move.accuracy));
|
|
1382
1337
|
var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
|
|
1383
1338
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1384
1339
|
boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
|
|
1385
1340
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1386
1341
|
accuracy = @min(255, @max(1, accuracy));
|
|
1387
1342
|
|
|
1388
|
-
|
|
1389
|
-
if (showdown) overwrite = side.active.volatiles.Rage or side.active.volatiles.Thrashing;
|
|
1390
|
-
if (overwrite) side.active.volatiles.state = accuracy;
|
|
1343
|
+
if (move.effect == .Thrashing or move.effect == .Rage) state.* = accuracy;
|
|
1391
1344
|
|
|
1392
1345
|
// GLITCH: max accuracy is 255 so 1/256 chance of miss
|
|
1393
|
-
break :miss
|
|
1346
|
+
break :miss if (showdown)
|
|
1347
|
+
!battle.rng.chance(u8, @intCast(u8, accuracy), 256)
|
|
1348
|
+
else
|
|
1349
|
+
battle.rng.next() >= accuracy;
|
|
1394
1350
|
};
|
|
1395
1351
|
|
|
1396
1352
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1522,6 +1478,9 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
|
|
|
1522
1478
|
damage *= volatiles.toxic;
|
|
1523
1479
|
}
|
|
1524
1480
|
|
|
1481
|
+
// Pokémon Showdown erroneously updates last damage with uncapped Leech Seed damage
|
|
1482
|
+
if (showdown) battle.last_damage = damage;
|
|
1483
|
+
|
|
1525
1484
|
const amount = @min(damage, stored.hp);
|
|
1526
1485
|
stored.hp -= amount;
|
|
1527
1486
|
|
|
@@ -1540,13 +1499,13 @@ fn endTurn(battle: anytype, log: anytype) @TypeOf(log).Error!Result {
|
|
|
1540
1499
|
assert(!battle.side(.P1).active.volatiles.MultiHit);
|
|
1541
1500
|
assert(!battle.side(.P2).active.volatiles.MultiHit);
|
|
1542
1501
|
|
|
1543
|
-
battle.turn += 1;
|
|
1544
|
-
|
|
1545
1502
|
if (showdown and options.ebc and checkEBC(battle)) {
|
|
1546
1503
|
try log.tie();
|
|
1547
1504
|
return Result.Tie;
|
|
1548
1505
|
}
|
|
1549
1506
|
|
|
1507
|
+
battle.turn += 1;
|
|
1508
|
+
|
|
1550
1509
|
if (showdown and battle.turn >= 1000) {
|
|
1551
1510
|
try log.tie();
|
|
1552
1511
|
return Result.Tie;
|
|
@@ -1613,42 +1572,6 @@ fn checkEBC(battle: anytype) bool {
|
|
|
1613
1572
|
return false;
|
|
1614
1573
|
}
|
|
1615
1574
|
|
|
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
1575
|
inline fn onBegin(
|
|
1653
1576
|
battle: anytype,
|
|
1654
1577
|
player: Player,
|
|
@@ -1739,7 +1662,10 @@ pub const Effects = struct {
|
|
|
1739
1662
|
side.active.volatiles.Bide = true;
|
|
1740
1663
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1741
1664
|
side.active.volatiles.state = 0;
|
|
1742
|
-
side.active.volatiles.attacks =
|
|
1665
|
+
side.active.volatiles.attacks = @intCast(u3, if (showdown)
|
|
1666
|
+
battle.rng.range(u4, 2, 4)
|
|
1667
|
+
else
|
|
1668
|
+
(battle.rng.next() & 1) + 2);
|
|
1743
1669
|
|
|
1744
1670
|
try log.start(battle.active(player), .Bide);
|
|
1745
1671
|
}
|
|
@@ -1761,8 +1687,14 @@ pub const Effects = struct {
|
|
|
1761
1687
|
return;
|
|
1762
1688
|
}
|
|
1763
1689
|
|
|
1764
|
-
|
|
1765
|
-
|
|
1690
|
+
const chance = !foe.active.types.includes(move.type) and if (showdown)
|
|
1691
|
+
battle.rng.chance(u8, @as(u8, if (move.effect == .BurnChance1) 26 else 77), 256)
|
|
1692
|
+
else
|
|
1693
|
+
battle.rng.next() < 1 + (if (move.effect == .BurnChance1)
|
|
1694
|
+
Gen12.percent(10)
|
|
1695
|
+
else
|
|
1696
|
+
Gen12.percent(30));
|
|
1697
|
+
if (!chance) return;
|
|
1766
1698
|
|
|
1767
1699
|
foe_stored.status = Status.init(.BRN);
|
|
1768
1700
|
foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
|
|
@@ -1786,7 +1718,11 @@ pub const Effects = struct {
|
|
|
1786
1718
|
const sub = foe.active.volatiles.Substitute;
|
|
1787
1719
|
|
|
1788
1720
|
if (move.effect == .ConfusionChance) {
|
|
1789
|
-
if (
|
|
1721
|
+
const chance = if (showdown)
|
|
1722
|
+
battle.rng.chance(u8, 25, 256)
|
|
1723
|
+
else
|
|
1724
|
+
battle.rng.next() < Gen12.percent(10);
|
|
1725
|
+
if (!chance) return;
|
|
1790
1726
|
} else {
|
|
1791
1727
|
if (showdown) {
|
|
1792
1728
|
if (!try checkHit(battle, player, move, log)) {
|
|
@@ -1805,7 +1741,10 @@ pub const Effects = struct {
|
|
|
1805
1741
|
|
|
1806
1742
|
if (foe.active.volatiles.Confusion) return;
|
|
1807
1743
|
foe.active.volatiles.Confusion = true;
|
|
1808
|
-
foe.active.volatiles.confusion =
|
|
1744
|
+
foe.active.volatiles.confusion = @intCast(u3, if (showdown)
|
|
1745
|
+
battle.rng.range(u8, 2, 6)
|
|
1746
|
+
else
|
|
1747
|
+
(battle.rng.next() & 3) + 2);
|
|
1809
1748
|
|
|
1810
1749
|
try log.start(battle.active(player.foe()), .Confusion);
|
|
1811
1750
|
}
|
|
@@ -1855,8 +1794,11 @@ pub const Effects = struct {
|
|
|
1855
1794
|
return Result.Error;
|
|
1856
1795
|
}
|
|
1857
1796
|
|
|
1858
|
-
volatiles.disabled_move = @intCast(u3,
|
|
1859
|
-
volatiles.disabled_duration =
|
|
1797
|
+
volatiles.disabled_move = @intCast(u3, randomMoveSlot(&battle.rng, &foe.active.moves, n));
|
|
1798
|
+
volatiles.disabled_duration = @intCast(u4, if (showdown)
|
|
1799
|
+
battle.rng.range(u8, 1, 9)
|
|
1800
|
+
else
|
|
1801
|
+
(battle.rng.next() & 7) + 1);
|
|
1860
1802
|
|
|
1861
1803
|
try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
|
|
1862
1804
|
return null;
|
|
@@ -1868,7 +1810,7 @@ pub const Effects = struct {
|
|
|
1868
1810
|
|
|
1869
1811
|
if (battle.last_damage == 0) {
|
|
1870
1812
|
assert(showdown);
|
|
1871
|
-
|
|
1813
|
+
return;
|
|
1872
1814
|
}
|
|
1873
1815
|
|
|
1874
1816
|
const drain = @max(battle.last_damage / 2, 1);
|
|
@@ -1895,7 +1837,15 @@ pub const Effects = struct {
|
|
|
1895
1837
|
var volatiles = &battle.foe(player).active.volatiles;
|
|
1896
1838
|
|
|
1897
1839
|
if (volatiles.Substitute) return;
|
|
1898
|
-
|
|
1840
|
+
|
|
1841
|
+
const chance = if (showdown)
|
|
1842
|
+
battle.rng.chance(u8, @as(u8, if (move.effect == .FlinchChance1) 26 else 77), 256)
|
|
1843
|
+
else
|
|
1844
|
+
battle.rng.next() < 1 + (if (move.effect == .FlinchChance1)
|
|
1845
|
+
Gen12.percent(10)
|
|
1846
|
+
else
|
|
1847
|
+
Gen12.percent(30));
|
|
1848
|
+
if (!chance) return;
|
|
1899
1849
|
|
|
1900
1850
|
volatiles.Flinch = true;
|
|
1901
1851
|
volatiles.Recharging = false;
|
|
@@ -1921,8 +1871,12 @@ pub const Effects = struct {
|
|
|
1921
1871
|
return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
|
|
1922
1872
|
}
|
|
1923
1873
|
|
|
1924
|
-
|
|
1925
|
-
|
|
1874
|
+
const chance = !foe.active.types.includes(move.type) and if (showdown)
|
|
1875
|
+
battle.rng.chance(u8, 26, 256)
|
|
1876
|
+
else
|
|
1877
|
+
battle.rng.next() < 1 + Gen12.percent(10);
|
|
1878
|
+
if (!chance) return;
|
|
1879
|
+
|
|
1926
1880
|
// Freeze Clause Mod
|
|
1927
1881
|
if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
1928
1882
|
|
|
@@ -2059,15 +2013,15 @@ pub const Effects = struct {
|
|
|
2059
2013
|
}
|
|
2060
2014
|
break :has_mimic false;
|
|
2061
2015
|
};
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2016
|
+
// If the foe is Invulnerable we still want to fall through to checkHit to be able to
|
|
2017
|
+
// trigger |-miss| instead of |-fail|
|
|
2018
|
+
if (!has_mimic and !foe.active.volatiles.Invulnerable) {
|
|
2065
2019
|
return try log.fail(battle.active(player.foe()), .None);
|
|
2066
2020
|
}
|
|
2067
2021
|
}
|
|
2068
2022
|
if (!try checkHit(battle, player, move, log)) return;
|
|
2069
2023
|
|
|
2070
|
-
const rslot =
|
|
2024
|
+
const rslot = randomMoveSlot(&battle.rng, &foe.active.moves, 0);
|
|
2071
2025
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
2072
2026
|
|
|
2073
2027
|
try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
|
|
@@ -2088,8 +2042,7 @@ pub const Effects = struct {
|
|
|
2088
2042
|
assert(!side.active.volatiles.MultiHit);
|
|
2089
2043
|
side.active.volatiles.MultiHit = true;
|
|
2090
2044
|
|
|
2091
|
-
side.active.volatiles.attacks =
|
|
2092
|
-
if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
|
|
2045
|
+
side.active.volatiles.attacks = if (move.effect == .MultiHit) distribution(battle) else 2;
|
|
2093
2046
|
}
|
|
2094
2047
|
|
|
2095
2048
|
fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2133,8 +2086,14 @@ pub const Effects = struct {
|
|
|
2133
2086
|
}
|
|
2134
2087
|
|
|
2135
2088
|
// Body Slam can't paralyze a Normal type Pokémon
|
|
2136
|
-
|
|
2137
|
-
|
|
2089
|
+
const chance = !foe.active.types.includes(move.type) and if (showdown)
|
|
2090
|
+
battle.rng.chance(u8, @as(u8, if (move.effect == .ParalyzeChance1) 26 else 77), 256)
|
|
2091
|
+
else
|
|
2092
|
+
battle.rng.next() < 1 + (if (move.effect == .ParalyzeChance1)
|
|
2093
|
+
Gen12.percent(10)
|
|
2094
|
+
else
|
|
2095
|
+
Gen12.percent(30));
|
|
2096
|
+
if (!chance) return;
|
|
2138
2097
|
|
|
2139
2098
|
foe_stored.status = Status.init(.PAR);
|
|
2140
2099
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
@@ -2177,7 +2136,16 @@ pub const Effects = struct {
|
|
|
2177
2136
|
|
|
2178
2137
|
if (move.effect == .Poison) {
|
|
2179
2138
|
if (!showdown and !try checkHit(battle, player, move, log)) return;
|
|
2180
|
-
} else
|
|
2139
|
+
} else {
|
|
2140
|
+
const chance = if (showdown)
|
|
2141
|
+
battle.rng.chance(u8, @as(u8, if (move.effect == .PoisonChance1) 52 else 103), 256)
|
|
2142
|
+
else
|
|
2143
|
+
battle.rng.next() < 1 + (if (move.effect == .PoisonChance1)
|
|
2144
|
+
Gen12.percent(20)
|
|
2145
|
+
else
|
|
2146
|
+
Gen12.percent(40));
|
|
2147
|
+
if (!chance) return;
|
|
2148
|
+
}
|
|
2181
2149
|
|
|
2182
2150
|
foe_stored.status = Status.init(.PSN);
|
|
2183
2151
|
if (toxic) {
|
|
@@ -2191,8 +2159,10 @@ pub const Effects = struct {
|
|
|
2191
2159
|
|
|
2192
2160
|
fn rage(battle: anytype, player: Player) !void {
|
|
2193
2161
|
var volatiles = &battle.side(player).active.volatiles;
|
|
2194
|
-
|
|
2162
|
+
if (volatiles.Rage) return;
|
|
2195
2163
|
volatiles.Rage = true;
|
|
2164
|
+
assert(!volatiles.Bide);
|
|
2165
|
+
volatiles.state = 0;
|
|
2196
2166
|
}
|
|
2197
2167
|
|
|
2198
2168
|
fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
|
|
@@ -2244,7 +2214,16 @@ pub const Effects = struct {
|
|
|
2244
2214
|
}
|
|
2245
2215
|
foe.active.volatiles.Recharging = false;
|
|
2246
2216
|
|
|
2247
|
-
|
|
2217
|
+
const duration = @intCast(u3, if (showdown)
|
|
2218
|
+
battle.rng.range(u8, 1, 8)
|
|
2219
|
+
else loop: {
|
|
2220
|
+
while (true) {
|
|
2221
|
+
const r = battle.rng.next() & 7;
|
|
2222
|
+
if (r != 0) break :loop r;
|
|
2223
|
+
}
|
|
2224
|
+
});
|
|
2225
|
+
|
|
2226
|
+
foe_stored.status = Status.slp(duration);
|
|
2248
2227
|
try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
|
|
2249
2228
|
}
|
|
2250
2229
|
|
|
@@ -2297,7 +2276,11 @@ pub const Effects = struct {
|
|
|
2297
2276
|
assert(!volatiles.Bide);
|
|
2298
2277
|
|
|
2299
2278
|
volatiles.Thrashing = true;
|
|
2300
|
-
volatiles.
|
|
2279
|
+
volatiles.state = 0;
|
|
2280
|
+
volatiles.attacks = @intCast(u3, if (showdown)
|
|
2281
|
+
battle.rng.range(u8, 2, 4)
|
|
2282
|
+
else
|
|
2283
|
+
(battle.rng.next() & 1) + 2);
|
|
2301
2284
|
}
|
|
2302
2285
|
|
|
2303
2286
|
fn transform(battle: anytype, player: Player, log: anytype) !void {
|
|
@@ -2329,7 +2312,7 @@ pub const Effects = struct {
|
|
|
2329
2312
|
try log.transform(battle.active(player), foe_ident);
|
|
2330
2313
|
}
|
|
2331
2314
|
|
|
2332
|
-
fn binding(battle: anytype, player: Player
|
|
2315
|
+
fn binding(battle: anytype, player: Player) void {
|
|
2333
2316
|
var side = battle.side(player);
|
|
2334
2317
|
var foe = battle.foe(player);
|
|
2335
2318
|
|
|
@@ -2337,11 +2320,9 @@ pub const Effects = struct {
|
|
|
2337
2320
|
side.active.volatiles.Binding = true;
|
|
2338
2321
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2339
2322
|
// (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);
|
|
2323
|
+
if (!showdown) foe.active.volatiles.Recharging = false;
|
|
2343
2324
|
|
|
2344
|
-
side.active.volatiles.attacks =
|
|
2325
|
+
side.active.volatiles.attacks = distribution(battle) - 1;
|
|
2345
2326
|
}
|
|
2346
2327
|
|
|
2347
2328
|
fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2368,7 +2349,7 @@ pub const Effects = struct {
|
|
|
2368
2349
|
return try log.fail(ident, .None);
|
|
2369
2350
|
}
|
|
2370
2351
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2371
|
-
const stat = unmodifiedStats(battle,
|
|
2352
|
+
const stat = unmodifiedStats(battle, player).atk;
|
|
2372
2353
|
stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2373
2354
|
try log.boost(ident, reason, n);
|
|
2374
2355
|
// Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
|
|
@@ -2388,7 +2369,7 @@ pub const Effects = struct {
|
|
|
2388
2369
|
return try log.fail(ident, .None);
|
|
2389
2370
|
}
|
|
2390
2371
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2391
|
-
const stat = unmodifiedStats(battle,
|
|
2372
|
+
const stat = unmodifiedStats(battle, player).def;
|
|
2392
2373
|
stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2393
2374
|
try log.boost(ident, .Defense, n);
|
|
2394
2375
|
},
|
|
@@ -2405,7 +2386,7 @@ pub const Effects = struct {
|
|
|
2405
2386
|
return try log.fail(ident, .None);
|
|
2406
2387
|
}
|
|
2407
2388
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2408
|
-
const stat = unmodifiedStats(battle,
|
|
2389
|
+
const stat = unmodifiedStats(battle, player).spe;
|
|
2409
2390
|
stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2410
2391
|
try log.boost(ident, .Speed, 2);
|
|
2411
2392
|
},
|
|
@@ -2425,7 +2406,7 @@ pub const Effects = struct {
|
|
|
2425
2406
|
return try log.fail(ident, .None);
|
|
2426
2407
|
}
|
|
2427
2408
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2428
|
-
const stat = unmodifiedStats(battle,
|
|
2409
|
+
const stat = unmodifiedStats(battle, player).spc;
|
|
2429
2410
|
stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2430
2411
|
try log.boost(ident, .SpecialAttack, n);
|
|
2431
2412
|
try log.boost(ident, .SpecialDefense, n);
|
|
@@ -2452,7 +2433,11 @@ pub const Effects = struct {
|
|
|
2452
2433
|
}
|
|
2453
2434
|
|
|
2454
2435
|
if (move.effect.isStatDownChance()) {
|
|
2455
|
-
if (
|
|
2436
|
+
const chance = if (showdown)
|
|
2437
|
+
battle.rng.chance(u8, 85, 256)
|
|
2438
|
+
else
|
|
2439
|
+
battle.rng.next() < Gen12.percent(33) + 1;
|
|
2440
|
+
if (!chance or foe.active.volatiles.Invulnerable) return;
|
|
2456
2441
|
} else if (!showdown and !try checkHit(battle, player, move, log)) {
|
|
2457
2442
|
return; // checkHit already checks for Invulnerable
|
|
2458
2443
|
}
|
|
@@ -2474,7 +2459,7 @@ pub const Effects = struct {
|
|
|
2474
2459
|
return try log.fail(foe_ident, .None);
|
|
2475
2460
|
}
|
|
2476
2461
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2477
|
-
const stat = unmodifiedStats(battle, foe).atk;
|
|
2462
|
+
const stat = unmodifiedStats(battle, player.foe()).atk;
|
|
2478
2463
|
stats.atk = @max(1, stat * mod[0] / mod[1]);
|
|
2479
2464
|
try log.boost(foe_ident, .Attack, -1);
|
|
2480
2465
|
},
|
|
@@ -2492,7 +2477,7 @@ pub const Effects = struct {
|
|
|
2492
2477
|
return try log.fail(foe_ident, .None);
|
|
2493
2478
|
}
|
|
2494
2479
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2495
|
-
const stat = unmodifiedStats(battle, foe).def;
|
|
2480
|
+
const stat = unmodifiedStats(battle, player.foe()).def;
|
|
2496
2481
|
stats.def = @max(1, stat * mod[0] / mod[1]);
|
|
2497
2482
|
try log.boost(foe_ident, .Defense, -@as(i8, n));
|
|
2498
2483
|
},
|
|
@@ -2509,7 +2494,7 @@ pub const Effects = struct {
|
|
|
2509
2494
|
return try log.fail(foe_ident, .None);
|
|
2510
2495
|
}
|
|
2511
2496
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2512
|
-
const stat = unmodifiedStats(battle, foe).spe;
|
|
2497
|
+
const stat = unmodifiedStats(battle, player.foe()).spe;
|
|
2513
2498
|
stats.spe = @max(1, stat * mod[0] / mod[1]);
|
|
2514
2499
|
try log.boost(foe_ident, .Speed, -1);
|
|
2515
2500
|
assert(boosts.spe >= -6);
|
|
@@ -2529,7 +2514,7 @@ pub const Effects = struct {
|
|
|
2529
2514
|
return try log.fail(foe_ident, .None);
|
|
2530
2515
|
}
|
|
2531
2516
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2532
|
-
const stat = unmodifiedStats(battle, foe).spc;
|
|
2517
|
+
const stat = unmodifiedStats(battle, player.foe()).spc;
|
|
2533
2518
|
stats.spc = @max(1, stat * mod[0] / mod[1]);
|
|
2534
2519
|
try log.boost(foe_ident, .SpecialAttack, -1);
|
|
2535
2520
|
try log.boost(foe_ident, .SpecialDefense, -1);
|
|
@@ -2548,7 +2533,8 @@ pub const Effects = struct {
|
|
|
2548
2533
|
}
|
|
2549
2534
|
};
|
|
2550
2535
|
|
|
2551
|
-
fn unmodifiedStats(battle: anytype,
|
|
2536
|
+
fn unmodifiedStats(battle: anytype, who: Player) *Stats(u16) {
|
|
2537
|
+
const side = battle.side(who);
|
|
2552
2538
|
if (!side.active.volatiles.Transform) return &side.stored().stats;
|
|
2553
2539
|
const id = ID.from(side.active.volatiles.transform);
|
|
2554
2540
|
return &battle.side(id.player).pokemon[id.id - 1].stats;
|
|
@@ -2614,140 +2600,80 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
|
|
|
2614
2600
|
}
|
|
2615
2601
|
}
|
|
2616
2602
|
|
|
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;
|
|
2603
|
+
fn disabled(side: *Side, ident: ID, log: anytype) !bool {
|
|
2604
|
+
if (side.active.volatiles.disabled_move != 0) {
|
|
2605
|
+
// A Pokémon that transforms after being disabled may end up with less move slots
|
|
2606
|
+
const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
|
|
2607
|
+
// side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
|
|
2608
|
+
const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
|
|
2609
|
+
m
|
|
2610
|
+
else
|
|
2611
|
+
side.last_selected_move;
|
|
2612
|
+
if (m != .None and m == last) {
|
|
2613
|
+
side.active.volatiles.Charging = false;
|
|
2614
|
+
try log.disabled(ident, last);
|
|
2615
|
+
return true;
|
|
2633
2616
|
}
|
|
2634
2617
|
}
|
|
2618
|
+
return false;
|
|
2619
|
+
}
|
|
2635
2620
|
|
|
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);
|
|
2621
|
+
inline fn buildRage(battle: anytype, who: Player, log: anytype) !void {
|
|
2622
|
+
const side = battle.side(who);
|
|
2623
|
+
if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
|
|
2624
|
+
try Effects.boost(battle, who, Move.get(.Rage), log);
|
|
2654
2625
|
}
|
|
2626
|
+
}
|
|
2655
2627
|
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2628
|
+
fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
|
|
2629
|
+
var volatiles = &active.volatiles;
|
|
2630
|
+
assert(volatiles.Thrashing);
|
|
2631
|
+
if (volatiles.attacks > 0) return false;
|
|
2660
2632
|
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2633
|
+
volatiles.Thrashing = false;
|
|
2634
|
+
volatiles.Confusion = true;
|
|
2635
|
+
volatiles.confusion = @intCast(u3, if (showdown)
|
|
2636
|
+
battle.rng.range(u8, 2, 6)
|
|
2637
|
+
else
|
|
2638
|
+
(battle.rng.next() & 3) + 2);
|
|
2665
2639
|
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
return battle.rng.next() < 1 + (if (low) Gen12.percent(20) else Gen12.percent(40));
|
|
2669
|
-
}
|
|
2640
|
+
return true;
|
|
2641
|
+
}
|
|
2670
2642
|
|
|
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
|
-
}
|
|
2643
|
+
const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
|
|
2678
2644
|
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2645
|
+
fn distribution(battle: anytype) u3 {
|
|
2646
|
+
if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
|
|
2647
|
+
const r = (battle.rng.next() & 3);
|
|
2648
|
+
return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
|
|
2649
|
+
}
|
|
2683
2650
|
|
|
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
|
-
}
|
|
2651
|
+
fn randomMoveSlot(rand: anytype, moves: []MoveSlot, check_pp: u4) u4 {
|
|
2652
|
+
if (showdown) {
|
|
2653
|
+
if (check_pp == 0) {
|
|
2654
|
+
var i: usize = moves.len;
|
|
2655
|
+
while (i > 0) {
|
|
2656
|
+
i -= 1;
|
|
2657
|
+
if (moves[i].id != .None) return rand.range(u4, 0, @intCast(u4, i + 1)) + 1;
|
|
2658
|
+
}
|
|
2659
|
+
} else {
|
|
2660
|
+
var r = rand.range(u4, 0, @intCast(u4, check_pp)) + 1;
|
|
2661
|
+
var i: usize = 0;
|
|
2662
|
+
while (i < moves.len and r > 0) : (i += 1) {
|
|
2663
|
+
if (moves[i].pp > 0) {
|
|
2664
|
+
r -= 1;
|
|
2665
|
+
if (r == 0) break;
|
|
2702
2666
|
}
|
|
2703
|
-
return @intCast(u4, i + 1);
|
|
2704
2667
|
}
|
|
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;
|
|
2668
|
+
return @intCast(u4, i + 1);
|
|
2710
2669
|
}
|
|
2711
2670
|
}
|
|
2712
2671
|
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2672
|
+
while (true) {
|
|
2673
|
+
const r = @intCast(u4, rand.next() & 3);
|
|
2674
|
+
if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
|
|
2716
2675
|
}
|
|
2717
|
-
|
|
2718
|
-
inline fn metronome(battle: anytype) Move {
|
|
2719
|
-
if (showdown) {
|
|
2720
|
-
const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
|
|
2721
|
-
const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
|
|
2722
|
-
return @intToEnum(Move, r + mod);
|
|
2723
|
-
}
|
|
2724
|
-
while (true) {
|
|
2725
|
-
const r = battle.rng.next();
|
|
2726
|
-
if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
|
|
2727
|
-
if (r >= @enumToInt(Move.Struggle)) continue;
|
|
2728
|
-
return @intToEnum(Move, r);
|
|
2729
|
-
}
|
|
2730
|
-
}
|
|
2731
|
-
|
|
2732
|
-
inline fn psywave(battle: anytype, max: u8) u8 {
|
|
2733
|
-
if (showdown) return battle.rng.range(u8, 0, max);
|
|
2734
|
-
while (true) {
|
|
2735
|
-
const r = battle.rng.next();
|
|
2736
|
-
if (r < max) return r;
|
|
2737
|
-
}
|
|
2738
|
-
}
|
|
2739
|
-
|
|
2740
|
-
inline fn distribution(battle: anytype) u3 {
|
|
2741
|
-
if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
|
|
2742
|
-
const r = (battle.rng.next() & 3);
|
|
2743
|
-
return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
|
|
2744
|
-
}
|
|
2745
|
-
|
|
2746
|
-
inline fn unboost(battle: anytype) bool {
|
|
2747
|
-
if (showdown) return battle.rng.chance(u8, 85, 256);
|
|
2748
|
-
return battle.rng.next() < Gen12.percent(33) + 1;
|
|
2749
|
-
}
|
|
2750
|
-
};
|
|
2676
|
+
}
|
|
2751
2677
|
|
|
2752
2678
|
test "RNG agreement" {
|
|
2753
2679
|
if (!showdown) return;
|