@pkmn/engine 0.1.0-dev.83de29ee → 0.1.0-dev.84cc1a02
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 -35
- 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.zig +31 -23
- package/package.json +29 -40
- package/src/bin/install-pkmn-engine +28 -47
- 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 +63 -93
- package/src/lib/gen1/data.zig +17 -19
- package/src/lib/gen1/helpers.zig +13 -11
- package/src/lib/gen1/mechanics.zig +301 -378
- 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 -401
- 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;
|
|
@@ -320,7 +301,11 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
|
|
|
320
301
|
// Pokémon Showdown's beforeTurnCallback shenanigans
|
|
321
302
|
if (showdown and m1 == .Counter and m2 == .Counter) battle.rng.advance(1);
|
|
322
303
|
|
|
323
|
-
const p1 =
|
|
304
|
+
const p1 = if (showdown)
|
|
305
|
+
battle.rng.range(u8, 0, 2) == 0
|
|
306
|
+
else
|
|
307
|
+
battle.rng.next() < Gen12.percent(50) + 1;
|
|
308
|
+
|
|
324
309
|
if (!showdown) return if (p1) .P1 else .P2;
|
|
325
310
|
|
|
326
311
|
// Pokémon Showdown's "lockedmove" volatile's onBeforeTurn uses BattleQueue#changeAction,
|
|
@@ -330,7 +315,7 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
|
|
|
330
315
|
const t1 = battle.side(.P1).active.volatiles.Thrashing;
|
|
331
316
|
const t2 = battle.side(.P2).active.volatiles.Thrashing;
|
|
332
317
|
// If *both* sides are thrashing it really should be another speed tie, but we've patched
|
|
333
|
-
// 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
|
|
334
319
|
// won the original coin flip
|
|
335
320
|
if (t1 and t2) return .P1;
|
|
336
321
|
return if (p1) if (t1 and !t2) .P2 else .P1 else if (t2 and !t1) .P1 else .P2;
|
|
@@ -343,13 +328,9 @@ fn doTurn(
|
|
|
343
328
|
battle: anytype,
|
|
344
329
|
player: Player,
|
|
345
330
|
player_choice: Choice,
|
|
346
|
-
player_rewrap: bool,
|
|
347
|
-
player_skip: bool,
|
|
348
331
|
player_from: ?Move,
|
|
349
332
|
foe_player: Player,
|
|
350
333
|
foe_choice: Choice,
|
|
351
|
-
foe_rewrap: bool,
|
|
352
|
-
foe_skip: bool,
|
|
353
334
|
foe_from: ?Move,
|
|
354
335
|
log: anytype,
|
|
355
336
|
) !?Result {
|
|
@@ -357,16 +338,7 @@ fn doTurn(
|
|
|
357
338
|
|
|
358
339
|
var residual = true;
|
|
359
340
|
var replace = battle.side(player).stored().hp == 0;
|
|
360
|
-
if (try executeMove(
|
|
361
|
-
battle,
|
|
362
|
-
player,
|
|
363
|
-
player_choice,
|
|
364
|
-
player_rewrap,
|
|
365
|
-
player_skip,
|
|
366
|
-
player_from,
|
|
367
|
-
&residual,
|
|
368
|
-
log,
|
|
369
|
-
)) |r| return r;
|
|
341
|
+
if (try executeMove(battle, player, player_choice, player_from, &residual, log)) |r| return r;
|
|
370
342
|
if (!replace) {
|
|
371
343
|
if (player_choice.type != .Switch) {
|
|
372
344
|
if (try checkFaint(battle, foe_player, log)) |r| return r;
|
|
@@ -377,16 +349,7 @@ fn doTurn(
|
|
|
377
349
|
|
|
378
350
|
residual = true;
|
|
379
351
|
replace = battle.side(foe_player).stored().hp == 0;
|
|
380
|
-
if (try executeMove(
|
|
381
|
-
battle,
|
|
382
|
-
foe_player,
|
|
383
|
-
foe_choice,
|
|
384
|
-
foe_rewrap,
|
|
385
|
-
foe_skip,
|
|
386
|
-
foe_from,
|
|
387
|
-
&residual,
|
|
388
|
-
log,
|
|
389
|
-
)) |r| return r;
|
|
352
|
+
if (try executeMove(battle, foe_player, foe_choice, foe_from, &residual, log)) |r| return r;
|
|
390
353
|
if (!replace) {
|
|
391
354
|
if (foe_choice.type != .Switch) {
|
|
392
355
|
if (try checkFaint(battle, player, log)) |r| return r;
|
|
@@ -409,8 +372,6 @@ fn executeMove(
|
|
|
409
372
|
battle: anytype,
|
|
410
373
|
player: Player,
|
|
411
374
|
choice: Choice,
|
|
412
|
-
rewrap: bool,
|
|
413
|
-
skip: bool,
|
|
414
375
|
from: ?Move,
|
|
415
376
|
residual: *bool,
|
|
416
377
|
log: anytype,
|
|
@@ -422,10 +383,9 @@ fn executeMove(
|
|
|
422
383
|
return null;
|
|
423
384
|
}
|
|
424
385
|
|
|
425
|
-
// This is the correct place to check for SKIP_TURN and abort early, however since Pokémon
|
|
426
|
-
// Showdown overwrites the SKIP_TURN sentinel with its botched move select we need to add an
|
|
427
|
-
// additional skip boolean to accomplish the same thing in the Binding check of BeforeMove
|
|
428
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
|
|
429
389
|
assert(!showdown);
|
|
430
390
|
if (battle.foe(player).active.volatiles.Binding) {
|
|
431
391
|
try log.cant(battle.active(player), .Bound);
|
|
@@ -444,9 +404,9 @@ fn executeMove(
|
|
|
444
404
|
// choice.data == 0 only happens with Struggle on Pokémon Showdown
|
|
445
405
|
assert(!showdown);
|
|
446
406
|
mslot = @intCast(u4, if (player == .P1)
|
|
447
|
-
battle.
|
|
407
|
+
battle.last_selected_indexes.p1
|
|
448
408
|
else
|
|
449
|
-
battle.
|
|
409
|
+
battle.last_selected_indexes.p2);
|
|
450
410
|
const stored = side.stored();
|
|
451
411
|
// GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
|
|
452
412
|
auto = side.last_selected_move == .HyperBeam or side.active.volatiles.Bide or
|
|
@@ -468,14 +428,14 @@ fn executeMove(
|
|
|
468
428
|
// Incorrect mslot with Pokémon Showdown choice semantics so we need to recover from index
|
|
469
429
|
assert(mslot == 1);
|
|
470
430
|
mslot = @intCast(u4, if (player == .P1)
|
|
471
|
-
battle.
|
|
431
|
+
battle.last_selected_indexes.p1
|
|
472
432
|
else
|
|
473
|
-
battle.
|
|
433
|
+
battle.last_selected_indexes.p2);
|
|
474
434
|
}
|
|
475
435
|
|
|
476
436
|
var skip_can = false;
|
|
477
437
|
var skip_pp = false;
|
|
478
|
-
switch (try beforeMove(battle, player,
|
|
438
|
+
switch (try beforeMove(battle, player, from, residual, log)) {
|
|
479
439
|
.done => return null,
|
|
480
440
|
.skip_can => skip_can = true,
|
|
481
441
|
.skip_pp => skip_pp = true,
|
|
@@ -487,15 +447,14 @@ fn executeMove(
|
|
|
487
447
|
return null;
|
|
488
448
|
}
|
|
489
449
|
|
|
490
|
-
return doMove(battle, player, mslot,
|
|
450
|
+
return doMove(battle, player, mslot, auto, residual, log);
|
|
491
451
|
}
|
|
492
452
|
|
|
493
|
-
const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
|
|
453
|
+
const BeforeMove = union(enum) { done, skip_can, skip_pp, ok, err };
|
|
494
454
|
|
|
495
455
|
fn beforeMove(
|
|
496
456
|
battle: anytype,
|
|
497
457
|
player: Player,
|
|
498
|
-
skip: bool,
|
|
499
458
|
from: ?Move,
|
|
500
459
|
residual: *bool,
|
|
501
460
|
log: anytype,
|
|
@@ -527,15 +486,13 @@ fn beforeMove(
|
|
|
527
486
|
return .done;
|
|
528
487
|
}
|
|
529
488
|
|
|
530
|
-
if (
|
|
489
|
+
if (foe.active.volatiles.Binding) {
|
|
531
490
|
try log.cant(ident, .Bound);
|
|
532
491
|
return .done;
|
|
533
492
|
}
|
|
534
493
|
|
|
535
|
-
if (volatiles.Flinch) {
|
|
536
|
-
|
|
537
|
-
// Pokémon can sometimes flinch multiple times from the same original hit
|
|
538
|
-
if (!showdown) volatiles.Flinch = false;
|
|
494
|
+
if (!showdown and volatiles.Flinch) {
|
|
495
|
+
volatiles.Flinch = false;
|
|
539
496
|
try log.cant(ident, .Flinch);
|
|
540
497
|
return .done;
|
|
541
498
|
}
|
|
@@ -556,6 +513,13 @@ fn beforeMove(
|
|
|
556
513
|
// Pokémon Showdown's disable condition has a single onBeforeMove handler
|
|
557
514
|
if (showdown and try disabled(side, ident, log)) return .done;
|
|
558
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
|
+
|
|
559
523
|
// This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
|
|
560
524
|
// before the side had a selected a move - we simply need to assume this leads to a desync
|
|
561
525
|
if (side.last_selected_move == .None) {
|
|
@@ -573,7 +537,12 @@ fn beforeMove(
|
|
|
573
537
|
} else {
|
|
574
538
|
try log.activate(ident, .Confusion);
|
|
575
539
|
|
|
576
|
-
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) {
|
|
577
546
|
assert(!volatiles.MultiHit);
|
|
578
547
|
if (!volatiles.Rage) volatiles.state = 0;
|
|
579
548
|
volatiles.Bide = false;
|
|
@@ -599,12 +568,8 @@ fn beforeMove(
|
|
|
599
568
|
player
|
|
600
569
|
else
|
|
601
570
|
player.foe();
|
|
602
|
-
|
|
603
|
-
const uncapped = battle.last_damage;
|
|
604
571
|
// Skipping adjustDamage / randomizeDamage / checkHit
|
|
605
572
|
_ = try applyDamage(battle, player, target, .Confusion, log);
|
|
606
|
-
// Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
|
|
607
|
-
if (showdown) battle.last_damage = uncapped;
|
|
608
573
|
|
|
609
574
|
return .done;
|
|
610
575
|
}
|
|
@@ -613,15 +578,22 @@ fn beforeMove(
|
|
|
613
578
|
|
|
614
579
|
if (!showdown and try disabled(side, ident, log)) return .done;
|
|
615
580
|
|
|
616
|
-
if (Status.is(stored.status, .PAR)
|
|
617
|
-
if (
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
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
|
+
}
|
|
625
597
|
}
|
|
626
598
|
|
|
627
599
|
if (volatiles.Bide) {
|
|
@@ -648,9 +620,8 @@ fn beforeMove(
|
|
|
648
620
|
return .done;
|
|
649
621
|
}
|
|
650
622
|
|
|
651
|
-
const sub = showdown and foe.active.volatiles.Substitute;
|
|
652
623
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
653
|
-
if (foe.stored().hp > 0
|
|
624
|
+
if (foe.stored().hp > 0) try buildRage(battle, player.foe(), log);
|
|
654
625
|
|
|
655
626
|
// For reasons passing understanding, Pokémon Showdown still inflicts residual damage to
|
|
656
627
|
// Bide's user even if the above damage has caused the foe to faint. It's simpler to always
|
|
@@ -671,13 +642,8 @@ fn beforeMove(
|
|
|
671
642
|
try log.start(battle.active(player), .ConfusionSilent);
|
|
672
643
|
}
|
|
673
644
|
try log.move(ident, side.last_selected_move, battle.active(player.foe()), from);
|
|
674
|
-
if (showdown) {
|
|
675
|
-
|
|
676
|
-
// used in useMove and doesn't have the notion of skipping canMove semantics
|
|
677
|
-
side.last_used_move = side.last_selected_move;
|
|
678
|
-
if (handleThrashing(battle, active)) {
|
|
679
|
-
try log.start(battle.active(player), .ConfusionSilent);
|
|
680
|
-
}
|
|
645
|
+
if (showdown and handleThrashing(battle, active)) {
|
|
646
|
+
try log.start(battle.active(player), .ConfusionSilent);
|
|
681
647
|
}
|
|
682
648
|
return .skip_can;
|
|
683
649
|
}
|
|
@@ -692,11 +658,7 @@ fn beforeMove(
|
|
|
692
658
|
side.last_selected_move,
|
|
693
659
|
);
|
|
694
660
|
if (showdown or battle.last_damage != 0) {
|
|
695
|
-
const sub = showdown and foe.active.volatiles.Substitute;
|
|
696
661
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
697
|
-
if (battle.foe(player).stored().hp > 0 and !sub) {
|
|
698
|
-
try buildRage(battle, player.foe(), log);
|
|
699
|
-
}
|
|
700
662
|
}
|
|
701
663
|
return .done;
|
|
702
664
|
}
|
|
@@ -745,19 +707,6 @@ fn canMove(
|
|
|
745
707
|
const f = if (from != null and from.? == .None) null else from;
|
|
746
708
|
try log.move(player_ident, side.last_selected_move, battle.active(target), f);
|
|
747
709
|
|
|
748
|
-
// The Counter desync is caused by the cartridge not calling GetCurrentMove until now, meaning
|
|
749
|
-
// in cases where an early return happens the data for a players last selected move does not get
|
|
750
|
-
// reloaded and HandleCounterMove actually bases its success/failure off of stale information.
|
|
751
|
-
// This boolean state we track here doesn't exist on the cartridge because it instead manifests
|
|
752
|
-
// as the desync results from actually having two separate battle states that subtly disagree.
|
|
753
|
-
const counterable = side.last_selected_move != .Counter and move.bp > 0 and
|
|
754
|
-
(move.type == .Normal or move.type == .Fighting);
|
|
755
|
-
if (player == .P1) {
|
|
756
|
-
battle.last_moves.p1_counterable = @boolToInt(counterable);
|
|
757
|
-
} else {
|
|
758
|
-
battle.last_moves.p2_counterable = @boolToInt(counterable);
|
|
759
|
-
}
|
|
760
|
-
|
|
761
710
|
if (move.effect.onBegin()) {
|
|
762
711
|
try onBegin(battle, player, move, mslot, residual, log);
|
|
763
712
|
return false;
|
|
@@ -767,7 +716,7 @@ fn canMove(
|
|
|
767
716
|
Effects.thrashing(battle, player);
|
|
768
717
|
} else if (!showdown and move.effect == .Binding) {
|
|
769
718
|
// Pokémon Showdown handles this after hit/miss checks and damage calculation
|
|
770
|
-
Effects.binding(battle, player
|
|
719
|
+
Effects.binding(battle, player);
|
|
771
720
|
}
|
|
772
721
|
|
|
773
722
|
return true;
|
|
@@ -808,7 +757,6 @@ fn doMove(
|
|
|
808
757
|
battle: anytype,
|
|
809
758
|
player: Player,
|
|
810
759
|
mslot: u4,
|
|
811
|
-
rewrap: bool,
|
|
812
760
|
auto: bool,
|
|
813
761
|
residual: *bool,
|
|
814
762
|
log: anytype,
|
|
@@ -839,15 +787,12 @@ fn doMove(
|
|
|
839
787
|
(@enumToInt(move.type.effectiveness(foe.active.types.type1)) == 0 or
|
|
840
788
|
@enumToInt(move.type.effectiveness(foe.active.types.type2)) == 0);
|
|
841
789
|
if (immune and move.effect != .Binding) break :miss true;
|
|
842
|
-
if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe)
|
|
843
|
-
battle.last_damage = 0;
|
|
844
|
-
break :miss true;
|
|
845
|
-
}
|
|
790
|
+
if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) break :miss true;
|
|
846
791
|
break :miss move.target != .Self and !moveHit(battle, player, move, &immune, &mist);
|
|
847
792
|
};
|
|
848
793
|
assert(!immune or miss or (showdown and move.effect == .Binding));
|
|
849
794
|
|
|
850
|
-
var late = showdown
|
|
795
|
+
var late = showdown;
|
|
851
796
|
const skip = status or immune;
|
|
852
797
|
if ((!showdown or (!skip or counter)) and !miss) blk: {
|
|
853
798
|
if (showdown and move.effect.isMulti()) {
|
|
@@ -900,9 +845,9 @@ fn doMove(
|
|
|
900
845
|
|
|
901
846
|
if (!showdown or !miss) {
|
|
902
847
|
if (move.effect == .MirrorMove) {
|
|
903
|
-
return mirrorMove(battle, player, mslot,
|
|
848
|
+
return mirrorMove(battle, player, mslot, auto, residual, log);
|
|
904
849
|
} else if (move.effect == .Metronome) {
|
|
905
|
-
return metronome(battle, player, mslot,
|
|
850
|
+
return metronome(battle, player, mslot, auto, residual, log);
|
|
906
851
|
} else if (move.effect.onEnd()) {
|
|
907
852
|
try onEnd(battle, player, move, log);
|
|
908
853
|
return null;
|
|
@@ -917,7 +862,7 @@ fn doMove(
|
|
|
917
862
|
move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
|
|
918
863
|
if (ohko) {
|
|
919
864
|
try log.immune(foe_ident, .OHKO);
|
|
920
|
-
} else if (immune and !invulnerable
|
|
865
|
+
} else if (immune and !invulnerable) {
|
|
921
866
|
try log.immune(foe_ident, .None);
|
|
922
867
|
} else if (mist) {
|
|
923
868
|
if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
|
|
@@ -949,9 +894,6 @@ fn doMove(
|
|
|
949
894
|
hits = side.active.volatiles.attacks;
|
|
950
895
|
}
|
|
951
896
|
|
|
952
|
-
// Pokémon Showdown only builds Rage for Disable/Explosion (hit/miss) when attacking into a sub
|
|
953
|
-
const sub = showdown and foe.active.volatiles.Substitute;
|
|
954
|
-
|
|
955
897
|
var nullified = false;
|
|
956
898
|
var hit: u4 = 0;
|
|
957
899
|
while (hit < hits) {
|
|
@@ -967,7 +909,7 @@ fn doMove(
|
|
|
967
909
|
if (hit == 0 and ohko) try log.ohko();
|
|
968
910
|
hit += 1;
|
|
969
911
|
if (foe.stored().hp == 0) break;
|
|
970
|
-
if (!late
|
|
912
|
+
if (!late) try buildRage(battle, player.foe(), log);
|
|
971
913
|
// If the substitute breaks during a multi-hit attack, the attack ends
|
|
972
914
|
if (nullified) break;
|
|
973
915
|
}
|
|
@@ -983,17 +925,11 @@ fn doMove(
|
|
|
983
925
|
} else if (showdown) {
|
|
984
926
|
// This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
|
|
985
927
|
if (move.effect == .Binding) {
|
|
986
|
-
Effects.binding(battle, player
|
|
928
|
+
Effects.binding(battle, player);
|
|
987
929
|
if (immune) {
|
|
988
930
|
battle.last_damage = 0;
|
|
989
|
-
assert(foe.stored().hp > 0);
|
|
990
931
|
// Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
|
|
991
|
-
|
|
992
|
-
try log.activate(battle.active(player.foe()), .Substitute);
|
|
993
|
-
} else {
|
|
994
|
-
try log.damage(battle.active(player.foe()), foe.stored(), .None);
|
|
995
|
-
try buildRage(battle, player.foe(), log);
|
|
996
|
-
}
|
|
932
|
+
try log.damage(battle.active(player.foe()), foe.stored(), .None);
|
|
997
933
|
return null;
|
|
998
934
|
}
|
|
999
935
|
}
|
|
@@ -1010,8 +946,8 @@ fn doMove(
|
|
|
1010
946
|
|
|
1011
947
|
if (foe.stored().hp == 0) return null;
|
|
1012
948
|
|
|
1013
|
-
// Pokémon Showdown builds Rage at the wrong time for non-MultiHit
|
|
1014
|
-
if (late and
|
|
949
|
+
// Pokémon Showdown builds Rage at the wrong time for non-MultiHit moves
|
|
950
|
+
if (late and move.effect != .Disable) try buildRage(battle, player.foe(), log);
|
|
1015
951
|
|
|
1016
952
|
if (!move.effect.isSpecial()) {
|
|
1017
953
|
// On the cartridge Rage is not considered to be "special" and thus gets executed for a
|
|
@@ -1044,7 +980,8 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
|
|
|
1044
980
|
chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
|
|
1045
981
|
chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
|
|
1046
982
|
|
|
1047
|
-
return
|
|
983
|
+
if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
|
|
984
|
+
return std.math.rotl(u8, battle.rng.next(), 3) < chance;
|
|
1048
985
|
}
|
|
1049
986
|
|
|
1050
987
|
fn calcDamage(
|
|
@@ -1148,36 +1085,50 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
|
1148
1085
|
|
|
1149
1086
|
fn randomizeDamage(battle: anytype) void {
|
|
1150
1087
|
if (battle.last_damage <= 1) return;
|
|
1151
|
-
|
|
1088
|
+
|
|
1089
|
+
const random = if (showdown)
|
|
1090
|
+
battle.rng.range(u8, 217, 256)
|
|
1091
|
+
else loop: {
|
|
1092
|
+
while (true) {
|
|
1093
|
+
const r = std.math.rotr(u8, battle.rng.next(), 1);
|
|
1094
|
+
if (r >= 217) break :loop r;
|
|
1095
|
+
}
|
|
1096
|
+
};
|
|
1097
|
+
|
|
1152
1098
|
battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
|
|
1153
1099
|
}
|
|
1154
1100
|
|
|
1155
1101
|
fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
|
|
1156
1102
|
const side = battle.side(player);
|
|
1157
|
-
const foe = battle.foe(player);
|
|
1158
1103
|
|
|
1159
1104
|
if (!try checkHit(battle, player, move, log)) return null;
|
|
1160
1105
|
|
|
1161
1106
|
battle.last_damage = switch (side.last_selected_move) {
|
|
1162
|
-
.SuperFang => @max(foe.stored().hp / 2, 1),
|
|
1107
|
+
.SuperFang => @max(battle.foe(player).stored().hp / 2, 1),
|
|
1163
1108
|
.SeismicToss, .NightShade => side.stored().level,
|
|
1164
1109
|
.SonicBoom => 20,
|
|
1165
1110
|
.DragonRage => 40,
|
|
1166
1111
|
// GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
|
|
1167
1112
|
.Psywave => power: {
|
|
1168
1113
|
const max = @intCast(u8, @as(u16, side.stored().level) * 3 / 2);
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1114
|
+
if (showdown) {
|
|
1115
|
+
break :power battle.rng.range(u8, 0, max);
|
|
1116
|
+
} else {
|
|
1117
|
+
// GLITCH: Psywave infinite glitch loop
|
|
1118
|
+
if (max <= 1) return Result.Error;
|
|
1119
|
+
while (true) {
|
|
1120
|
+
const r = battle.rng.next();
|
|
1121
|
+
if (r < max) break :power r;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1172
1124
|
},
|
|
1173
1125
|
else => unreachable,
|
|
1174
1126
|
};
|
|
1175
1127
|
|
|
1176
1128
|
if (battle.last_damage == 0) return if (showdown) null else Result.Error;
|
|
1177
1129
|
|
|
1178
|
-
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1179
1130
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
1180
|
-
if (battle.foe(player).stored().hp > 0
|
|
1131
|
+
if (battle.foe(player).stored().hp > 0) try buildRage(battle, player.foe(), log);
|
|
1181
1132
|
|
|
1182
1133
|
return null;
|
|
1183
1134
|
}
|
|
@@ -1190,17 +1141,19 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
|
|
|
1190
1141
|
return null;
|
|
1191
1142
|
}
|
|
1192
1143
|
|
|
1193
|
-
// Pretend
|
|
1144
|
+
// Pretend Counter was used as a stand-in when no move has been used to fail below with 0 BP
|
|
1145
|
+
const foe_last_used_move =
|
|
1146
|
+
Move.get(if (foe.last_used_move == .None) .Counter else foe.last_used_move);
|
|
1194
1147
|
const foe_last_selected_move =
|
|
1195
1148
|
Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
|
|
1196
|
-
.
|
|
1149
|
+
.Counter
|
|
1197
1150
|
else
|
|
1198
1151
|
foe.last_selected_move);
|
|
1199
1152
|
|
|
1200
|
-
const used =
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1153
|
+
const used = foe_last_used_move.bp > 0 and
|
|
1154
|
+
foe.last_used_move != .Counter and
|
|
1155
|
+
(foe_last_used_move.type == .Normal or
|
|
1156
|
+
foe_last_used_move.type == .Fighting);
|
|
1204
1157
|
|
|
1205
1158
|
const selected = foe_last_selected_move.bp > 0 and
|
|
1206
1159
|
foe.last_selected_move != .Counter and
|
|
@@ -1224,9 +1177,7 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
|
|
|
1224
1177
|
// Pokémon Showdown calls checkHit before Counter
|
|
1225
1178
|
if (!showdown and !try checkHit(battle, player, move, log)) return null;
|
|
1226
1179
|
|
|
1227
|
-
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1228
1180
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
1229
|
-
if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
|
|
1230
1181
|
return null;
|
|
1231
1182
|
}
|
|
1232
1183
|
|
|
@@ -1270,7 +1221,6 @@ fn mirrorMove(
|
|
|
1270
1221
|
battle: anytype,
|
|
1271
1222
|
player: Player,
|
|
1272
1223
|
mslot: u4,
|
|
1273
|
-
rewrap: bool,
|
|
1274
1224
|
auto: bool,
|
|
1275
1225
|
residual: *bool,
|
|
1276
1226
|
log: anytype,
|
|
@@ -1283,30 +1233,41 @@ fn mirrorMove(
|
|
|
1283
1233
|
if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
|
|
1284
1234
|
try log.fail(battle.active(player), .None);
|
|
1285
1235
|
return null;
|
|
1286
|
-
} else if (!showdown or foe.last_used_move != .Struggle) {
|
|
1287
|
-
incrementPP(side, mslot);
|
|
1288
1236
|
}
|
|
1289
1237
|
|
|
1238
|
+
incrementPP(side, mslot);
|
|
1239
|
+
|
|
1290
1240
|
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
|
|
1291
|
-
return doMove(battle, player, mslot,
|
|
1241
|
+
return doMove(battle, player, mslot, auto, residual, log);
|
|
1292
1242
|
}
|
|
1293
1243
|
|
|
1294
1244
|
fn metronome(
|
|
1295
1245
|
battle: anytype,
|
|
1296
1246
|
player: Player,
|
|
1297
1247
|
mslot: u4,
|
|
1298
|
-
rewrap: bool,
|
|
1299
1248
|
auto: bool,
|
|
1300
1249
|
residual: *bool,
|
|
1301
1250
|
log: anytype,
|
|
1302
1251
|
) !?Result {
|
|
1303
1252
|
var side = battle.side(player);
|
|
1304
1253
|
|
|
1305
|
-
side.last_selected_move =
|
|
1254
|
+
side.last_selected_move = if (showdown) blk: {
|
|
1255
|
+
const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
|
|
1256
|
+
const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
|
|
1257
|
+
break :blk @intToEnum(Move, r + mod);
|
|
1258
|
+
} else loop: {
|
|
1259
|
+
while (true) {
|
|
1260
|
+
const r = battle.rng.next();
|
|
1261
|
+
if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
|
|
1262
|
+
if (r >= @enumToInt(Move.Struggle)) continue;
|
|
1263
|
+
break :loop @intToEnum(Move, r);
|
|
1264
|
+
}
|
|
1265
|
+
};
|
|
1266
|
+
|
|
1306
1267
|
incrementPP(side, mslot);
|
|
1307
1268
|
|
|
1308
1269
|
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
|
|
1309
|
-
return doMove(battle, player, mslot,
|
|
1270
|
+
return doMove(battle, player, mslot, auto, residual, log);
|
|
1310
1271
|
}
|
|
1311
1272
|
|
|
1312
1273
|
fn checkHit(battle: anytype, player: Player, move: Move.Data, log: anytype) !bool {
|
|
@@ -1360,24 +1321,23 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
|
|
|
1360
1321
|
}
|
|
1361
1322
|
|
|
1362
1323
|
// GLITCH: Thrash / Petal Dance / Rage get their accuracy overwritten on subsequent hits
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
var accuracy = if (overwritten) state else @as(u16, Gen12.percent(move.accuracy));
|
|
1324
|
+
var state = &side.active.volatiles.state;
|
|
1325
|
+
const overwritten = (move.effect == .Thrashing or move.effect == .Rage) and state.* > 0;
|
|
1326
|
+
assert(!overwritten or (0 < state.* and state.* <= 255 and !side.active.volatiles.Bide));
|
|
1327
|
+
var accuracy = if (overwritten) state.* else @as(u16, Gen12.percent(move.accuracy));
|
|
1369
1328
|
var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
|
|
1370
1329
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1371
1330
|
boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
|
|
1372
1331
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1373
1332
|
accuracy = @min(255, @max(1, accuracy));
|
|
1374
1333
|
|
|
1375
|
-
|
|
1376
|
-
if (showdown) overwrite = side.active.volatiles.Rage or side.active.volatiles.Thrashing;
|
|
1377
|
-
if (overwrite) side.active.volatiles.state = accuracy;
|
|
1334
|
+
if (move.effect == .Thrashing or move.effect == .Rage) state.* = accuracy;
|
|
1378
1335
|
|
|
1379
1336
|
// GLITCH: max accuracy is 255 so 1/256 chance of miss
|
|
1380
|
-
break :miss
|
|
1337
|
+
break :miss if (showdown)
|
|
1338
|
+
!battle.rng.chance(u8, @intCast(u8, accuracy), 256)
|
|
1339
|
+
else
|
|
1340
|
+
battle.rng.next() >= accuracy;
|
|
1381
1341
|
};
|
|
1382
1342
|
|
|
1383
1343
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1509,6 +1469,9 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
|
|
|
1509
1469
|
damage *= volatiles.toxic;
|
|
1510
1470
|
}
|
|
1511
1471
|
|
|
1472
|
+
// Pokémon Showdown erroneously updates last damage with uncapped Leech Seed damage
|
|
1473
|
+
if (showdown) battle.last_damage = damage;
|
|
1474
|
+
|
|
1512
1475
|
const amount = @min(damage, stored.hp);
|
|
1513
1476
|
stored.hp -= amount;
|
|
1514
1477
|
|
|
@@ -1600,42 +1563,6 @@ fn checkEBC(battle: anytype) bool {
|
|
|
1600
1563
|
return false;
|
|
1601
1564
|
}
|
|
1602
1565
|
|
|
1603
|
-
fn disabled(side: *Side, ident: ID, log: anytype) !bool {
|
|
1604
|
-
if (side.active.volatiles.disabled_move != 0) {
|
|
1605
|
-
// A Pokémon that transforms after being disabled may end up with less move slots
|
|
1606
|
-
const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
|
|
1607
|
-
// side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
|
|
1608
|
-
const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
|
|
1609
|
-
m
|
|
1610
|
-
else
|
|
1611
|
-
side.last_selected_move;
|
|
1612
|
-
if (m != .None and m == last) {
|
|
1613
|
-
side.active.volatiles.Charging = false;
|
|
1614
|
-
try log.disabled(ident, last);
|
|
1615
|
-
return true;
|
|
1616
|
-
}
|
|
1617
|
-
}
|
|
1618
|
-
return false;
|
|
1619
|
-
}
|
|
1620
|
-
|
|
1621
|
-
inline fn buildRage(battle: anytype, who: Player, log: anytype) !void {
|
|
1622
|
-
const side = battle.side(who);
|
|
1623
|
-
if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
|
|
1624
|
-
try Effects.boost(battle, who, Move.get(.Rage), log);
|
|
1625
|
-
}
|
|
1626
|
-
}
|
|
1627
|
-
|
|
1628
|
-
fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
|
|
1629
|
-
var volatiles = &active.volatiles;
|
|
1630
|
-
assert(volatiles.Thrashing);
|
|
1631
|
-
if (volatiles.attacks > 0) return false;
|
|
1632
|
-
|
|
1633
|
-
volatiles.Thrashing = false;
|
|
1634
|
-
volatiles.Confusion = true;
|
|
1635
|
-
volatiles.confusion = Rolls.confusionDuration(battle);
|
|
1636
|
-
return true;
|
|
1637
|
-
}
|
|
1638
|
-
|
|
1639
1566
|
inline fn onBegin(
|
|
1640
1567
|
battle: anytype,
|
|
1641
1568
|
player: Player,
|
|
@@ -1726,7 +1653,10 @@ pub const Effects = struct {
|
|
|
1726
1653
|
side.active.volatiles.Bide = true;
|
|
1727
1654
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1728
1655
|
side.active.volatiles.state = 0;
|
|
1729
|
-
side.active.volatiles.attacks =
|
|
1656
|
+
side.active.volatiles.attacks = @intCast(u3, if (showdown)
|
|
1657
|
+
battle.rng.range(u4, 2, 4)
|
|
1658
|
+
else
|
|
1659
|
+
(battle.rng.next() & 1) + 2);
|
|
1730
1660
|
|
|
1731
1661
|
try log.start(battle.active(player), .Bide);
|
|
1732
1662
|
}
|
|
@@ -1748,8 +1678,14 @@ pub const Effects = struct {
|
|
|
1748
1678
|
return;
|
|
1749
1679
|
}
|
|
1750
1680
|
|
|
1751
|
-
|
|
1752
|
-
|
|
1681
|
+
const chance = !foe.active.types.includes(move.type) and if (showdown)
|
|
1682
|
+
battle.rng.chance(u8, @as(u8, if (move.effect == .BurnChance1) 26 else 77), 256)
|
|
1683
|
+
else
|
|
1684
|
+
battle.rng.next() < 1 + (if (move.effect == .BurnChance1)
|
|
1685
|
+
Gen12.percent(10)
|
|
1686
|
+
else
|
|
1687
|
+
Gen12.percent(30));
|
|
1688
|
+
if (!chance) return;
|
|
1753
1689
|
|
|
1754
1690
|
foe_stored.status = Status.init(.BRN);
|
|
1755
1691
|
foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
|
|
@@ -1773,7 +1709,11 @@ pub const Effects = struct {
|
|
|
1773
1709
|
const sub = foe.active.volatiles.Substitute;
|
|
1774
1710
|
|
|
1775
1711
|
if (move.effect == .ConfusionChance) {
|
|
1776
|
-
if (
|
|
1712
|
+
const chance = if (showdown)
|
|
1713
|
+
battle.rng.chance(u8, 25, 256)
|
|
1714
|
+
else
|
|
1715
|
+
battle.rng.next() < Gen12.percent(10);
|
|
1716
|
+
if (!chance) return;
|
|
1777
1717
|
} else {
|
|
1778
1718
|
if (showdown) {
|
|
1779
1719
|
if (!try checkHit(battle, player, move, log)) {
|
|
@@ -1792,7 +1732,10 @@ pub const Effects = struct {
|
|
|
1792
1732
|
|
|
1793
1733
|
if (foe.active.volatiles.Confusion) return;
|
|
1794
1734
|
foe.active.volatiles.Confusion = true;
|
|
1795
|
-
foe.active.volatiles.confusion =
|
|
1735
|
+
foe.active.volatiles.confusion = @intCast(u3, if (showdown)
|
|
1736
|
+
battle.rng.range(u8, 2, 6)
|
|
1737
|
+
else
|
|
1738
|
+
(battle.rng.next() & 3) + 2);
|
|
1796
1739
|
|
|
1797
1740
|
try log.start(battle.active(player.foe()), .Confusion);
|
|
1798
1741
|
}
|
|
@@ -1842,8 +1785,11 @@ pub const Effects = struct {
|
|
|
1842
1785
|
return Result.Error;
|
|
1843
1786
|
}
|
|
1844
1787
|
|
|
1845
|
-
volatiles.disabled_move = @intCast(u3,
|
|
1846
|
-
volatiles.disabled_duration =
|
|
1788
|
+
volatiles.disabled_move = @intCast(u3, randomMoveSlot(&battle.rng, &foe.active.moves, n));
|
|
1789
|
+
volatiles.disabled_duration = @intCast(u4, if (showdown)
|
|
1790
|
+
battle.rng.range(u8, 1, 9)
|
|
1791
|
+
else
|
|
1792
|
+
(battle.rng.next() & 7) + 1);
|
|
1847
1793
|
|
|
1848
1794
|
try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
|
|
1849
1795
|
return null;
|
|
@@ -1855,7 +1801,7 @@ pub const Effects = struct {
|
|
|
1855
1801
|
|
|
1856
1802
|
if (battle.last_damage == 0) {
|
|
1857
1803
|
assert(showdown);
|
|
1858
|
-
|
|
1804
|
+
return;
|
|
1859
1805
|
}
|
|
1860
1806
|
|
|
1861
1807
|
const drain = @max(battle.last_damage / 2, 1);
|
|
@@ -1882,7 +1828,15 @@ pub const Effects = struct {
|
|
|
1882
1828
|
var volatiles = &battle.foe(player).active.volatiles;
|
|
1883
1829
|
|
|
1884
1830
|
if (volatiles.Substitute) return;
|
|
1885
|
-
|
|
1831
|
+
|
|
1832
|
+
const chance = if (showdown)
|
|
1833
|
+
battle.rng.chance(u8, @as(u8, if (move.effect == .FlinchChance1) 26 else 77), 256)
|
|
1834
|
+
else
|
|
1835
|
+
battle.rng.next() < 1 + (if (move.effect == .FlinchChance1)
|
|
1836
|
+
Gen12.percent(10)
|
|
1837
|
+
else
|
|
1838
|
+
Gen12.percent(30));
|
|
1839
|
+
if (!chance) return;
|
|
1886
1840
|
|
|
1887
1841
|
volatiles.Flinch = true;
|
|
1888
1842
|
volatiles.Recharging = false;
|
|
@@ -1908,8 +1862,12 @@ pub const Effects = struct {
|
|
|
1908
1862
|
return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
|
|
1909
1863
|
}
|
|
1910
1864
|
|
|
1911
|
-
|
|
1912
|
-
|
|
1865
|
+
const chance = !foe.active.types.includes(move.type) and if (showdown)
|
|
1866
|
+
battle.rng.chance(u8, 26, 256)
|
|
1867
|
+
else
|
|
1868
|
+
battle.rng.next() < 1 + Gen12.percent(10);
|
|
1869
|
+
if (!chance) return;
|
|
1870
|
+
|
|
1913
1871
|
// Freeze Clause Mod
|
|
1914
1872
|
if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
1915
1873
|
|
|
@@ -2046,15 +2004,15 @@ pub const Effects = struct {
|
|
|
2046
2004
|
}
|
|
2047
2005
|
break :has_mimic false;
|
|
2048
2006
|
};
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2007
|
+
// If the foe is Invulnerable we still want to fall through to checkHit to be able to
|
|
2008
|
+
// trigger |-miss| instead of |-fail|
|
|
2009
|
+
if (!has_mimic and !foe.active.volatiles.Invulnerable) {
|
|
2052
2010
|
return try log.fail(battle.active(player.foe()), .None);
|
|
2053
2011
|
}
|
|
2054
2012
|
}
|
|
2055
2013
|
if (!try checkHit(battle, player, move, log)) return;
|
|
2056
2014
|
|
|
2057
|
-
const rslot =
|
|
2015
|
+
const rslot = randomMoveSlot(&battle.rng, &foe.active.moves, 0);
|
|
2058
2016
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
2059
2017
|
|
|
2060
2018
|
try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
|
|
@@ -2075,8 +2033,7 @@ pub const Effects = struct {
|
|
|
2075
2033
|
assert(!side.active.volatiles.MultiHit);
|
|
2076
2034
|
side.active.volatiles.MultiHit = true;
|
|
2077
2035
|
|
|
2078
|
-
side.active.volatiles.attacks =
|
|
2079
|
-
if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
|
|
2036
|
+
side.active.volatiles.attacks = if (move.effect == .MultiHit) distribution(battle) else 2;
|
|
2080
2037
|
}
|
|
2081
2038
|
|
|
2082
2039
|
fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2120,8 +2077,14 @@ pub const Effects = struct {
|
|
|
2120
2077
|
}
|
|
2121
2078
|
|
|
2122
2079
|
// Body Slam can't paralyze a Normal type Pokémon
|
|
2123
|
-
|
|
2124
|
-
|
|
2080
|
+
const chance = !foe.active.types.includes(move.type) and if (showdown)
|
|
2081
|
+
battle.rng.chance(u8, @as(u8, if (move.effect == .ParalyzeChance1) 26 else 77), 256)
|
|
2082
|
+
else
|
|
2083
|
+
battle.rng.next() < 1 + (if (move.effect == .ParalyzeChance1)
|
|
2084
|
+
Gen12.percent(10)
|
|
2085
|
+
else
|
|
2086
|
+
Gen12.percent(30));
|
|
2087
|
+
if (!chance) return;
|
|
2125
2088
|
|
|
2126
2089
|
foe_stored.status = Status.init(.PAR);
|
|
2127
2090
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
@@ -2164,7 +2127,16 @@ pub const Effects = struct {
|
|
|
2164
2127
|
|
|
2165
2128
|
if (move.effect == .Poison) {
|
|
2166
2129
|
if (!showdown and !try checkHit(battle, player, move, log)) return;
|
|
2167
|
-
} else
|
|
2130
|
+
} else {
|
|
2131
|
+
const chance = if (showdown)
|
|
2132
|
+
battle.rng.chance(u8, @as(u8, if (move.effect == .PoisonChance1) 52 else 103), 256)
|
|
2133
|
+
else
|
|
2134
|
+
battle.rng.next() < 1 + (if (move.effect == .PoisonChance1)
|
|
2135
|
+
Gen12.percent(20)
|
|
2136
|
+
else
|
|
2137
|
+
Gen12.percent(40));
|
|
2138
|
+
if (!chance) return;
|
|
2139
|
+
}
|
|
2168
2140
|
|
|
2169
2141
|
foe_stored.status = Status.init(.PSN);
|
|
2170
2142
|
if (toxic) {
|
|
@@ -2178,8 +2150,10 @@ pub const Effects = struct {
|
|
|
2178
2150
|
|
|
2179
2151
|
fn rage(battle: anytype, player: Player) !void {
|
|
2180
2152
|
var volatiles = &battle.side(player).active.volatiles;
|
|
2181
|
-
|
|
2153
|
+
if (volatiles.Rage) return;
|
|
2182
2154
|
volatiles.Rage = true;
|
|
2155
|
+
assert(!volatiles.Bide);
|
|
2156
|
+
volatiles.state = 0;
|
|
2183
2157
|
}
|
|
2184
2158
|
|
|
2185
2159
|
fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
|
|
@@ -2231,7 +2205,16 @@ pub const Effects = struct {
|
|
|
2231
2205
|
}
|
|
2232
2206
|
foe.active.volatiles.Recharging = false;
|
|
2233
2207
|
|
|
2234
|
-
|
|
2208
|
+
const duration = @intCast(u3, if (showdown)
|
|
2209
|
+
battle.rng.range(u8, 1, 8)
|
|
2210
|
+
else loop: {
|
|
2211
|
+
while (true) {
|
|
2212
|
+
const r = battle.rng.next() & 7;
|
|
2213
|
+
if (r != 0) break :loop r;
|
|
2214
|
+
}
|
|
2215
|
+
});
|
|
2216
|
+
|
|
2217
|
+
foe_stored.status = Status.slp(duration);
|
|
2235
2218
|
try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
|
|
2236
2219
|
}
|
|
2237
2220
|
|
|
@@ -2284,7 +2267,11 @@ pub const Effects = struct {
|
|
|
2284
2267
|
assert(!volatiles.Bide);
|
|
2285
2268
|
|
|
2286
2269
|
volatiles.Thrashing = true;
|
|
2287
|
-
volatiles.
|
|
2270
|
+
volatiles.state = 0;
|
|
2271
|
+
volatiles.attacks = @intCast(u3, if (showdown)
|
|
2272
|
+
battle.rng.range(u8, 2, 4)
|
|
2273
|
+
else
|
|
2274
|
+
(battle.rng.next() & 1) + 2);
|
|
2288
2275
|
}
|
|
2289
2276
|
|
|
2290
2277
|
fn transform(battle: anytype, player: Player, log: anytype) !void {
|
|
@@ -2316,7 +2303,7 @@ pub const Effects = struct {
|
|
|
2316
2303
|
try log.transform(battle.active(player), foe_ident);
|
|
2317
2304
|
}
|
|
2318
2305
|
|
|
2319
|
-
fn binding(battle: anytype, player: Player
|
|
2306
|
+
fn binding(battle: anytype, player: Player) void {
|
|
2320
2307
|
var side = battle.side(player);
|
|
2321
2308
|
var foe = battle.foe(player);
|
|
2322
2309
|
|
|
@@ -2324,11 +2311,9 @@ pub const Effects = struct {
|
|
|
2324
2311
|
side.active.volatiles.Binding = true;
|
|
2325
2312
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2326
2313
|
// (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
|
|
2327
|
-
if (!showdown)
|
|
2328
|
-
foe.active.volatiles.Recharging = false;
|
|
2329
|
-
} else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
|
|
2314
|
+
if (!showdown) foe.active.volatiles.Recharging = false;
|
|
2330
2315
|
|
|
2331
|
-
side.active.volatiles.attacks =
|
|
2316
|
+
side.active.volatiles.attacks = distribution(battle) - 1;
|
|
2332
2317
|
}
|
|
2333
2318
|
|
|
2334
2319
|
fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2355,11 +2340,9 @@ pub const Effects = struct {
|
|
|
2355
2340
|
return try log.fail(ident, .None);
|
|
2356
2341
|
}
|
|
2357
2342
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2358
|
-
const stat = unmodifiedStats(battle,
|
|
2343
|
+
const stat = unmodifiedStats(battle, player).atk;
|
|
2359
2344
|
stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2360
2345
|
try log.boost(ident, reason, n);
|
|
2361
|
-
// Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
|
|
2362
|
-
if (showdown and move.effect == .Rage) return;
|
|
2363
2346
|
},
|
|
2364
2347
|
.DefenseUp1, .DefenseUp2 => {
|
|
2365
2348
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
@@ -2375,7 +2358,7 @@ pub const Effects = struct {
|
|
|
2375
2358
|
return try log.fail(ident, .None);
|
|
2376
2359
|
}
|
|
2377
2360
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2378
|
-
const stat = unmodifiedStats(battle,
|
|
2361
|
+
const stat = unmodifiedStats(battle, player).def;
|
|
2379
2362
|
stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2380
2363
|
try log.boost(ident, .Defense, n);
|
|
2381
2364
|
},
|
|
@@ -2392,7 +2375,7 @@ pub const Effects = struct {
|
|
|
2392
2375
|
return try log.fail(ident, .None);
|
|
2393
2376
|
}
|
|
2394
2377
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2395
|
-
const stat = unmodifiedStats(battle,
|
|
2378
|
+
const stat = unmodifiedStats(battle, player).spe;
|
|
2396
2379
|
stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2397
2380
|
try log.boost(ident, .Speed, 2);
|
|
2398
2381
|
},
|
|
@@ -2412,7 +2395,7 @@ pub const Effects = struct {
|
|
|
2412
2395
|
return try log.fail(ident, .None);
|
|
2413
2396
|
}
|
|
2414
2397
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2415
|
-
const stat = unmodifiedStats(battle,
|
|
2398
|
+
const stat = unmodifiedStats(battle, player).spc;
|
|
2416
2399
|
stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2417
2400
|
try log.boost(ident, .SpecialAttack, n);
|
|
2418
2401
|
try log.boost(ident, .SpecialDefense, n);
|
|
@@ -2439,7 +2422,11 @@ pub const Effects = struct {
|
|
|
2439
2422
|
}
|
|
2440
2423
|
|
|
2441
2424
|
if (move.effect.isStatDownChance()) {
|
|
2442
|
-
if (
|
|
2425
|
+
const chance = if (showdown)
|
|
2426
|
+
battle.rng.chance(u8, 85, 256)
|
|
2427
|
+
else
|
|
2428
|
+
battle.rng.next() < Gen12.percent(33) + 1;
|
|
2429
|
+
if (!chance or foe.active.volatiles.Invulnerable) return;
|
|
2443
2430
|
} else if (!showdown and !try checkHit(battle, player, move, log)) {
|
|
2444
2431
|
return; // checkHit already checks for Invulnerable
|
|
2445
2432
|
}
|
|
@@ -2461,7 +2448,7 @@ pub const Effects = struct {
|
|
|
2461
2448
|
return try log.fail(foe_ident, .None);
|
|
2462
2449
|
}
|
|
2463
2450
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2464
|
-
const stat = unmodifiedStats(battle, foe).atk;
|
|
2451
|
+
const stat = unmodifiedStats(battle, player.foe()).atk;
|
|
2465
2452
|
stats.atk = @max(1, stat * mod[0] / mod[1]);
|
|
2466
2453
|
try log.boost(foe_ident, .Attack, -1);
|
|
2467
2454
|
},
|
|
@@ -2479,7 +2466,7 @@ pub const Effects = struct {
|
|
|
2479
2466
|
return try log.fail(foe_ident, .None);
|
|
2480
2467
|
}
|
|
2481
2468
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2482
|
-
const stat = unmodifiedStats(battle, foe).def;
|
|
2469
|
+
const stat = unmodifiedStats(battle, player.foe()).def;
|
|
2483
2470
|
stats.def = @max(1, stat * mod[0] / mod[1]);
|
|
2484
2471
|
try log.boost(foe_ident, .Defense, -@as(i8, n));
|
|
2485
2472
|
},
|
|
@@ -2496,7 +2483,7 @@ pub const Effects = struct {
|
|
|
2496
2483
|
return try log.fail(foe_ident, .None);
|
|
2497
2484
|
}
|
|
2498
2485
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2499
|
-
const stat = unmodifiedStats(battle, foe).spe;
|
|
2486
|
+
const stat = unmodifiedStats(battle, player.foe()).spe;
|
|
2500
2487
|
stats.spe = @max(1, stat * mod[0] / mod[1]);
|
|
2501
2488
|
try log.boost(foe_ident, .Speed, -1);
|
|
2502
2489
|
assert(boosts.spe >= -6);
|
|
@@ -2516,7 +2503,7 @@ pub const Effects = struct {
|
|
|
2516
2503
|
return try log.fail(foe_ident, .None);
|
|
2517
2504
|
}
|
|
2518
2505
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2519
|
-
const stat = unmodifiedStats(battle, foe).spc;
|
|
2506
|
+
const stat = unmodifiedStats(battle, player.foe()).spc;
|
|
2520
2507
|
stats.spc = @max(1, stat * mod[0] / mod[1]);
|
|
2521
2508
|
try log.boost(foe_ident, .SpecialAttack, -1);
|
|
2522
2509
|
try log.boost(foe_ident, .SpecialDefense, -1);
|
|
@@ -2535,7 +2522,8 @@ pub const Effects = struct {
|
|
|
2535
2522
|
}
|
|
2536
2523
|
};
|
|
2537
2524
|
|
|
2538
|
-
fn unmodifiedStats(battle: anytype,
|
|
2525
|
+
fn unmodifiedStats(battle: anytype, who: Player) *Stats(u16) {
|
|
2526
|
+
const side = battle.side(who);
|
|
2539
2527
|
if (!side.active.volatiles.Transform) return &side.stored().stats;
|
|
2540
2528
|
const id = ID.from(side.active.volatiles.transform);
|
|
2541
2529
|
return &battle.side(id.player).pokemon[id.id - 1].stats;
|
|
@@ -2601,140 +2589,75 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
|
|
|
2601
2589
|
}
|
|
2602
2590
|
}
|
|
2603
2591
|
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
return std.math.rotl(u8, battle.rng.next(), 3) < chance;
|
|
2613
|
-
}
|
|
2614
|
-
|
|
2615
|
-
inline fn damage(battle: anytype) u8 {
|
|
2616
|
-
if (showdown) return battle.rng.range(u8, 217, 256);
|
|
2617
|
-
while (true) {
|
|
2618
|
-
const r = std.math.rotr(u8, battle.rng.next(), 1);
|
|
2619
|
-
if (r >= 217) return r;
|
|
2592
|
+
fn disabled(side: *Side, ident: ID, log: anytype) !bool {
|
|
2593
|
+
if (side.active.volatiles.disabled_move != 0) {
|
|
2594
|
+
// A Pokémon that transforms after being disabled may end up with less move slots
|
|
2595
|
+
const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
|
|
2596
|
+
if (m != .None and m == side.last_selected_move) {
|
|
2597
|
+
side.active.volatiles.Charging = false;
|
|
2598
|
+
try log.disabled(ident, side.last_selected_move);
|
|
2599
|
+
return true;
|
|
2620
2600
|
}
|
|
2621
2601
|
}
|
|
2602
|
+
return false;
|
|
2603
|
+
}
|
|
2622
2604
|
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
inline fn confusionDuration(battle: anytype) u3 {
|
|
2629
|
-
if (showdown) return @intCast(u3, battle.rng.range(u8, 2, 6));
|
|
2630
|
-
return @intCast(u3, (battle.rng.next() & 3) + 2);
|
|
2631
|
-
}
|
|
2632
|
-
|
|
2633
|
-
inline fn confused(battle: anytype) bool {
|
|
2634
|
-
if (showdown) return !battle.rng.chance(u8, 128, 256);
|
|
2635
|
-
return battle.rng.next() >= Gen12.percent(50) + 1;
|
|
2636
|
-
}
|
|
2637
|
-
|
|
2638
|
-
inline fn paralyzed(battle: anytype) bool {
|
|
2639
|
-
if (showdown) return battle.rng.chance(u8, 63, 256);
|
|
2640
|
-
return battle.rng.next() < Gen12.percent(25);
|
|
2605
|
+
inline fn buildRage(battle: anytype, who: Player, log: anytype) !void {
|
|
2606
|
+
const side = battle.side(who);
|
|
2607
|
+
if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
|
|
2608
|
+
try Effects.boost(battle, who, Move.get(.Rage), log);
|
|
2641
2609
|
}
|
|
2610
|
+
}
|
|
2642
2611
|
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2612
|
+
fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
|
|
2613
|
+
var volatiles = &active.volatiles;
|
|
2614
|
+
assert(volatiles.Thrashing);
|
|
2615
|
+
if (volatiles.attacks > 0) return false;
|
|
2647
2616
|
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2617
|
+
volatiles.Thrashing = false;
|
|
2618
|
+
volatiles.Confusion = true;
|
|
2619
|
+
volatiles.confusion = @intCast(u3, if (showdown)
|
|
2620
|
+
battle.rng.range(u8, 2, 6)
|
|
2621
|
+
else
|
|
2622
|
+
(battle.rng.next() & 3) + 2);
|
|
2652
2623
|
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
return battle.rng.next() < 1 + (if (low) Gen12.percent(20) else Gen12.percent(40));
|
|
2656
|
-
}
|
|
2624
|
+
return true;
|
|
2625
|
+
}
|
|
2657
2626
|
|
|
2658
|
-
|
|
2659
|
-
if (showdown) return @intCast(u3, battle.rng.range(u8, 1, 8));
|
|
2660
|
-
while (true) {
|
|
2661
|
-
const r = battle.rng.next() & 7;
|
|
2662
|
-
if (r != 0) return @intCast(u3, r);
|
|
2663
|
-
}
|
|
2664
|
-
}
|
|
2627
|
+
const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
|
|
2665
2628
|
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2629
|
+
fn distribution(battle: anytype) u3 {
|
|
2630
|
+
if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
|
|
2631
|
+
const r = (battle.rng.next() & 3);
|
|
2632
|
+
return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
|
|
2633
|
+
}
|
|
2670
2634
|
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
if (
|
|
2686
|
-
r -= 1;
|
|
2687
|
-
if (r == 0) break;
|
|
2688
|
-
}
|
|
2635
|
+
fn randomMoveSlot(rand: anytype, moves: []MoveSlot, check_pp: u4) u4 {
|
|
2636
|
+
if (showdown) {
|
|
2637
|
+
if (check_pp == 0) {
|
|
2638
|
+
var i: usize = moves.len;
|
|
2639
|
+
while (i > 0) {
|
|
2640
|
+
i -= 1;
|
|
2641
|
+
if (moves[i].id != .None) return rand.range(u4, 0, @intCast(u4, i + 1)) + 1;
|
|
2642
|
+
}
|
|
2643
|
+
} else {
|
|
2644
|
+
var r = rand.range(u4, 0, @intCast(u4, check_pp)) + 1;
|
|
2645
|
+
var i: usize = 0;
|
|
2646
|
+
while (i < moves.len and r > 0) : (i += 1) {
|
|
2647
|
+
if (moves[i].pp > 0) {
|
|
2648
|
+
r -= 1;
|
|
2649
|
+
if (r == 0) break;
|
|
2689
2650
|
}
|
|
2690
|
-
return @intCast(u4, i + 1);
|
|
2691
2651
|
}
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
while (true) {
|
|
2695
|
-
const r = @intCast(u4, battle.rng.next() & 3);
|
|
2696
|
-
if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
|
|
2652
|
+
return @intCast(u4, i + 1);
|
|
2697
2653
|
}
|
|
2698
2654
|
}
|
|
2699
2655
|
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2656
|
+
while (true) {
|
|
2657
|
+
const r = @intCast(u4, rand.next() & 3);
|
|
2658
|
+
if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
|
|
2703
2659
|
}
|
|
2704
|
-
|
|
2705
|
-
inline fn metronome(battle: anytype) Move {
|
|
2706
|
-
if (showdown) {
|
|
2707
|
-
const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
|
|
2708
|
-
const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
|
|
2709
|
-
return @intToEnum(Move, r + mod);
|
|
2710
|
-
}
|
|
2711
|
-
while (true) {
|
|
2712
|
-
const r = battle.rng.next();
|
|
2713
|
-
if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
|
|
2714
|
-
if (r >= @enumToInt(Move.Struggle)) continue;
|
|
2715
|
-
return @intToEnum(Move, r);
|
|
2716
|
-
}
|
|
2717
|
-
}
|
|
2718
|
-
|
|
2719
|
-
inline fn psywave(battle: anytype, max: u8) u8 {
|
|
2720
|
-
if (showdown) return battle.rng.range(u8, 0, max);
|
|
2721
|
-
while (true) {
|
|
2722
|
-
const r = battle.rng.next();
|
|
2723
|
-
if (r < max) return r;
|
|
2724
|
-
}
|
|
2725
|
-
}
|
|
2726
|
-
|
|
2727
|
-
inline fn distribution(battle: anytype) u3 {
|
|
2728
|
-
if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
|
|
2729
|
-
const r = (battle.rng.next() & 3);
|
|
2730
|
-
return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
|
|
2731
|
-
}
|
|
2732
|
-
|
|
2733
|
-
inline fn unboost(battle: anytype) bool {
|
|
2734
|
-
if (showdown) return battle.rng.chance(u8, 85, 256);
|
|
2735
|
-
return battle.rng.next() < Gen12.percent(33) + 1;
|
|
2736
|
-
}
|
|
2737
|
-
};
|
|
2660
|
+
}
|
|
2738
2661
|
|
|
2739
2662
|
test "RNG agreement" {
|
|
2740
2663
|
if (!showdown) return;
|
|
@@ -2834,7 +2757,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
2834
2757
|
while (slot <= 4) : (slot += 1) {
|
|
2835
2758
|
const m = active.moves[slot - 1];
|
|
2836
2759
|
if (m.id == .None) break;
|
|
2837
|
-
if (m.id ==
|
|
2760
|
+
if (m.id == side.last_selected_move) {
|
|
2838
2761
|
// Pokémon Showdown displays Struggle if limited to Bide but unable to pick
|
|
2839
2762
|
const struggle =
|
|
2840
2763
|
m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);
|