@pkmn/engine 0.1.0-dev.af1565df → 0.1.0-dev.b9fb6eab
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -50
- package/build/pkg/data/layout.json +1 -1
- package/build/pkg/data.d.ts +2 -1
- package/build/pkg/data.js +5 -3
- package/build/pkg/data.js.map +1 -1
- package/build/pkg/gen1.d.ts +4 -5
- package/build/pkg/gen1.js +24 -43
- package/build/pkg/gen1.js.map +1 -1
- package/build/pkg/index.d.ts +12 -15
- package/build/pkg/index.js.map +1 -1
- package/build/pkg/protocol.d.ts +2 -4
- package/build/pkg/protocol.js +2 -4
- package/build/pkg/protocol.js.map +1 -1
- package/build.zig +22 -17
- package/package.json +29 -40
- package/src/bin/install-pkmn-engine +3 -3
- package/src/lib/binding/node.zig +5 -5
- package/src/lib/binding/wasm.zig +3 -3
- package/src/lib/common/debug.zig +0 -1
- package/src/lib/common/options.zig +3 -3
- package/src/lib/common/protocol.zig +38 -38
- package/src/lib/gen1/README.md +70 -102
- package/src/lib/gen1/data.zig +17 -19
- package/src/lib/gen1/helpers.zig +15 -4
- package/src/lib/gen1/mechanics.zig +311 -394
- package/src/lib/gen2/data.zig +4 -4
- package/src/lib/pkmn.zig +7 -7
- package/build/pkg/addon.d.ts +0 -7
- package/build/pkg/addon.js +0 -111
- package/build/pkg/addon.js.map +0 -1
- package/build/test/display.js +0 -400
- package/build/test/display.js.map +0 -1
- package/build/tools/debug.js +0 -133
- package/build/tools/debug.js.map +0 -1
- package/src/bin/pkmn-debug +0 -62
- package/src/test/display/pkmn.html.tmpl +0 -368
|
@@ -56,34 +56,30 @@ const BOOSTS = &[_][2]u8{
|
|
|
56
56
|
|
|
57
57
|
const MAX_STAT_VALUE = 999;
|
|
58
58
|
|
|
59
|
-
const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
|
|
60
|
-
|
|
61
59
|
pub fn update(battle: anytype, c1: Choice, c2: Choice, log: anytype) !Result {
|
|
62
60
|
assert(c1.type != .Pass or c2.type != .Pass or battle.turn == 0);
|
|
63
61
|
if (battle.turn == 0) return start(battle, log);
|
|
64
62
|
|
|
65
|
-
var s1 = false;
|
|
66
|
-
var s2 = false;
|
|
67
63
|
var f1: ?Move = null;
|
|
68
64
|
var f2: ?Move = null;
|
|
69
65
|
|
|
70
|
-
if (selectMove(battle, .P1, c1, c2, &
|
|
71
|
-
if (selectMove(battle, .P2, c2, c1, &
|
|
72
|
-
|
|
73
|
-
var p1 = battle.side(.P1);
|
|
74
|
-
var p2 = battle.side(.P2);
|
|
75
|
-
|
|
76
|
-
var r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
|
|
77
|
-
var r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
|
|
66
|
+
if (selectMove(battle, .P1, c1, c2, &f1)) |r| return r;
|
|
67
|
+
if (selectMove(battle, .P2, c2, c1, &f2)) |r| return r;
|
|
78
68
|
|
|
79
69
|
if (turnOrder(battle, c1, c2) == .P1) {
|
|
80
|
-
if (try doTurn(battle, .P1, c1,
|
|
70
|
+
if (try doTurn(battle, .P1, c1, f1, .P2, c2, f2, log)) |r| return r;
|
|
81
71
|
} else {
|
|
82
|
-
if (try doTurn(battle, .P2, c2,
|
|
72
|
+
if (try doTurn(battle, .P2, c2, f2, .P1, c1, f1, log)) |r| return r;
|
|
83
73
|
}
|
|
84
74
|
|
|
85
|
-
|
|
86
|
-
if (
|
|
75
|
+
var p1 = battle.side(.P1);
|
|
76
|
+
if (p1.active.volatiles.attacks == 0) {
|
|
77
|
+
p1.active.volatiles.Binding = false;
|
|
78
|
+
}
|
|
79
|
+
var p2 = battle.side(.P2);
|
|
80
|
+
if (p2.active.volatiles.attacks == 0) {
|
|
81
|
+
p2.active.volatiles.Binding = false;
|
|
82
|
+
}
|
|
87
83
|
|
|
88
84
|
return endTurn(battle, log);
|
|
89
85
|
}
|
|
@@ -116,7 +112,6 @@ fn selectMove(
|
|
|
116
112
|
player: Player,
|
|
117
113
|
choice: Choice,
|
|
118
114
|
foe_choice: Choice,
|
|
119
|
-
skip_turn: *bool,
|
|
120
115
|
from: *?Move,
|
|
121
116
|
) ?Result {
|
|
122
117
|
if (choice.type == .Pass) return null;
|
|
@@ -129,26 +124,17 @@ fn selectMove(
|
|
|
129
124
|
(choice.type == .Move and choice.data == @boolToInt(showdown)));
|
|
130
125
|
|
|
131
126
|
// pre-battle menu
|
|
132
|
-
if (volatiles.Recharging)
|
|
133
|
-
if (showdown and battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
|
|
134
|
-
return null;
|
|
135
|
-
}
|
|
127
|
+
if (volatiles.Recharging) return null;
|
|
136
128
|
if (volatiles.Rage) {
|
|
137
129
|
from.* = side.last_used_move;
|
|
138
|
-
if (showdown)
|
|
139
|
-
if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
|
|
140
|
-
saveMove(battle, player, null);
|
|
141
|
-
}
|
|
130
|
+
if (showdown) saveMove(battle, player, null);
|
|
142
131
|
return null;
|
|
143
132
|
}
|
|
144
133
|
// Pokémon Showdown removes Flinch at the end-of-turn in its residual handler
|
|
145
134
|
if (!showdown) volatiles.Flinch = false;
|
|
146
135
|
if (volatiles.Thrashing or volatiles.Charging) {
|
|
147
136
|
from.* = side.last_selected_move;
|
|
148
|
-
if (showdown)
|
|
149
|
-
if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
|
|
150
|
-
saveMove(battle, player, null);
|
|
151
|
-
}
|
|
137
|
+
if (showdown) saveMove(battle, player, null);
|
|
152
138
|
return null;
|
|
153
139
|
}
|
|
154
140
|
|
|
@@ -158,10 +144,7 @@ fn selectMove(
|
|
|
158
144
|
// pre-move select
|
|
159
145
|
if (Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP) or volatiles.Bide) {
|
|
160
146
|
assert(showdown or choice.data == 0);
|
|
161
|
-
if (showdown)
|
|
162
|
-
if (volatiles.Bide and battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
|
|
163
|
-
saveMove(battle, player, choice);
|
|
164
|
-
}
|
|
147
|
+
if (showdown) saveMove(battle, player, choice);
|
|
165
148
|
return null;
|
|
166
149
|
}
|
|
167
150
|
if (volatiles.Binding) {
|
|
@@ -180,9 +163,9 @@ fn selectMove(
|
|
|
180
163
|
// GLITCH: https://glitchcity.wiki/Partial_trapping_move_Mirror_Move_link_battle_glitch
|
|
181
164
|
if (foe_choice.type == .Switch) {
|
|
182
165
|
const slot = if (player == .P1)
|
|
183
|
-
battle.
|
|
166
|
+
battle.last_selected_indexes.p1
|
|
184
167
|
else
|
|
185
|
-
battle.
|
|
168
|
+
battle.last_selected_indexes.p2;
|
|
186
169
|
const last = side.active.move(@intCast(u4, slot));
|
|
187
170
|
if (last.id == .Metronome) side.last_selected_move = last.id;
|
|
188
171
|
if (last.id == .MirrorMove) return Result.Error;
|
|
@@ -192,7 +175,6 @@ fn selectMove(
|
|
|
192
175
|
}
|
|
193
176
|
|
|
194
177
|
if (battle.foe(player).active.volatiles.Binding) {
|
|
195
|
-
skip_turn.* = true;
|
|
196
178
|
if (showdown) {
|
|
197
179
|
saveMove(battle, player, choice);
|
|
198
180
|
} else {
|
|
@@ -203,7 +185,6 @@ fn selectMove(
|
|
|
203
185
|
}
|
|
204
186
|
|
|
205
187
|
// move select
|
|
206
|
-
volatiles.state = 0;
|
|
207
188
|
if (choice.data == 0) {
|
|
208
189
|
const struggle = ok: {
|
|
209
190
|
for (side.active.moves, 0..) |move, i| {
|
|
@@ -237,9 +218,9 @@ fn saveMove(battle: anytype, player: Player, choice: ?Choice) void {
|
|
|
237
218
|
|
|
238
219
|
side.last_selected_move = move.id;
|
|
239
220
|
if (player == .P1) {
|
|
240
|
-
battle.
|
|
221
|
+
battle.last_selected_indexes.p1 = @intCast(u4, c.data);
|
|
241
222
|
} else {
|
|
242
|
-
battle.
|
|
223
|
+
battle.last_selected_indexes.p2 = @intCast(u4, c.data);
|
|
243
224
|
}
|
|
244
225
|
}
|
|
245
226
|
}
|
|
@@ -259,9 +240,9 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, log: anyty
|
|
|
259
240
|
side.order[slot - 1] = out;
|
|
260
241
|
|
|
261
242
|
if (player == .P1) {
|
|
262
|
-
battle.
|
|
243
|
+
battle.last_selected_indexes.p1 = 1;
|
|
263
244
|
} else {
|
|
264
|
-
battle.
|
|
245
|
+
battle.last_selected_indexes.p2 = 1;
|
|
265
246
|
}
|
|
266
247
|
|
|
267
248
|
side.last_used_move = .None;
|
|
@@ -318,11 +299,13 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
|
|
|
318
299
|
const spe2 = battle.side(.P2).active.stats.spe;
|
|
319
300
|
if (spe1 == spe2) {
|
|
320
301
|
// Pokémon Showdown's beforeTurnCallback shenanigans
|
|
321
|
-
if (showdown and
|
|
322
|
-
|
|
323
|
-
|
|
302
|
+
if (showdown and m1 == .Counter and m2 == .Counter) battle.rng.advance(1);
|
|
303
|
+
|
|
304
|
+
const p1 = if (showdown)
|
|
305
|
+
battle.rng.range(u8, 0, 2) == 0
|
|
306
|
+
else
|
|
307
|
+
battle.rng.next() < Gen12.percent(50) + 1;
|
|
324
308
|
|
|
325
|
-
const p1 = Rolls.speedTie(battle);
|
|
326
309
|
if (!showdown) return if (p1) .P1 else .P2;
|
|
327
310
|
|
|
328
311
|
// Pokémon Showdown's "lockedmove" volatile's onBeforeTurn uses BattleQueue#changeAction,
|
|
@@ -332,7 +315,7 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
|
|
|
332
315
|
const t1 = battle.side(.P1).active.volatiles.Thrashing;
|
|
333
316
|
const t2 = battle.side(.P2).active.volatiles.Thrashing;
|
|
334
317
|
// If *both* sides are thrashing it really should be another speed tie, but we've patched
|
|
335
|
-
// that out and enforce host ordering of events, so P1 just goes first
|
|
318
|
+
// that out and enforce host ordering of events, so P1 just goes first reqgardless of who
|
|
336
319
|
// won the original coin flip
|
|
337
320
|
if (t1 and t2) return .P1;
|
|
338
321
|
return if (p1) if (t1 and !t2) .P2 else .P1 else if (t2 and !t1) .P1 else .P2;
|
|
@@ -345,13 +328,9 @@ fn doTurn(
|
|
|
345
328
|
battle: anytype,
|
|
346
329
|
player: Player,
|
|
347
330
|
player_choice: Choice,
|
|
348
|
-
player_rewrap: bool,
|
|
349
|
-
player_skip: bool,
|
|
350
331
|
player_from: ?Move,
|
|
351
332
|
foe_player: Player,
|
|
352
333
|
foe_choice: Choice,
|
|
353
|
-
foe_rewrap: bool,
|
|
354
|
-
foe_skip: bool,
|
|
355
334
|
foe_from: ?Move,
|
|
356
335
|
log: anytype,
|
|
357
336
|
) !?Result {
|
|
@@ -359,16 +338,7 @@ fn doTurn(
|
|
|
359
338
|
|
|
360
339
|
var residual = true;
|
|
361
340
|
var replace = battle.side(player).stored().hp == 0;
|
|
362
|
-
if (try executeMove(
|
|
363
|
-
battle,
|
|
364
|
-
player,
|
|
365
|
-
player_choice,
|
|
366
|
-
player_rewrap,
|
|
367
|
-
player_skip,
|
|
368
|
-
player_from,
|
|
369
|
-
&residual,
|
|
370
|
-
log,
|
|
371
|
-
)) |r| return r;
|
|
341
|
+
if (try executeMove(battle, player, player_choice, player_from, &residual, log)) |r| return r;
|
|
372
342
|
if (!replace) {
|
|
373
343
|
if (player_choice.type != .Switch) {
|
|
374
344
|
if (try checkFaint(battle, foe_player, log)) |r| return r;
|
|
@@ -379,16 +349,7 @@ fn doTurn(
|
|
|
379
349
|
|
|
380
350
|
residual = true;
|
|
381
351
|
replace = battle.side(foe_player).stored().hp == 0;
|
|
382
|
-
if (try executeMove(
|
|
383
|
-
battle,
|
|
384
|
-
foe_player,
|
|
385
|
-
foe_choice,
|
|
386
|
-
foe_rewrap,
|
|
387
|
-
foe_skip,
|
|
388
|
-
foe_from,
|
|
389
|
-
&residual,
|
|
390
|
-
log,
|
|
391
|
-
)) |r| return r;
|
|
352
|
+
if (try executeMove(battle, foe_player, foe_choice, foe_from, &residual, log)) |r| return r;
|
|
392
353
|
if (!replace) {
|
|
393
354
|
if (foe_choice.type != .Switch) {
|
|
394
355
|
if (try checkFaint(battle, player, log)) |r| return r;
|
|
@@ -411,8 +372,6 @@ fn executeMove(
|
|
|
411
372
|
battle: anytype,
|
|
412
373
|
player: Player,
|
|
413
374
|
choice: Choice,
|
|
414
|
-
rewrap: bool,
|
|
415
|
-
skip: bool,
|
|
416
375
|
from: ?Move,
|
|
417
376
|
residual: *bool,
|
|
418
377
|
log: anytype,
|
|
@@ -424,10 +383,9 @@ fn executeMove(
|
|
|
424
383
|
return null;
|
|
425
384
|
}
|
|
426
385
|
|
|
427
|
-
// This is the correct place to check for SKIP_TURN and abort early, however since Pokémon
|
|
428
|
-
// Showdown overwrites the SKIP_TURN sentinel with its botched move select we need to add an
|
|
429
|
-
// additional skip boolean to accomplish the same thing in the Binding check of BeforeMove
|
|
430
386
|
if (side.last_selected_move == .SKIP_TURN) {
|
|
387
|
+
// Pokémon Showdown overwrites the SKIP_TURN sentinel with its botched move select,
|
|
388
|
+
// Binding instead gets handled in beforeMove after sleep and freeze
|
|
431
389
|
assert(!showdown);
|
|
432
390
|
if (battle.foe(player).active.volatiles.Binding) {
|
|
433
391
|
try log.cant(battle.active(player), .Bound);
|
|
@@ -446,9 +404,9 @@ fn executeMove(
|
|
|
446
404
|
// choice.data == 0 only happens with Struggle on Pokémon Showdown
|
|
447
405
|
assert(!showdown);
|
|
448
406
|
mslot = @intCast(u4, if (player == .P1)
|
|
449
|
-
battle.
|
|
407
|
+
battle.last_selected_indexes.p1
|
|
450
408
|
else
|
|
451
|
-
battle.
|
|
409
|
+
battle.last_selected_indexes.p2);
|
|
452
410
|
const stored = side.stored();
|
|
453
411
|
// GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
|
|
454
412
|
auto = side.last_selected_move == .HyperBeam or side.active.volatiles.Bide or
|
|
@@ -470,14 +428,14 @@ fn executeMove(
|
|
|
470
428
|
// Incorrect mslot with Pokémon Showdown choice semantics so we need to recover from index
|
|
471
429
|
assert(mslot == 1);
|
|
472
430
|
mslot = @intCast(u4, if (player == .P1)
|
|
473
|
-
battle.
|
|
431
|
+
battle.last_selected_indexes.p1
|
|
474
432
|
else
|
|
475
|
-
battle.
|
|
433
|
+
battle.last_selected_indexes.p2);
|
|
476
434
|
}
|
|
477
435
|
|
|
478
436
|
var skip_can = false;
|
|
479
437
|
var skip_pp = false;
|
|
480
|
-
switch (try beforeMove(battle, player,
|
|
438
|
+
switch (try beforeMove(battle, player, from, residual, log)) {
|
|
481
439
|
.done => return null,
|
|
482
440
|
.skip_can => skip_can = true,
|
|
483
441
|
.skip_pp => skip_pp = true,
|
|
@@ -489,15 +447,14 @@ fn executeMove(
|
|
|
489
447
|
return null;
|
|
490
448
|
}
|
|
491
449
|
|
|
492
|
-
return doMove(battle, player, mslot,
|
|
450
|
+
return doMove(battle, player, mslot, auto, residual, log);
|
|
493
451
|
}
|
|
494
452
|
|
|
495
|
-
const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
|
|
453
|
+
const BeforeMove = union(enum) { done, skip_can, skip_pp, ok, err };
|
|
496
454
|
|
|
497
455
|
fn beforeMove(
|
|
498
456
|
battle: anytype,
|
|
499
457
|
player: Player,
|
|
500
|
-
skip: bool,
|
|
501
458
|
from: ?Move,
|
|
502
459
|
residual: *bool,
|
|
503
460
|
log: anytype,
|
|
@@ -529,15 +486,13 @@ fn beforeMove(
|
|
|
529
486
|
return .done;
|
|
530
487
|
}
|
|
531
488
|
|
|
532
|
-
if (
|
|
489
|
+
if (foe.active.volatiles.Binding) {
|
|
533
490
|
try log.cant(ident, .Bound);
|
|
534
491
|
return .done;
|
|
535
492
|
}
|
|
536
493
|
|
|
537
|
-
if (volatiles.Flinch) {
|
|
538
|
-
|
|
539
|
-
// Pokémon can sometimes flinch multiple times from the same original hit
|
|
540
|
-
if (!showdown) volatiles.Flinch = false;
|
|
494
|
+
if (!showdown and volatiles.Flinch) {
|
|
495
|
+
volatiles.Flinch = false;
|
|
541
496
|
try log.cant(ident, .Flinch);
|
|
542
497
|
return .done;
|
|
543
498
|
}
|
|
@@ -558,6 +513,13 @@ fn beforeMove(
|
|
|
558
513
|
// Pokémon Showdown's disable condition has a single onBeforeMove handler
|
|
559
514
|
if (showdown and try disabled(side, ident, log)) return .done;
|
|
560
515
|
|
|
516
|
+
// Pokémon Showdown checks for Flinch *after* instead of before
|
|
517
|
+
if (showdown and volatiles.Flinch) {
|
|
518
|
+
volatiles.Flinch = false;
|
|
519
|
+
try log.cant(ident, .Flinch);
|
|
520
|
+
return .done;
|
|
521
|
+
}
|
|
522
|
+
|
|
561
523
|
// This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
|
|
562
524
|
// before the side had a selected a move - we simply need to assume this leads to a desync
|
|
563
525
|
if (side.last_selected_move == .None) {
|
|
@@ -575,7 +537,12 @@ fn beforeMove(
|
|
|
575
537
|
} else {
|
|
576
538
|
try log.activate(ident, .Confusion);
|
|
577
539
|
|
|
578
|
-
if (
|
|
540
|
+
const confused = if (showdown)
|
|
541
|
+
!battle.rng.chance(u8, 128, 256)
|
|
542
|
+
else
|
|
543
|
+
battle.rng.next() >= Gen12.percent(50) + 1;
|
|
544
|
+
|
|
545
|
+
if (confused) {
|
|
579
546
|
assert(!volatiles.MultiHit);
|
|
580
547
|
if (!volatiles.Rage) volatiles.state = 0;
|
|
581
548
|
volatiles.Bide = false;
|
|
@@ -601,12 +568,8 @@ fn beforeMove(
|
|
|
601
568
|
player
|
|
602
569
|
else
|
|
603
570
|
player.foe();
|
|
604
|
-
|
|
605
|
-
const uncapped = battle.last_damage;
|
|
606
571
|
// Skipping adjustDamage / randomizeDamage / checkHit
|
|
607
572
|
_ = try applyDamage(battle, player, target, .Confusion, log);
|
|
608
|
-
// Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
|
|
609
|
-
if (showdown) battle.last_damage = uncapped;
|
|
610
573
|
|
|
611
574
|
return .done;
|
|
612
575
|
}
|
|
@@ -615,28 +578,28 @@ fn beforeMove(
|
|
|
615
578
|
|
|
616
579
|
if (!showdown and try disabled(side, ident, log)) return .done;
|
|
617
580
|
|
|
618
|
-
if (Status.is(stored.status, .PAR)
|
|
619
|
-
if (
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
581
|
+
if (Status.is(stored.status, .PAR)) {
|
|
582
|
+
const paralyzed = if (showdown)
|
|
583
|
+
battle.rng.chance(u8, 63, 256)
|
|
584
|
+
else
|
|
585
|
+
battle.rng.next() < Gen12.percent(25);
|
|
586
|
+
|
|
587
|
+
if (paralyzed) {
|
|
588
|
+
if (!volatiles.Rage) volatiles.state = 0;
|
|
589
|
+
volatiles.Bide = false;
|
|
590
|
+
volatiles.Thrashing = false;
|
|
591
|
+
volatiles.Charging = false;
|
|
592
|
+
volatiles.Binding = false;
|
|
593
|
+
// GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
|
|
594
|
+
try log.cant(ident, .Paralysis);
|
|
595
|
+
return .done;
|
|
596
|
+
}
|
|
627
597
|
}
|
|
628
598
|
|
|
629
599
|
if (volatiles.Bide) {
|
|
630
600
|
assert(!volatiles.Thrashing and !volatiles.Rage);
|
|
631
601
|
|
|
632
|
-
|
|
633
|
-
// Pokémon Showdown doesn't implement Bide potentially overflowing in the event of
|
|
634
|
-
// OHKO-move damage, but we can fake this incorrect behavior by simply saturating the
|
|
635
|
-
// addition because 65535 is sufficient to faint any Pokémon anyway
|
|
636
|
-
volatiles.state +|= battle.last_damage;
|
|
637
|
-
} else {
|
|
638
|
-
volatiles.state +%= battle.last_damage;
|
|
639
|
-
}
|
|
602
|
+
volatiles.state +%= battle.last_damage;
|
|
640
603
|
|
|
641
604
|
assert(volatiles.attacks > 0);
|
|
642
605
|
|
|
@@ -657,9 +620,8 @@ fn beforeMove(
|
|
|
657
620
|
return .done;
|
|
658
621
|
}
|
|
659
622
|
|
|
660
|
-
const sub = showdown and foe.active.volatiles.Substitute;
|
|
661
623
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
662
|
-
if (foe.stored().hp > 0
|
|
624
|
+
if (foe.stored().hp > 0) try buildRage(battle, player.foe(), log);
|
|
663
625
|
|
|
664
626
|
// For reasons passing understanding, Pokémon Showdown still inflicts residual damage to
|
|
665
627
|
// Bide's user even if the above damage has caused the foe to faint. It's simpler to always
|
|
@@ -680,13 +642,8 @@ fn beforeMove(
|
|
|
680
642
|
try log.start(battle.active(player), .ConfusionSilent);
|
|
681
643
|
}
|
|
682
644
|
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
|
-
}
|
|
645
|
+
if (showdown and handleThrashing(battle, active)) {
|
|
646
|
+
try log.start(battle.active(player), .ConfusionSilent);
|
|
690
647
|
}
|
|
691
648
|
return .skip_can;
|
|
692
649
|
}
|
|
@@ -701,11 +658,7 @@ fn beforeMove(
|
|
|
701
658
|
side.last_selected_move,
|
|
702
659
|
);
|
|
703
660
|
if (showdown or battle.last_damage != 0) {
|
|
704
|
-
const sub = showdown and foe.active.volatiles.Substitute;
|
|
705
661
|
_ = 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
662
|
}
|
|
710
663
|
return .done;
|
|
711
664
|
}
|
|
@@ -737,7 +690,6 @@ fn canMove(
|
|
|
737
690
|
side.active.volatiles.Invulnerable = false;
|
|
738
691
|
} else if (move.effect == .Charge) {
|
|
739
692
|
try log.move(player_ident, side.last_selected_move, .{}, from);
|
|
740
|
-
setCounterable(battle, player, side, move);
|
|
741
693
|
try Effects.charge(battle, player, log);
|
|
742
694
|
return false;
|
|
743
695
|
}
|
|
@@ -754,7 +706,6 @@ fn canMove(
|
|
|
754
706
|
// regrettable, but this information is more "nice to have" than required
|
|
755
707
|
const f = if (from != null and from.? == .None) null else from;
|
|
756
708
|
try log.move(player_ident, side.last_selected_move, battle.active(target), f);
|
|
757
|
-
setCounterable(battle, player, side, move);
|
|
758
709
|
|
|
759
710
|
if (move.effect.onBegin()) {
|
|
760
711
|
try onBegin(battle, player, move, mslot, residual, log);
|
|
@@ -765,27 +716,12 @@ fn canMove(
|
|
|
765
716
|
Effects.thrashing(battle, player);
|
|
766
717
|
} else if (!showdown and move.effect == .Binding) {
|
|
767
718
|
// Pokémon Showdown handles this after hit/miss checks and damage calculation
|
|
768
|
-
Effects.binding(battle, player
|
|
719
|
+
Effects.binding(battle, player);
|
|
769
720
|
}
|
|
770
721
|
|
|
771
722
|
return true;
|
|
772
723
|
}
|
|
773
724
|
|
|
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
725
|
fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
|
|
790
726
|
if (side.last_selected_move == .Struggle) return;
|
|
791
727
|
|
|
@@ -821,7 +757,6 @@ fn doMove(
|
|
|
821
757
|
battle: anytype,
|
|
822
758
|
player: Player,
|
|
823
759
|
mslot: u4,
|
|
824
|
-
rewrap: bool,
|
|
825
760
|
auto: bool,
|
|
826
761
|
residual: *bool,
|
|
827
762
|
log: anytype,
|
|
@@ -852,15 +787,12 @@ fn doMove(
|
|
|
852
787
|
(@enumToInt(move.type.effectiveness(foe.active.types.type1)) == 0 or
|
|
853
788
|
@enumToInt(move.type.effectiveness(foe.active.types.type2)) == 0);
|
|
854
789
|
if (immune and move.effect != .Binding) break :miss true;
|
|
855
|
-
if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe)
|
|
856
|
-
battle.last_damage = 0;
|
|
857
|
-
break :miss true;
|
|
858
|
-
}
|
|
790
|
+
if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) break :miss true;
|
|
859
791
|
break :miss move.target != .Self and !moveHit(battle, player, move, &immune, &mist);
|
|
860
792
|
};
|
|
861
793
|
assert(!immune or miss or (showdown and move.effect == .Binding));
|
|
862
794
|
|
|
863
|
-
var late = showdown
|
|
795
|
+
var late = showdown;
|
|
864
796
|
const skip = status or immune;
|
|
865
797
|
if ((!showdown or (!skip or counter)) and !miss) blk: {
|
|
866
798
|
if (showdown and move.effect.isMulti()) {
|
|
@@ -913,9 +845,9 @@ fn doMove(
|
|
|
913
845
|
|
|
914
846
|
if (!showdown or !miss) {
|
|
915
847
|
if (move.effect == .MirrorMove) {
|
|
916
|
-
return mirrorMove(battle, player, mslot,
|
|
848
|
+
return mirrorMove(battle, player, mslot, auto, residual, log);
|
|
917
849
|
} else if (move.effect == .Metronome) {
|
|
918
|
-
return metronome(battle, player, mslot,
|
|
850
|
+
return metronome(battle, player, mslot, auto, residual, log);
|
|
919
851
|
} else if (move.effect.onEnd()) {
|
|
920
852
|
try onEnd(battle, player, move, log);
|
|
921
853
|
return null;
|
|
@@ -930,7 +862,7 @@ fn doMove(
|
|
|
930
862
|
move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
|
|
931
863
|
if (ohko) {
|
|
932
864
|
try log.immune(foe_ident, .OHKO);
|
|
933
|
-
} else if (immune and !invulnerable
|
|
865
|
+
} else if (immune and !invulnerable) {
|
|
934
866
|
try log.immune(foe_ident, .None);
|
|
935
867
|
} else if (mist) {
|
|
936
868
|
if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
|
|
@@ -962,9 +894,6 @@ fn doMove(
|
|
|
962
894
|
hits = side.active.volatiles.attacks;
|
|
963
895
|
}
|
|
964
896
|
|
|
965
|
-
// Pokémon Showdown only builds Rage for Disable/Explosion (hit/miss) when attacking into a sub
|
|
966
|
-
const sub = showdown and foe.active.volatiles.Substitute;
|
|
967
|
-
|
|
968
897
|
var nullified = false;
|
|
969
898
|
var hit: u4 = 0;
|
|
970
899
|
while (hit < hits) {
|
|
@@ -980,7 +909,7 @@ fn doMove(
|
|
|
980
909
|
if (hit == 0 and ohko) try log.ohko();
|
|
981
910
|
hit += 1;
|
|
982
911
|
if (foe.stored().hp == 0) break;
|
|
983
|
-
if (!late
|
|
912
|
+
if (!late) try buildRage(battle, player.foe(), log);
|
|
984
913
|
// If the substitute breaks during a multi-hit attack, the attack ends
|
|
985
914
|
if (nullified) break;
|
|
986
915
|
}
|
|
@@ -996,17 +925,11 @@ fn doMove(
|
|
|
996
925
|
} else if (showdown) {
|
|
997
926
|
// This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
|
|
998
927
|
if (move.effect == .Binding) {
|
|
999
|
-
Effects.binding(battle, player
|
|
928
|
+
Effects.binding(battle, player);
|
|
1000
929
|
if (immune) {
|
|
1001
930
|
battle.last_damage = 0;
|
|
1002
|
-
assert(foe.stored().hp > 0);
|
|
1003
931
|
// 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
|
-
}
|
|
932
|
+
try log.damage(battle.active(player.foe()), foe.stored(), .None);
|
|
1010
933
|
return null;
|
|
1011
934
|
}
|
|
1012
935
|
}
|
|
@@ -1023,8 +946,8 @@ fn doMove(
|
|
|
1023
946
|
|
|
1024
947
|
if (foe.stored().hp == 0) return null;
|
|
1025
948
|
|
|
1026
|
-
// Pokémon Showdown builds Rage at the wrong time for non-MultiHit
|
|
1027
|
-
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);
|
|
1028
951
|
|
|
1029
952
|
if (!move.effect.isSpecial()) {
|
|
1030
953
|
// On the cartridge Rage is not considered to be "special" and thus gets executed for a
|
|
@@ -1057,7 +980,8 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
|
|
|
1057
980
|
chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
|
|
1058
981
|
chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
|
|
1059
982
|
|
|
1060
|
-
return
|
|
983
|
+
if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
|
|
984
|
+
return std.math.rotl(u8, battle.rng.next(), 3) < chance;
|
|
1061
985
|
}
|
|
1062
986
|
|
|
1063
987
|
fn calcDamage(
|
|
@@ -1161,36 +1085,50 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
|
1161
1085
|
|
|
1162
1086
|
fn randomizeDamage(battle: anytype) void {
|
|
1163
1087
|
if (battle.last_damage <= 1) return;
|
|
1164
|
-
|
|
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
|
+
|
|
1165
1098
|
battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
|
|
1166
1099
|
}
|
|
1167
1100
|
|
|
1168
1101
|
fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
|
|
1169
1102
|
const side = battle.side(player);
|
|
1170
|
-
const foe = battle.foe(player);
|
|
1171
1103
|
|
|
1172
1104
|
if (!try checkHit(battle, player, move, log)) return null;
|
|
1173
1105
|
|
|
1174
1106
|
battle.last_damage = switch (side.last_selected_move) {
|
|
1175
|
-
.SuperFang => @max(foe.stored().hp / 2, 1),
|
|
1107
|
+
.SuperFang => @max(battle.foe(player).stored().hp / 2, 1),
|
|
1176
1108
|
.SeismicToss, .NightShade => side.stored().level,
|
|
1177
1109
|
.SonicBoom => 20,
|
|
1178
1110
|
.DragonRage => 40,
|
|
1179
1111
|
// GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
|
|
1180
1112
|
.Psywave => power: {
|
|
1181
1113
|
const max = @intCast(u8, @as(u16, side.stored().level) * 3 / 2);
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
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
|
+
}
|
|
1185
1124
|
},
|
|
1186
1125
|
else => unreachable,
|
|
1187
1126
|
};
|
|
1188
1127
|
|
|
1189
1128
|
if (battle.last_damage == 0) return if (showdown) null else Result.Error;
|
|
1190
1129
|
|
|
1191
|
-
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1192
1130
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
1193
|
-
if (battle.foe(player).stored().hp > 0
|
|
1131
|
+
if (battle.foe(player).stored().hp > 0) try buildRage(battle, player.foe(), log);
|
|
1194
1132
|
|
|
1195
1133
|
return null;
|
|
1196
1134
|
}
|
|
@@ -1203,17 +1141,19 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
|
|
|
1203
1141
|
return null;
|
|
1204
1142
|
}
|
|
1205
1143
|
|
|
1206
|
-
// 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);
|
|
1207
1147
|
const foe_last_selected_move =
|
|
1208
1148
|
Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
|
|
1209
|
-
.
|
|
1149
|
+
.Counter
|
|
1210
1150
|
else
|
|
1211
1151
|
foe.last_selected_move);
|
|
1212
1152
|
|
|
1213
|
-
const used =
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
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);
|
|
1217
1157
|
|
|
1218
1158
|
const selected = foe_last_selected_move.bp > 0 and
|
|
1219
1159
|
foe.last_selected_move != .Counter and
|
|
@@ -1232,14 +1172,12 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
|
|
|
1232
1172
|
return null;
|
|
1233
1173
|
}
|
|
1234
1174
|
|
|
1235
|
-
battle.last_damage
|
|
1175
|
+
battle.last_damage = if (battle.last_damage > 0x7FFF) 0xFFFF else battle.last_damage * 2;
|
|
1236
1176
|
|
|
1237
1177
|
// Pokémon Showdown calls checkHit before Counter
|
|
1238
1178
|
if (!showdown and !try checkHit(battle, player, move, log)) return null;
|
|
1239
1179
|
|
|
1240
|
-
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1241
1180
|
_ = 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
1181
|
return null;
|
|
1244
1182
|
}
|
|
1245
1183
|
|
|
@@ -1283,7 +1221,6 @@ fn mirrorMove(
|
|
|
1283
1221
|
battle: anytype,
|
|
1284
1222
|
player: Player,
|
|
1285
1223
|
mslot: u4,
|
|
1286
|
-
rewrap: bool,
|
|
1287
1224
|
auto: bool,
|
|
1288
1225
|
residual: *bool,
|
|
1289
1226
|
log: anytype,
|
|
@@ -1296,30 +1233,41 @@ fn mirrorMove(
|
|
|
1296
1233
|
if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
|
|
1297
1234
|
try log.fail(battle.active(player), .None);
|
|
1298
1235
|
return null;
|
|
1299
|
-
} else if (!showdown or foe.last_used_move != .Struggle) {
|
|
1300
|
-
incrementPP(side, mslot);
|
|
1301
1236
|
}
|
|
1302
1237
|
|
|
1238
|
+
incrementPP(side, mslot);
|
|
1239
|
+
|
|
1303
1240
|
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
|
|
1304
|
-
return doMove(battle, player, mslot,
|
|
1241
|
+
return doMove(battle, player, mslot, auto, residual, log);
|
|
1305
1242
|
}
|
|
1306
1243
|
|
|
1307
1244
|
fn metronome(
|
|
1308
1245
|
battle: anytype,
|
|
1309
1246
|
player: Player,
|
|
1310
1247
|
mslot: u4,
|
|
1311
|
-
rewrap: bool,
|
|
1312
1248
|
auto: bool,
|
|
1313
1249
|
residual: *bool,
|
|
1314
1250
|
log: anytype,
|
|
1315
1251
|
) !?Result {
|
|
1316
1252
|
var side = battle.side(player);
|
|
1317
1253
|
|
|
1318
|
-
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
|
+
|
|
1319
1267
|
incrementPP(side, mslot);
|
|
1320
1268
|
|
|
1321
1269
|
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
|
|
1322
|
-
return doMove(battle, player, mslot,
|
|
1270
|
+
return doMove(battle, player, mslot, auto, residual, log);
|
|
1323
1271
|
}
|
|
1324
1272
|
|
|
1325
1273
|
fn checkHit(battle: anytype, player: Player, move: Move.Data, log: anytype) !bool {
|
|
@@ -1373,24 +1321,23 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
|
|
|
1373
1321
|
}
|
|
1374
1322
|
|
|
1375
1323
|
// 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));
|
|
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));
|
|
1382
1328
|
var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
|
|
1383
1329
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1384
1330
|
boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
|
|
1385
1331
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1386
1332
|
accuracy = @min(255, @max(1, accuracy));
|
|
1387
1333
|
|
|
1388
|
-
|
|
1389
|
-
if (showdown) overwrite = side.active.volatiles.Rage or side.active.volatiles.Thrashing;
|
|
1390
|
-
if (overwrite) side.active.volatiles.state = accuracy;
|
|
1334
|
+
if (move.effect == .Thrashing or move.effect == .Rage) state.* = accuracy;
|
|
1391
1335
|
|
|
1392
1336
|
// GLITCH: max accuracy is 255 so 1/256 chance of miss
|
|
1393
|
-
break :miss
|
|
1337
|
+
break :miss if (showdown)
|
|
1338
|
+
!battle.rng.chance(u8, @intCast(u8, accuracy), 256)
|
|
1339
|
+
else
|
|
1340
|
+
battle.rng.next() >= accuracy;
|
|
1394
1341
|
};
|
|
1395
1342
|
|
|
1396
1343
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1522,6 +1469,9 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
|
|
|
1522
1469
|
damage *= volatiles.toxic;
|
|
1523
1470
|
}
|
|
1524
1471
|
|
|
1472
|
+
// Pokémon Showdown erroneously updates last damage with uncapped Leech Seed damage
|
|
1473
|
+
if (showdown) battle.last_damage = damage;
|
|
1474
|
+
|
|
1525
1475
|
const amount = @min(damage, stored.hp);
|
|
1526
1476
|
stored.hp -= amount;
|
|
1527
1477
|
|
|
@@ -1540,13 +1490,13 @@ fn endTurn(battle: anytype, log: anytype) @TypeOf(log).Error!Result {
|
|
|
1540
1490
|
assert(!battle.side(.P1).active.volatiles.MultiHit);
|
|
1541
1491
|
assert(!battle.side(.P2).active.volatiles.MultiHit);
|
|
1542
1492
|
|
|
1543
|
-
battle.turn += 1;
|
|
1544
|
-
|
|
1545
1493
|
if (showdown and options.ebc and checkEBC(battle)) {
|
|
1546
1494
|
try log.tie();
|
|
1547
1495
|
return Result.Tie;
|
|
1548
1496
|
}
|
|
1549
1497
|
|
|
1498
|
+
battle.turn += 1;
|
|
1499
|
+
|
|
1550
1500
|
if (showdown and battle.turn >= 1000) {
|
|
1551
1501
|
try log.tie();
|
|
1552
1502
|
return Result.Tie;
|
|
@@ -1613,42 +1563,6 @@ fn checkEBC(battle: anytype) bool {
|
|
|
1613
1563
|
return false;
|
|
1614
1564
|
}
|
|
1615
1565
|
|
|
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
1566
|
inline fn onBegin(
|
|
1653
1567
|
battle: anytype,
|
|
1654
1568
|
player: Player,
|
|
@@ -1739,7 +1653,10 @@ pub const Effects = struct {
|
|
|
1739
1653
|
side.active.volatiles.Bide = true;
|
|
1740
1654
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1741
1655
|
side.active.volatiles.state = 0;
|
|
1742
|
-
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);
|
|
1743
1660
|
|
|
1744
1661
|
try log.start(battle.active(player), .Bide);
|
|
1745
1662
|
}
|
|
@@ -1761,8 +1678,14 @@ pub const Effects = struct {
|
|
|
1761
1678
|
return;
|
|
1762
1679
|
}
|
|
1763
1680
|
|
|
1764
|
-
|
|
1765
|
-
|
|
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;
|
|
1766
1689
|
|
|
1767
1690
|
foe_stored.status = Status.init(.BRN);
|
|
1768
1691
|
foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
|
|
@@ -1786,7 +1709,11 @@ pub const Effects = struct {
|
|
|
1786
1709
|
const sub = foe.active.volatiles.Substitute;
|
|
1787
1710
|
|
|
1788
1711
|
if (move.effect == .ConfusionChance) {
|
|
1789
|
-
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;
|
|
1790
1717
|
} else {
|
|
1791
1718
|
if (showdown) {
|
|
1792
1719
|
if (!try checkHit(battle, player, move, log)) {
|
|
@@ -1805,7 +1732,10 @@ pub const Effects = struct {
|
|
|
1805
1732
|
|
|
1806
1733
|
if (foe.active.volatiles.Confusion) return;
|
|
1807
1734
|
foe.active.volatiles.Confusion = true;
|
|
1808
|
-
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);
|
|
1809
1739
|
|
|
1810
1740
|
try log.start(battle.active(player.foe()), .Confusion);
|
|
1811
1741
|
}
|
|
@@ -1855,8 +1785,11 @@ pub const Effects = struct {
|
|
|
1855
1785
|
return Result.Error;
|
|
1856
1786
|
}
|
|
1857
1787
|
|
|
1858
|
-
volatiles.disabled_move = @intCast(u3,
|
|
1859
|
-
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);
|
|
1860
1793
|
|
|
1861
1794
|
try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
|
|
1862
1795
|
return null;
|
|
@@ -1868,7 +1801,7 @@ pub const Effects = struct {
|
|
|
1868
1801
|
|
|
1869
1802
|
if (battle.last_damage == 0) {
|
|
1870
1803
|
assert(showdown);
|
|
1871
|
-
|
|
1804
|
+
return;
|
|
1872
1805
|
}
|
|
1873
1806
|
|
|
1874
1807
|
const drain = @max(battle.last_damage / 2, 1);
|
|
@@ -1895,7 +1828,15 @@ pub const Effects = struct {
|
|
|
1895
1828
|
var volatiles = &battle.foe(player).active.volatiles;
|
|
1896
1829
|
|
|
1897
1830
|
if (volatiles.Substitute) return;
|
|
1898
|
-
|
|
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;
|
|
1899
1840
|
|
|
1900
1841
|
volatiles.Flinch = true;
|
|
1901
1842
|
volatiles.Recharging = false;
|
|
@@ -1921,8 +1862,12 @@ pub const Effects = struct {
|
|
|
1921
1862
|
return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
|
|
1922
1863
|
}
|
|
1923
1864
|
|
|
1924
|
-
|
|
1925
|
-
|
|
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
|
+
|
|
1926
1871
|
// Freeze Clause Mod
|
|
1927
1872
|
if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
1928
1873
|
|
|
@@ -2059,15 +2004,15 @@ pub const Effects = struct {
|
|
|
2059
2004
|
}
|
|
2060
2005
|
break :has_mimic false;
|
|
2061
2006
|
};
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
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) {
|
|
2065
2010
|
return try log.fail(battle.active(player.foe()), .None);
|
|
2066
2011
|
}
|
|
2067
2012
|
}
|
|
2068
2013
|
if (!try checkHit(battle, player, move, log)) return;
|
|
2069
2014
|
|
|
2070
|
-
const rslot =
|
|
2015
|
+
const rslot = randomMoveSlot(&battle.rng, &foe.active.moves, 0);
|
|
2071
2016
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
2072
2017
|
|
|
2073
2018
|
try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
|
|
@@ -2088,8 +2033,7 @@ pub const Effects = struct {
|
|
|
2088
2033
|
assert(!side.active.volatiles.MultiHit);
|
|
2089
2034
|
side.active.volatiles.MultiHit = true;
|
|
2090
2035
|
|
|
2091
|
-
side.active.volatiles.attacks =
|
|
2092
|
-
if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
|
|
2036
|
+
side.active.volatiles.attacks = if (move.effect == .MultiHit) distribution(battle) else 2;
|
|
2093
2037
|
}
|
|
2094
2038
|
|
|
2095
2039
|
fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2133,8 +2077,14 @@ pub const Effects = struct {
|
|
|
2133
2077
|
}
|
|
2134
2078
|
|
|
2135
2079
|
// Body Slam can't paralyze a Normal type Pokémon
|
|
2136
|
-
|
|
2137
|
-
|
|
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;
|
|
2138
2088
|
|
|
2139
2089
|
foe_stored.status = Status.init(.PAR);
|
|
2140
2090
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
@@ -2177,7 +2127,16 @@ pub const Effects = struct {
|
|
|
2177
2127
|
|
|
2178
2128
|
if (move.effect == .Poison) {
|
|
2179
2129
|
if (!showdown and !try checkHit(battle, player, move, log)) return;
|
|
2180
|
-
} 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
|
+
}
|
|
2181
2140
|
|
|
2182
2141
|
foe_stored.status = Status.init(.PSN);
|
|
2183
2142
|
if (toxic) {
|
|
@@ -2191,8 +2150,10 @@ pub const Effects = struct {
|
|
|
2191
2150
|
|
|
2192
2151
|
fn rage(battle: anytype, player: Player) !void {
|
|
2193
2152
|
var volatiles = &battle.side(player).active.volatiles;
|
|
2194
|
-
|
|
2153
|
+
if (volatiles.Rage) return;
|
|
2195
2154
|
volatiles.Rage = true;
|
|
2155
|
+
assert(!volatiles.Bide);
|
|
2156
|
+
volatiles.state = 0;
|
|
2196
2157
|
}
|
|
2197
2158
|
|
|
2198
2159
|
fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
|
|
@@ -2244,7 +2205,16 @@ pub const Effects = struct {
|
|
|
2244
2205
|
}
|
|
2245
2206
|
foe.active.volatiles.Recharging = false;
|
|
2246
2207
|
|
|
2247
|
-
|
|
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);
|
|
2248
2218
|
try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
|
|
2249
2219
|
}
|
|
2250
2220
|
|
|
@@ -2297,7 +2267,11 @@ pub const Effects = struct {
|
|
|
2297
2267
|
assert(!volatiles.Bide);
|
|
2298
2268
|
|
|
2299
2269
|
volatiles.Thrashing = true;
|
|
2300
|
-
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);
|
|
2301
2275
|
}
|
|
2302
2276
|
|
|
2303
2277
|
fn transform(battle: anytype, player: Player, log: anytype) !void {
|
|
@@ -2329,7 +2303,7 @@ pub const Effects = struct {
|
|
|
2329
2303
|
try log.transform(battle.active(player), foe_ident);
|
|
2330
2304
|
}
|
|
2331
2305
|
|
|
2332
|
-
fn binding(battle: anytype, player: Player
|
|
2306
|
+
fn binding(battle: anytype, player: Player) void {
|
|
2333
2307
|
var side = battle.side(player);
|
|
2334
2308
|
var foe = battle.foe(player);
|
|
2335
2309
|
|
|
@@ -2337,11 +2311,9 @@ pub const Effects = struct {
|
|
|
2337
2311
|
side.active.volatiles.Binding = true;
|
|
2338
2312
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2339
2313
|
// (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);
|
|
2314
|
+
if (!showdown) foe.active.volatiles.Recharging = false;
|
|
2343
2315
|
|
|
2344
|
-
side.active.volatiles.attacks =
|
|
2316
|
+
side.active.volatiles.attacks = distribution(battle) - 1;
|
|
2345
2317
|
}
|
|
2346
2318
|
|
|
2347
2319
|
fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2368,7 +2340,7 @@ pub const Effects = struct {
|
|
|
2368
2340
|
return try log.fail(ident, .None);
|
|
2369
2341
|
}
|
|
2370
2342
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2371
|
-
const stat = unmodifiedStats(battle,
|
|
2343
|
+
const stat = unmodifiedStats(battle, player).atk;
|
|
2372
2344
|
stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2373
2345
|
try log.boost(ident, reason, n);
|
|
2374
2346
|
// Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
|
|
@@ -2388,7 +2360,7 @@ pub const Effects = struct {
|
|
|
2388
2360
|
return try log.fail(ident, .None);
|
|
2389
2361
|
}
|
|
2390
2362
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2391
|
-
const stat = unmodifiedStats(battle,
|
|
2363
|
+
const stat = unmodifiedStats(battle, player).def;
|
|
2392
2364
|
stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2393
2365
|
try log.boost(ident, .Defense, n);
|
|
2394
2366
|
},
|
|
@@ -2405,7 +2377,7 @@ pub const Effects = struct {
|
|
|
2405
2377
|
return try log.fail(ident, .None);
|
|
2406
2378
|
}
|
|
2407
2379
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2408
|
-
const stat = unmodifiedStats(battle,
|
|
2380
|
+
const stat = unmodifiedStats(battle, player).spe;
|
|
2409
2381
|
stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2410
2382
|
try log.boost(ident, .Speed, 2);
|
|
2411
2383
|
},
|
|
@@ -2425,7 +2397,7 @@ pub const Effects = struct {
|
|
|
2425
2397
|
return try log.fail(ident, .None);
|
|
2426
2398
|
}
|
|
2427
2399
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2428
|
-
const stat = unmodifiedStats(battle,
|
|
2400
|
+
const stat = unmodifiedStats(battle, player).spc;
|
|
2429
2401
|
stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2430
2402
|
try log.boost(ident, .SpecialAttack, n);
|
|
2431
2403
|
try log.boost(ident, .SpecialDefense, n);
|
|
@@ -2452,7 +2424,11 @@ pub const Effects = struct {
|
|
|
2452
2424
|
}
|
|
2453
2425
|
|
|
2454
2426
|
if (move.effect.isStatDownChance()) {
|
|
2455
|
-
if (
|
|
2427
|
+
const chance = if (showdown)
|
|
2428
|
+
battle.rng.chance(u8, 85, 256)
|
|
2429
|
+
else
|
|
2430
|
+
battle.rng.next() < Gen12.percent(33) + 1;
|
|
2431
|
+
if (!chance or foe.active.volatiles.Invulnerable) return;
|
|
2456
2432
|
} else if (!showdown and !try checkHit(battle, player, move, log)) {
|
|
2457
2433
|
return; // checkHit already checks for Invulnerable
|
|
2458
2434
|
}
|
|
@@ -2474,7 +2450,7 @@ pub const Effects = struct {
|
|
|
2474
2450
|
return try log.fail(foe_ident, .None);
|
|
2475
2451
|
}
|
|
2476
2452
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2477
|
-
const stat = unmodifiedStats(battle, foe).atk;
|
|
2453
|
+
const stat = unmodifiedStats(battle, player.foe()).atk;
|
|
2478
2454
|
stats.atk = @max(1, stat * mod[0] / mod[1]);
|
|
2479
2455
|
try log.boost(foe_ident, .Attack, -1);
|
|
2480
2456
|
},
|
|
@@ -2492,7 +2468,7 @@ pub const Effects = struct {
|
|
|
2492
2468
|
return try log.fail(foe_ident, .None);
|
|
2493
2469
|
}
|
|
2494
2470
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2495
|
-
const stat = unmodifiedStats(battle, foe).def;
|
|
2471
|
+
const stat = unmodifiedStats(battle, player.foe()).def;
|
|
2496
2472
|
stats.def = @max(1, stat * mod[0] / mod[1]);
|
|
2497
2473
|
try log.boost(foe_ident, .Defense, -@as(i8, n));
|
|
2498
2474
|
},
|
|
@@ -2509,7 +2485,7 @@ pub const Effects = struct {
|
|
|
2509
2485
|
return try log.fail(foe_ident, .None);
|
|
2510
2486
|
}
|
|
2511
2487
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2512
|
-
const stat = unmodifiedStats(battle, foe).spe;
|
|
2488
|
+
const stat = unmodifiedStats(battle, player.foe()).spe;
|
|
2513
2489
|
stats.spe = @max(1, stat * mod[0] / mod[1]);
|
|
2514
2490
|
try log.boost(foe_ident, .Speed, -1);
|
|
2515
2491
|
assert(boosts.spe >= -6);
|
|
@@ -2529,7 +2505,7 @@ pub const Effects = struct {
|
|
|
2529
2505
|
return try log.fail(foe_ident, .None);
|
|
2530
2506
|
}
|
|
2531
2507
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2532
|
-
const stat = unmodifiedStats(battle, foe).spc;
|
|
2508
|
+
const stat = unmodifiedStats(battle, player.foe()).spc;
|
|
2533
2509
|
stats.spc = @max(1, stat * mod[0] / mod[1]);
|
|
2534
2510
|
try log.boost(foe_ident, .SpecialAttack, -1);
|
|
2535
2511
|
try log.boost(foe_ident, .SpecialDefense, -1);
|
|
@@ -2548,7 +2524,8 @@ pub const Effects = struct {
|
|
|
2548
2524
|
}
|
|
2549
2525
|
};
|
|
2550
2526
|
|
|
2551
|
-
fn unmodifiedStats(battle: anytype,
|
|
2527
|
+
fn unmodifiedStats(battle: anytype, who: Player) *Stats(u16) {
|
|
2528
|
+
const side = battle.side(who);
|
|
2552
2529
|
if (!side.active.volatiles.Transform) return &side.stored().stats;
|
|
2553
2530
|
const id = ID.from(side.active.volatiles.transform);
|
|
2554
2531
|
return &battle.side(id.player).pokemon[id.id - 1].stats;
|
|
@@ -2614,140 +2591,80 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
|
|
|
2614
2591
|
}
|
|
2615
2592
|
}
|
|
2616
2593
|
|
|
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;
|
|
2594
|
+
fn disabled(side: *Side, ident: ID, log: anytype) !bool {
|
|
2595
|
+
if (side.active.volatiles.disabled_move != 0) {
|
|
2596
|
+
// A Pokémon that transforms after being disabled may end up with less move slots
|
|
2597
|
+
const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
|
|
2598
|
+
// side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
|
|
2599
|
+
const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
|
|
2600
|
+
m
|
|
2601
|
+
else
|
|
2602
|
+
side.last_selected_move;
|
|
2603
|
+
if (m != .None and m == last) {
|
|
2604
|
+
side.active.volatiles.Charging = false;
|
|
2605
|
+
try log.disabled(ident, last);
|
|
2606
|
+
return true;
|
|
2633
2607
|
}
|
|
2634
2608
|
}
|
|
2609
|
+
return false;
|
|
2610
|
+
}
|
|
2635
2611
|
|
|
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);
|
|
2612
|
+
inline fn buildRage(battle: anytype, who: Player, log: anytype) !void {
|
|
2613
|
+
const side = battle.side(who);
|
|
2614
|
+
if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
|
|
2615
|
+
try Effects.boost(battle, who, Move.get(.Rage), log);
|
|
2654
2616
|
}
|
|
2617
|
+
}
|
|
2655
2618
|
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2619
|
+
fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
|
|
2620
|
+
var volatiles = &active.volatiles;
|
|
2621
|
+
assert(volatiles.Thrashing);
|
|
2622
|
+
if (volatiles.attacks > 0) return false;
|
|
2660
2623
|
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2624
|
+
volatiles.Thrashing = false;
|
|
2625
|
+
volatiles.Confusion = true;
|
|
2626
|
+
volatiles.confusion = @intCast(u3, if (showdown)
|
|
2627
|
+
battle.rng.range(u8, 2, 6)
|
|
2628
|
+
else
|
|
2629
|
+
(battle.rng.next() & 3) + 2);
|
|
2665
2630
|
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
return battle.rng.next() < 1 + (if (low) Gen12.percent(20) else Gen12.percent(40));
|
|
2669
|
-
}
|
|
2631
|
+
return true;
|
|
2632
|
+
}
|
|
2670
2633
|
|
|
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
|
-
}
|
|
2634
|
+
const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
|
|
2678
2635
|
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2636
|
+
fn distribution(battle: anytype) u3 {
|
|
2637
|
+
if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
|
|
2638
|
+
const r = (battle.rng.next() & 3);
|
|
2639
|
+
return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
|
|
2640
|
+
}
|
|
2683
2641
|
|
|
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
|
-
}
|
|
2642
|
+
fn randomMoveSlot(rand: anytype, moves: []MoveSlot, check_pp: u4) u4 {
|
|
2643
|
+
if (showdown) {
|
|
2644
|
+
if (check_pp == 0) {
|
|
2645
|
+
var i: usize = moves.len;
|
|
2646
|
+
while (i > 0) {
|
|
2647
|
+
i -= 1;
|
|
2648
|
+
if (moves[i].id != .None) return rand.range(u4, 0, @intCast(u4, i + 1)) + 1;
|
|
2649
|
+
}
|
|
2650
|
+
} else {
|
|
2651
|
+
var r = rand.range(u4, 0, @intCast(u4, check_pp)) + 1;
|
|
2652
|
+
var i: usize = 0;
|
|
2653
|
+
while (i < moves.len and r > 0) : (i += 1) {
|
|
2654
|
+
if (moves[i].pp > 0) {
|
|
2655
|
+
r -= 1;
|
|
2656
|
+
if (r == 0) break;
|
|
2702
2657
|
}
|
|
2703
|
-
return @intCast(u4, i + 1);
|
|
2704
2658
|
}
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
while (true) {
|
|
2708
|
-
const r = @intCast(u4, battle.rng.next() & 3);
|
|
2709
|
-
if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
|
|
2710
|
-
}
|
|
2711
|
-
}
|
|
2712
|
-
|
|
2713
|
-
inline fn disableDuration(battle: anytype) u4 {
|
|
2714
|
-
if (showdown) return @intCast(u4, battle.rng.range(u8, 1, 9));
|
|
2715
|
-
return @intCast(u4, (battle.rng.next() & 7) + 1);
|
|
2716
|
-
}
|
|
2717
|
-
|
|
2718
|
-
inline fn metronome(battle: anytype) Move {
|
|
2719
|
-
if (showdown) {
|
|
2720
|
-
const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
|
|
2721
|
-
const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
|
|
2722
|
-
return @intToEnum(Move, r + mod);
|
|
2723
|
-
}
|
|
2724
|
-
while (true) {
|
|
2725
|
-
const r = battle.rng.next();
|
|
2726
|
-
if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
|
|
2727
|
-
if (r >= @enumToInt(Move.Struggle)) continue;
|
|
2728
|
-
return @intToEnum(Move, r);
|
|
2729
|
-
}
|
|
2730
|
-
}
|
|
2731
|
-
|
|
2732
|
-
inline fn psywave(battle: anytype, max: u8) u8 {
|
|
2733
|
-
if (showdown) return battle.rng.range(u8, 0, max);
|
|
2734
|
-
while (true) {
|
|
2735
|
-
const r = battle.rng.next();
|
|
2736
|
-
if (r < max) return r;
|
|
2659
|
+
return @intCast(u4, i + 1);
|
|
2737
2660
|
}
|
|
2738
2661
|
}
|
|
2739
2662
|
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
|
|
2663
|
+
while (true) {
|
|
2664
|
+
const r = @intCast(u4, rand.next() & 3);
|
|
2665
|
+
if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
|
|
2744
2666
|
}
|
|
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
|
-
};
|
|
2667
|
+
}
|
|
2751
2668
|
|
|
2752
2669
|
test "RNG agreement" {
|
|
2753
2670
|
if (!showdown) return;
|
|
@@ -2847,7 +2764,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
2847
2764
|
while (slot <= 4) : (slot += 1) {
|
|
2848
2765
|
const m = active.moves[slot - 1];
|
|
2849
2766
|
if (m.id == .None) break;
|
|
2850
|
-
if (m.id ==
|
|
2767
|
+
if (m.id == side.last_selected_move) {
|
|
2851
2768
|
// Pokémon Showdown displays Struggle if limited to Bide but unable to pick
|
|
2852
2769
|
const struggle =
|
|
2853
2770
|
m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);
|