@pkmn/engine 0.1.0-dev.3a273268 → 0.1.0-dev.4005e495
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 +18 -18
- package/build/pkg/addon.d.ts +1 -1
- package/build/pkg/addon.js +4 -4
- package/build/pkg/addon.js.map +1 -1
- package/build/pkg/data/layout.json +1 -1
- package/build/pkg/gen1.d.ts +5 -4
- package/build/pkg/gen1.js +42 -23
- package/build/pkg/gen1.js.map +1 -1
- package/build/pkg/index.d.ts +15 -12
- package/build/pkg/index.js.map +1 -1
- package/build/test/display.js +31 -13
- package/build/test/display.js.map +1 -1
- package/build.zig +15 -22
- package/package.json +10 -9
- package/src/bin/install-pkmn-engine +3 -3
- package/src/lib/binding/node.zig +3 -3
- package/src/lib/binding/wasm.zig +1 -1
- package/src/lib/common/debug.zig +1 -0
- package/src/lib/common/options.zig +3 -3
- package/src/lib/common/protocol.zig +38 -38
- package/src/lib/gen1/README.md +90 -69
- package/src/lib/gen1/data.zig +14 -12
- package/src/lib/gen1/helpers.zig +3 -14
- package/src/lib/gen1/mechanics.zig +162 -83
- package/src/lib/pkmn.zig +4 -4
|
@@ -62,26 +62,28 @@ pub fn update(battle: anytype, c1: Choice, c2: Choice, log: anytype) !Result {
|
|
|
62
62
|
assert(c1.type != .Pass or c2.type != .Pass or battle.turn == 0);
|
|
63
63
|
if (battle.turn == 0) return start(battle, log);
|
|
64
64
|
|
|
65
|
+
var s1 = false;
|
|
66
|
+
var s2 = false;
|
|
65
67
|
var f1: ?Move = null;
|
|
66
68
|
var f2: ?Move = null;
|
|
67
69
|
|
|
68
|
-
if (selectMove(battle, .P1, c1, c2, &f1)) |r| return r;
|
|
69
|
-
if (selectMove(battle, .P2, c2, c1, &f2)) |r| return r;
|
|
70
|
+
if (selectMove(battle, .P1, c1, c2, &s1, &f1)) |r| return r;
|
|
71
|
+
if (selectMove(battle, .P2, c2, c1, &s2, &f2)) |r| return r;
|
|
72
|
+
|
|
73
|
+
var p1 = battle.side(.P1);
|
|
74
|
+
var p2 = battle.side(.P2);
|
|
75
|
+
|
|
76
|
+
var r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
|
|
77
|
+
var r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
|
|
70
78
|
|
|
71
79
|
if (turnOrder(battle, c1, c2) == .P1) {
|
|
72
|
-
if (try doTurn(battle, .P1, c1, f1, .P2, c2, f2, log)) |r| return r;
|
|
80
|
+
if (try doTurn(battle, .P1, c1, r1, s1, f1, .P2, c2, r2, s2, f2, log)) |r| return r;
|
|
73
81
|
} else {
|
|
74
|
-
if (try doTurn(battle, .P2, c2, f2, .P1, c1, f1, log)) |r| return r;
|
|
82
|
+
if (try doTurn(battle, .P2, c2, r2, s2, f2, .P1, c1, r1, s1, f1, log)) |r| return r;
|
|
75
83
|
}
|
|
76
84
|
|
|
77
|
-
|
|
78
|
-
if (
|
|
79
|
-
p1.active.volatiles.Binding = false;
|
|
80
|
-
}
|
|
81
|
-
var p2 = battle.side(.P2);
|
|
82
|
-
if (p2.active.volatiles.attacks == 0) {
|
|
83
|
-
p2.active.volatiles.Binding = false;
|
|
84
|
-
}
|
|
85
|
+
if (p1.active.volatiles.attacks == 0) p1.active.volatiles.Binding = false;
|
|
86
|
+
if (p2.active.volatiles.attacks == 0) p2.active.volatiles.Binding = false;
|
|
85
87
|
|
|
86
88
|
return endTurn(battle, log);
|
|
87
89
|
}
|
|
@@ -114,6 +116,7 @@ fn selectMove(
|
|
|
114
116
|
player: Player,
|
|
115
117
|
choice: Choice,
|
|
116
118
|
foe_choice: Choice,
|
|
119
|
+
skip_turn: *bool,
|
|
117
120
|
from: *?Move,
|
|
118
121
|
) ?Result {
|
|
119
122
|
if (choice.type == .Pass) return null;
|
|
@@ -126,17 +129,26 @@ fn selectMove(
|
|
|
126
129
|
(choice.type == .Move and choice.data == @boolToInt(showdown)));
|
|
127
130
|
|
|
128
131
|
// pre-battle menu
|
|
129
|
-
if (volatiles.Recharging)
|
|
132
|
+
if (volatiles.Recharging) {
|
|
133
|
+
if (showdown and battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
130
136
|
if (volatiles.Rage) {
|
|
131
137
|
from.* = side.last_used_move;
|
|
132
|
-
if (showdown)
|
|
138
|
+
if (showdown) {
|
|
139
|
+
if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
|
|
140
|
+
saveMove(battle, player, null);
|
|
141
|
+
}
|
|
133
142
|
return null;
|
|
134
143
|
}
|
|
135
144
|
// Pokémon Showdown removes Flinch at the end-of-turn in its residual handler
|
|
136
145
|
if (!showdown) volatiles.Flinch = false;
|
|
137
146
|
if (volatiles.Thrashing or volatiles.Charging) {
|
|
138
147
|
from.* = side.last_selected_move;
|
|
139
|
-
if (showdown)
|
|
148
|
+
if (showdown) {
|
|
149
|
+
if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
|
|
150
|
+
saveMove(battle, player, null);
|
|
151
|
+
}
|
|
140
152
|
return null;
|
|
141
153
|
}
|
|
142
154
|
|
|
@@ -146,7 +158,10 @@ fn selectMove(
|
|
|
146
158
|
// pre-move select
|
|
147
159
|
if (Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP) or volatiles.Bide) {
|
|
148
160
|
assert(showdown or choice.data == 0);
|
|
149
|
-
if (showdown)
|
|
161
|
+
if (showdown) {
|
|
162
|
+
if (volatiles.Bide and battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
|
|
163
|
+
saveMove(battle, player, choice);
|
|
164
|
+
}
|
|
150
165
|
return null;
|
|
151
166
|
}
|
|
152
167
|
if (volatiles.Binding) {
|
|
@@ -165,9 +180,9 @@ fn selectMove(
|
|
|
165
180
|
// GLITCH: https://glitchcity.wiki/Partial_trapping_move_Mirror_Move_link_battle_glitch
|
|
166
181
|
if (foe_choice.type == .Switch) {
|
|
167
182
|
const slot = if (player == .P1)
|
|
168
|
-
battle.
|
|
183
|
+
battle.last_moves.p1_index
|
|
169
184
|
else
|
|
170
|
-
battle.
|
|
185
|
+
battle.last_moves.p2_index;
|
|
171
186
|
const last = side.active.move(@intCast(u4, slot));
|
|
172
187
|
if (last.id == .Metronome) side.last_selected_move = last.id;
|
|
173
188
|
if (last.id == .MirrorMove) return Result.Error;
|
|
@@ -177,6 +192,7 @@ fn selectMove(
|
|
|
177
192
|
}
|
|
178
193
|
|
|
179
194
|
if (battle.foe(player).active.volatiles.Binding) {
|
|
195
|
+
skip_turn.* = true;
|
|
180
196
|
if (showdown) {
|
|
181
197
|
saveMove(battle, player, choice);
|
|
182
198
|
} else {
|
|
@@ -221,9 +237,9 @@ fn saveMove(battle: anytype, player: Player, choice: ?Choice) void {
|
|
|
221
237
|
|
|
222
238
|
side.last_selected_move = move.id;
|
|
223
239
|
if (player == .P1) {
|
|
224
|
-
battle.
|
|
240
|
+
battle.last_moves.p1_index = @intCast(u3, c.data);
|
|
225
241
|
} else {
|
|
226
|
-
battle.
|
|
242
|
+
battle.last_moves.p2_index = @intCast(u3, c.data);
|
|
227
243
|
}
|
|
228
244
|
}
|
|
229
245
|
}
|
|
@@ -243,9 +259,9 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, log: anyty
|
|
|
243
259
|
side.order[slot - 1] = out;
|
|
244
260
|
|
|
245
261
|
if (player == .P1) {
|
|
246
|
-
battle.
|
|
262
|
+
battle.last_moves.p1_index = 1;
|
|
247
263
|
} else {
|
|
248
|
-
battle.
|
|
264
|
+
battle.last_moves.p2_index = 1;
|
|
249
265
|
}
|
|
250
266
|
|
|
251
267
|
side.last_used_move = .None;
|
|
@@ -302,7 +318,9 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
|
|
|
302
318
|
const spe2 = battle.side(.P2).active.stats.spe;
|
|
303
319
|
if (spe1 == spe2) {
|
|
304
320
|
// Pokémon Showdown's beforeTurnCallback shenanigans
|
|
305
|
-
if (showdown and m1 == .Counter and m2 == .Counter)
|
|
321
|
+
if (showdown and !double_switch and m1 == .Counter and m2 == .Counter) {
|
|
322
|
+
battle.rng.advance(1);
|
|
323
|
+
}
|
|
306
324
|
|
|
307
325
|
const p1 = Rolls.speedTie(battle);
|
|
308
326
|
if (!showdown) return if (p1) .P1 else .P2;
|
|
@@ -327,9 +345,13 @@ fn doTurn(
|
|
|
327
345
|
battle: anytype,
|
|
328
346
|
player: Player,
|
|
329
347
|
player_choice: Choice,
|
|
348
|
+
player_rewrap: bool,
|
|
349
|
+
player_skip: bool,
|
|
330
350
|
player_from: ?Move,
|
|
331
351
|
foe_player: Player,
|
|
332
352
|
foe_choice: Choice,
|
|
353
|
+
foe_rewrap: bool,
|
|
354
|
+
foe_skip: bool,
|
|
333
355
|
foe_from: ?Move,
|
|
334
356
|
log: anytype,
|
|
335
357
|
) !?Result {
|
|
@@ -337,7 +359,16 @@ fn doTurn(
|
|
|
337
359
|
|
|
338
360
|
var residual = true;
|
|
339
361
|
var replace = battle.side(player).stored().hp == 0;
|
|
340
|
-
if (try executeMove(
|
|
362
|
+
if (try executeMove(
|
|
363
|
+
battle,
|
|
364
|
+
player,
|
|
365
|
+
player_choice,
|
|
366
|
+
player_rewrap,
|
|
367
|
+
player_skip,
|
|
368
|
+
player_from,
|
|
369
|
+
&residual,
|
|
370
|
+
log,
|
|
371
|
+
)) |r| return r;
|
|
341
372
|
if (!replace) {
|
|
342
373
|
if (player_choice.type != .Switch) {
|
|
343
374
|
if (try checkFaint(battle, foe_player, log)) |r| return r;
|
|
@@ -348,7 +379,16 @@ fn doTurn(
|
|
|
348
379
|
|
|
349
380
|
residual = true;
|
|
350
381
|
replace = battle.side(foe_player).stored().hp == 0;
|
|
351
|
-
if (try executeMove(
|
|
382
|
+
if (try executeMove(
|
|
383
|
+
battle,
|
|
384
|
+
foe_player,
|
|
385
|
+
foe_choice,
|
|
386
|
+
foe_rewrap,
|
|
387
|
+
foe_skip,
|
|
388
|
+
foe_from,
|
|
389
|
+
&residual,
|
|
390
|
+
log,
|
|
391
|
+
)) |r| return r;
|
|
352
392
|
if (!replace) {
|
|
353
393
|
if (foe_choice.type != .Switch) {
|
|
354
394
|
if (try checkFaint(battle, player, log)) |r| return r;
|
|
@@ -371,6 +411,8 @@ fn executeMove(
|
|
|
371
411
|
battle: anytype,
|
|
372
412
|
player: Player,
|
|
373
413
|
choice: Choice,
|
|
414
|
+
rewrap: bool,
|
|
415
|
+
skip: bool,
|
|
374
416
|
from: ?Move,
|
|
375
417
|
residual: *bool,
|
|
376
418
|
log: anytype,
|
|
@@ -382,9 +424,10 @@ fn executeMove(
|
|
|
382
424
|
return null;
|
|
383
425
|
}
|
|
384
426
|
|
|
427
|
+
// This is the correct place to check for SKIP_TURN and abort early, however since Pokémon
|
|
428
|
+
// Showdown overwrites the SKIP_TURN sentinel with its botched move select we need to add an
|
|
429
|
+
// additional skip boolean to accomplish the same thing in the Binding check of BeforeMove
|
|
385
430
|
if (side.last_selected_move == .SKIP_TURN) {
|
|
386
|
-
// Pokémon Showdown overwrites the SKIP_TURN sentinel with its botched move select,
|
|
387
|
-
// Binding instead gets handled in beforeMove after sleep and freeze
|
|
388
431
|
assert(!showdown);
|
|
389
432
|
if (battle.foe(player).active.volatiles.Binding) {
|
|
390
433
|
try log.cant(battle.active(player), .Bound);
|
|
@@ -403,9 +446,9 @@ fn executeMove(
|
|
|
403
446
|
// choice.data == 0 only happens with Struggle on Pokémon Showdown
|
|
404
447
|
assert(!showdown);
|
|
405
448
|
mslot = @intCast(u4, if (player == .P1)
|
|
406
|
-
battle.
|
|
449
|
+
battle.last_moves.p1_index
|
|
407
450
|
else
|
|
408
|
-
battle.
|
|
451
|
+
battle.last_moves.p2_index);
|
|
409
452
|
const stored = side.stored();
|
|
410
453
|
// GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
|
|
411
454
|
auto = side.last_selected_move == .HyperBeam or side.active.volatiles.Bide or
|
|
@@ -427,14 +470,14 @@ fn executeMove(
|
|
|
427
470
|
// Incorrect mslot with Pokémon Showdown choice semantics so we need to recover from index
|
|
428
471
|
assert(mslot == 1);
|
|
429
472
|
mslot = @intCast(u4, if (player == .P1)
|
|
430
|
-
battle.
|
|
473
|
+
battle.last_moves.p1_index
|
|
431
474
|
else
|
|
432
|
-
battle.
|
|
475
|
+
battle.last_moves.p2_index);
|
|
433
476
|
}
|
|
434
477
|
|
|
435
478
|
var skip_can = false;
|
|
436
479
|
var skip_pp = false;
|
|
437
|
-
switch (try beforeMove(battle, player, from, residual, log)) {
|
|
480
|
+
switch (try beforeMove(battle, player, skip, from, residual, log)) {
|
|
438
481
|
.done => return null,
|
|
439
482
|
.skip_can => skip_can = true,
|
|
440
483
|
.skip_pp => skip_pp = true,
|
|
@@ -446,14 +489,15 @@ fn executeMove(
|
|
|
446
489
|
return null;
|
|
447
490
|
}
|
|
448
491
|
|
|
449
|
-
return doMove(battle, player, mslot, auto, residual, log);
|
|
492
|
+
return doMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
450
493
|
}
|
|
451
494
|
|
|
452
|
-
const BeforeMove =
|
|
495
|
+
const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
|
|
453
496
|
|
|
454
497
|
fn beforeMove(
|
|
455
498
|
battle: anytype,
|
|
456
499
|
player: Player,
|
|
500
|
+
skip: bool,
|
|
457
501
|
from: ?Move,
|
|
458
502
|
residual: *bool,
|
|
459
503
|
log: anytype,
|
|
@@ -485,13 +529,15 @@ fn beforeMove(
|
|
|
485
529
|
return .done;
|
|
486
530
|
}
|
|
487
531
|
|
|
488
|
-
if (foe.active.volatiles.Binding) {
|
|
532
|
+
if (skip or foe.active.volatiles.Binding) {
|
|
489
533
|
try log.cant(ident, .Bound);
|
|
490
534
|
return .done;
|
|
491
535
|
}
|
|
492
536
|
|
|
493
|
-
if (
|
|
494
|
-
|
|
537
|
+
if (volatiles.Flinch) {
|
|
538
|
+
// Pokémon Showdown doesn't clear Flinch until its imaginary "residual" phase, meaning
|
|
539
|
+
// Pokémon can sometimes flinch multiple times from the same original hit
|
|
540
|
+
if (!showdown) volatiles.Flinch = false;
|
|
495
541
|
try log.cant(ident, .Flinch);
|
|
496
542
|
return .done;
|
|
497
543
|
}
|
|
@@ -512,13 +558,6 @@ fn beforeMove(
|
|
|
512
558
|
// Pokémon Showdown's disable condition has a single onBeforeMove handler
|
|
513
559
|
if (showdown and try disabled(side, ident, log)) return .done;
|
|
514
560
|
|
|
515
|
-
// Pokémon Showdown checks for Flinch *after* instead of before
|
|
516
|
-
if (showdown and volatiles.Flinch) {
|
|
517
|
-
volatiles.Flinch = false;
|
|
518
|
-
try log.cant(ident, .Flinch);
|
|
519
|
-
return .done;
|
|
520
|
-
}
|
|
521
|
-
|
|
522
561
|
// This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
|
|
523
562
|
// before the side had a selected a move - we simply need to assume this leads to a desync
|
|
524
563
|
if (side.last_selected_move == .None) {
|
|
@@ -590,7 +629,14 @@ fn beforeMove(
|
|
|
590
629
|
if (volatiles.Bide) {
|
|
591
630
|
assert(!volatiles.Thrashing and !volatiles.Rage);
|
|
592
631
|
|
|
593
|
-
|
|
632
|
+
if (showdown) {
|
|
633
|
+
// Pokémon Showdown doesn't implement Bide potentially overflowing in the event of
|
|
634
|
+
// OHKO-move damage, but we can fake this incorrect behavior by simply saturating the
|
|
635
|
+
// addition because 65535 is sufficient to faint any Pokémon anyway
|
|
636
|
+
volatiles.state +|= battle.last_damage;
|
|
637
|
+
} else {
|
|
638
|
+
volatiles.state +%= battle.last_damage;
|
|
639
|
+
}
|
|
594
640
|
|
|
595
641
|
assert(volatiles.attacks > 0);
|
|
596
642
|
|
|
@@ -634,8 +680,13 @@ fn beforeMove(
|
|
|
634
680
|
try log.start(battle.active(player), .ConfusionSilent);
|
|
635
681
|
}
|
|
636
682
|
try log.move(ident, side.last_selected_move, battle.active(player.foe()), from);
|
|
637
|
-
if (showdown
|
|
638
|
-
|
|
683
|
+
if (showdown) {
|
|
684
|
+
// This shouldn't actually set last_used_move, but Pokémon Showdown sets last
|
|
685
|
+
// used in useMove and doesn't have the notion of skipping canMove semantics
|
|
686
|
+
side.last_used_move = side.last_selected_move;
|
|
687
|
+
if (handleThrashing(battle, active)) {
|
|
688
|
+
try log.start(battle.active(player), .ConfusionSilent);
|
|
689
|
+
}
|
|
639
690
|
}
|
|
640
691
|
return .skip_can;
|
|
641
692
|
}
|
|
@@ -650,7 +701,11 @@ fn beforeMove(
|
|
|
650
701
|
side.last_selected_move,
|
|
651
702
|
);
|
|
652
703
|
if (showdown or battle.last_damage != 0) {
|
|
704
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
653
705
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
706
|
+
if (battle.foe(player).stored().hp > 0 and !sub) {
|
|
707
|
+
try buildRage(battle, player.foe(), log);
|
|
708
|
+
}
|
|
654
709
|
}
|
|
655
710
|
return .done;
|
|
656
711
|
}
|
|
@@ -682,6 +737,7 @@ fn canMove(
|
|
|
682
737
|
side.active.volatiles.Invulnerable = false;
|
|
683
738
|
} else if (move.effect == .Charge) {
|
|
684
739
|
try log.move(player_ident, side.last_selected_move, .{}, from);
|
|
740
|
+
setCounterable(battle, player, side, move);
|
|
685
741
|
try Effects.charge(battle, player, log);
|
|
686
742
|
return false;
|
|
687
743
|
}
|
|
@@ -698,6 +754,7 @@ fn canMove(
|
|
|
698
754
|
// regrettable, but this information is more "nice to have" than required
|
|
699
755
|
const f = if (from != null and from.? == .None) null else from;
|
|
700
756
|
try log.move(player_ident, side.last_selected_move, battle.active(target), f);
|
|
757
|
+
setCounterable(battle, player, side, move);
|
|
701
758
|
|
|
702
759
|
if (move.effect.onBegin()) {
|
|
703
760
|
try onBegin(battle, player, move, mslot, residual, log);
|
|
@@ -708,12 +765,27 @@ fn canMove(
|
|
|
708
765
|
Effects.thrashing(battle, player);
|
|
709
766
|
} else if (!showdown and move.effect == .Binding) {
|
|
710
767
|
// Pokémon Showdown handles this after hit/miss checks and damage calculation
|
|
711
|
-
Effects.binding(battle, player);
|
|
768
|
+
Effects.binding(battle, player, false);
|
|
712
769
|
}
|
|
713
770
|
|
|
714
771
|
return true;
|
|
715
772
|
}
|
|
716
773
|
|
|
774
|
+
fn setCounterable(battle: anytype, player: Player, side: *Side, move: Move.Data) void {
|
|
775
|
+
// The Counter desync is caused by the cartridge not calling GetCurrentMove until now, meaning
|
|
776
|
+
// in cases where an early return happens the data for a players last selected move does not get
|
|
777
|
+
// reloaded and HandleCounterMove actually bases its success/failure off of stale information.
|
|
778
|
+
// This boolean state we track here doesn't exist on the cartridge because it instead manifests
|
|
779
|
+
// as the desync results from actually having two separate battle states that subtly disagree
|
|
780
|
+
const counterable = side.last_selected_move != .Counter and move.bp > 0 and
|
|
781
|
+
(move.type == .Normal or move.type == .Fighting);
|
|
782
|
+
if (player == .P1) {
|
|
783
|
+
battle.last_moves.p1_counterable = @boolToInt(counterable);
|
|
784
|
+
} else {
|
|
785
|
+
battle.last_moves.p2_counterable = @boolToInt(counterable);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
717
789
|
fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
|
|
718
790
|
if (side.last_selected_move == .Struggle) return;
|
|
719
791
|
|
|
@@ -749,6 +821,7 @@ fn doMove(
|
|
|
749
821
|
battle: anytype,
|
|
750
822
|
player: Player,
|
|
751
823
|
mslot: u4,
|
|
824
|
+
rewrap: bool,
|
|
752
825
|
auto: bool,
|
|
753
826
|
residual: *bool,
|
|
754
827
|
log: anytype,
|
|
@@ -840,9 +913,9 @@ fn doMove(
|
|
|
840
913
|
|
|
841
914
|
if (!showdown or !miss) {
|
|
842
915
|
if (move.effect == .MirrorMove) {
|
|
843
|
-
return mirrorMove(battle, player, mslot, auto, residual, log);
|
|
916
|
+
return mirrorMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
844
917
|
} else if (move.effect == .Metronome) {
|
|
845
|
-
return metronome(battle, player, mslot, auto, residual, log);
|
|
918
|
+
return metronome(battle, player, mslot, rewrap, auto, residual, log);
|
|
846
919
|
} else if (move.effect.onEnd()) {
|
|
847
920
|
try onEnd(battle, player, move, log);
|
|
848
921
|
return null;
|
|
@@ -857,7 +930,7 @@ fn doMove(
|
|
|
857
930
|
move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
|
|
858
931
|
if (ohko) {
|
|
859
932
|
try log.immune(foe_ident, .OHKO);
|
|
860
|
-
} else if (immune and !invulnerable) {
|
|
933
|
+
} else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
|
|
861
934
|
try log.immune(foe_ident, .None);
|
|
862
935
|
} else if (mist) {
|
|
863
936
|
if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
|
|
@@ -923,11 +996,17 @@ fn doMove(
|
|
|
923
996
|
} else if (showdown) {
|
|
924
997
|
// This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
|
|
925
998
|
if (move.effect == .Binding) {
|
|
926
|
-
Effects.binding(battle, player);
|
|
999
|
+
Effects.binding(battle, player, rewrap);
|
|
927
1000
|
if (immune) {
|
|
928
1001
|
battle.last_damage = 0;
|
|
1002
|
+
assert(foe.stored().hp > 0);
|
|
929
1003
|
// Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
|
|
930
|
-
|
|
1004
|
+
if (sub) {
|
|
1005
|
+
try log.activate(battle.active(player.foe()), .Substitute);
|
|
1006
|
+
} else {
|
|
1007
|
+
try log.damage(battle.active(player.foe()), foe.stored(), .None);
|
|
1008
|
+
try buildRage(battle, player.foe(), log);
|
|
1009
|
+
}
|
|
931
1010
|
return null;
|
|
932
1011
|
}
|
|
933
1012
|
}
|
|
@@ -1124,19 +1203,17 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
|
|
|
1124
1203
|
return null;
|
|
1125
1204
|
}
|
|
1126
1205
|
|
|
1127
|
-
// Pretend
|
|
1128
|
-
const foe_last_used_move =
|
|
1129
|
-
Move.get(if (foe.last_used_move == .None) .Counter else foe.last_used_move);
|
|
1206
|
+
// Pretend Splash was used as a stand-in when no move has been used to fail below with 0 BP
|
|
1130
1207
|
const foe_last_selected_move =
|
|
1131
1208
|
Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
|
|
1132
|
-
.
|
|
1209
|
+
.Splash
|
|
1133
1210
|
else
|
|
1134
1211
|
foe.last_selected_move);
|
|
1135
1212
|
|
|
1136
|
-
const used =
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1213
|
+
const used = (if (player.foe() == .P1)
|
|
1214
|
+
battle.last_moves.p1_counterable
|
|
1215
|
+
else
|
|
1216
|
+
battle.last_moves.p2_counterable) != 0;
|
|
1140
1217
|
|
|
1141
1218
|
const selected = foe_last_selected_move.bp > 0 and
|
|
1142
1219
|
foe.last_selected_move != .Counter and
|
|
@@ -1155,12 +1232,14 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
|
|
|
1155
1232
|
return null;
|
|
1156
1233
|
}
|
|
1157
1234
|
|
|
1158
|
-
battle.last_damage
|
|
1235
|
+
battle.last_damage *|= 2;
|
|
1159
1236
|
|
|
1160
1237
|
// Pokémon Showdown calls checkHit before Counter
|
|
1161
1238
|
if (!showdown and !try checkHit(battle, player, move, log)) return null;
|
|
1162
1239
|
|
|
1240
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1163
1241
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
1242
|
+
if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
|
|
1164
1243
|
return null;
|
|
1165
1244
|
}
|
|
1166
1245
|
|
|
@@ -1204,6 +1283,7 @@ fn mirrorMove(
|
|
|
1204
1283
|
battle: anytype,
|
|
1205
1284
|
player: Player,
|
|
1206
1285
|
mslot: u4,
|
|
1286
|
+
rewrap: bool,
|
|
1207
1287
|
auto: bool,
|
|
1208
1288
|
residual: *bool,
|
|
1209
1289
|
log: anytype,
|
|
@@ -1216,18 +1296,19 @@ fn mirrorMove(
|
|
|
1216
1296
|
if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
|
|
1217
1297
|
try log.fail(battle.active(player), .None);
|
|
1218
1298
|
return null;
|
|
1299
|
+
} else if (!showdown or foe.last_used_move != .Struggle) {
|
|
1300
|
+
incrementPP(side, mslot);
|
|
1219
1301
|
}
|
|
1220
1302
|
|
|
1221
|
-
incrementPP(side, mslot);
|
|
1222
|
-
|
|
1223
1303
|
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
|
|
1224
|
-
return doMove(battle, player, mslot, auto, residual, log);
|
|
1304
|
+
return doMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
1225
1305
|
}
|
|
1226
1306
|
|
|
1227
1307
|
fn metronome(
|
|
1228
1308
|
battle: anytype,
|
|
1229
1309
|
player: Player,
|
|
1230
1310
|
mslot: u4,
|
|
1311
|
+
rewrap: bool,
|
|
1231
1312
|
auto: bool,
|
|
1232
1313
|
residual: *bool,
|
|
1233
1314
|
log: anytype,
|
|
@@ -1238,7 +1319,7 @@ fn metronome(
|
|
|
1238
1319
|
incrementPP(side, mslot);
|
|
1239
1320
|
|
|
1240
1321
|
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
|
|
1241
|
-
return doMove(battle, player, mslot, auto, residual, log);
|
|
1322
|
+
return doMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
1242
1323
|
}
|
|
1243
1324
|
|
|
1244
1325
|
fn checkHit(battle: anytype, player: Player, move: Move.Data, log: anytype) !bool {
|
|
@@ -1445,11 +1526,7 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
|
|
|
1445
1526
|
stored.hp -= amount;
|
|
1446
1527
|
|
|
1447
1528
|
// As above, Pokémon Showdown uses damageOf but its not relevant
|
|
1448
|
-
if (amount > 0)
|
|
1449
|
-
try log.damage(ident, stored, .LeechSeed);
|
|
1450
|
-
// Pokémon Showdown erroneously updates last damage with uncapped Leech Seed damage
|
|
1451
|
-
if (showdown) battle.last_damage = damage;
|
|
1452
|
-
}
|
|
1529
|
+
if (amount > 0) try log.damage(ident, stored, .LeechSeed);
|
|
1453
1530
|
|
|
1454
1531
|
const before = foe_stored.hp;
|
|
1455
1532
|
// Uncapped damage is added back to the foe
|
|
@@ -1463,13 +1540,13 @@ fn endTurn(battle: anytype, log: anytype) @TypeOf(log).Error!Result {
|
|
|
1463
1540
|
assert(!battle.side(.P1).active.volatiles.MultiHit);
|
|
1464
1541
|
assert(!battle.side(.P2).active.volatiles.MultiHit);
|
|
1465
1542
|
|
|
1543
|
+
battle.turn += 1;
|
|
1544
|
+
|
|
1466
1545
|
if (showdown and options.ebc and checkEBC(battle)) {
|
|
1467
1546
|
try log.tie();
|
|
1468
1547
|
return Result.Tie;
|
|
1469
1548
|
}
|
|
1470
1549
|
|
|
1471
|
-
battle.turn += 1;
|
|
1472
|
-
|
|
1473
1550
|
if (showdown and battle.turn >= 1000) {
|
|
1474
1551
|
try log.tie();
|
|
1475
1552
|
return Result.Tie;
|
|
@@ -1662,7 +1739,7 @@ pub const Effects = struct {
|
|
|
1662
1739
|
side.active.volatiles.Bide = true;
|
|
1663
1740
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1664
1741
|
side.active.volatiles.state = 0;
|
|
1665
|
-
side.active.volatiles.attacks = Rolls.
|
|
1742
|
+
side.active.volatiles.attacks = Rolls.bideThrashDuration(battle);
|
|
1666
1743
|
|
|
1667
1744
|
try log.start(battle.active(player), .Bide);
|
|
1668
1745
|
}
|
|
@@ -1982,9 +2059,9 @@ pub const Effects = struct {
|
|
|
1982
2059
|
}
|
|
1983
2060
|
break :has_mimic false;
|
|
1984
2061
|
};
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
2062
|
+
if (!has_mimic) {
|
|
2063
|
+
// Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
|
|
2064
|
+
if (!try checkHit(battle, player, move, log)) return;
|
|
1988
2065
|
return try log.fail(battle.active(player.foe()), .None);
|
|
1989
2066
|
}
|
|
1990
2067
|
}
|
|
@@ -2167,7 +2244,7 @@ pub const Effects = struct {
|
|
|
2167
2244
|
}
|
|
2168
2245
|
foe.active.volatiles.Recharging = false;
|
|
2169
2246
|
|
|
2170
|
-
foe_stored.status = Status.slp(Rolls.
|
|
2247
|
+
foe_stored.status = Status.slp(Rolls.sleepDuration(battle));
|
|
2171
2248
|
try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
|
|
2172
2249
|
}
|
|
2173
2250
|
|
|
@@ -2220,7 +2297,7 @@ pub const Effects = struct {
|
|
|
2220
2297
|
assert(!volatiles.Bide);
|
|
2221
2298
|
|
|
2222
2299
|
volatiles.Thrashing = true;
|
|
2223
|
-
volatiles.attacks = Rolls.
|
|
2300
|
+
volatiles.attacks = Rolls.bideThrashDuration(battle);
|
|
2224
2301
|
}
|
|
2225
2302
|
|
|
2226
2303
|
fn transform(battle: anytype, player: Player, log: anytype) !void {
|
|
@@ -2252,7 +2329,7 @@ pub const Effects = struct {
|
|
|
2252
2329
|
try log.transform(battle.active(player), foe_ident);
|
|
2253
2330
|
}
|
|
2254
2331
|
|
|
2255
|
-
fn binding(battle: anytype, player: Player) void {
|
|
2332
|
+
fn binding(battle: anytype, player: Player, rewrap: bool) void {
|
|
2256
2333
|
var side = battle.side(player);
|
|
2257
2334
|
var foe = battle.foe(player);
|
|
2258
2335
|
|
|
@@ -2260,7 +2337,9 @@ pub const Effects = struct {
|
|
|
2260
2337
|
side.active.volatiles.Binding = true;
|
|
2261
2338
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2262
2339
|
// (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
|
|
2263
|
-
if (!showdown)
|
|
2340
|
+
if (!showdown) {
|
|
2341
|
+
foe.active.volatiles.Recharging = false;
|
|
2342
|
+
} else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
|
|
2264
2343
|
|
|
2265
2344
|
side.active.volatiles.attacks = Rolls.distribution(battle) - 1;
|
|
2266
2345
|
}
|
|
@@ -2589,7 +2668,7 @@ pub const Rolls = struct {
|
|
|
2589
2668
|
return battle.rng.next() < 1 + (if (low) Gen12.percent(20) else Gen12.percent(40));
|
|
2590
2669
|
}
|
|
2591
2670
|
|
|
2592
|
-
inline fn
|
|
2671
|
+
inline fn sleepDuration(battle: anytype) u3 {
|
|
2593
2672
|
if (showdown) return @intCast(u3, battle.rng.range(u8, 1, 8));
|
|
2594
2673
|
while (true) {
|
|
2595
2674
|
const r = battle.rng.next() & 7;
|
|
@@ -2597,7 +2676,7 @@ pub const Rolls = struct {
|
|
|
2597
2676
|
}
|
|
2598
2677
|
}
|
|
2599
2678
|
|
|
2600
|
-
inline fn
|
|
2679
|
+
inline fn bideThrashDuration(battle: anytype) u3 {
|
|
2601
2680
|
if (showdown) return @intCast(u3, battle.rng.range(u4, 2, 4));
|
|
2602
2681
|
return @intCast(u3, (battle.rng.next() & 1) + 2);
|
|
2603
2682
|
}
|
package/src/lib/pkmn.zig
CHANGED
|
@@ -25,12 +25,12 @@ pub const Result = @import("common/data.zig").Result;
|
|
|
25
25
|
/// Pokémon Showdown's RNG (backed by a Generation V & VI RNG).
|
|
26
26
|
pub const PSRNG = @import("common/rng.zig").PSRNG;
|
|
27
27
|
|
|
28
|
-
/// Namespace for helpers related to protocol
|
|
28
|
+
/// Namespace for helpers related to protocol message logging.
|
|
29
29
|
pub const protocol = if (options.internal) struct {
|
|
30
30
|
pub usingnamespace @import("common/protocol.zig");
|
|
31
31
|
} else struct {
|
|
32
32
|
/// Logs protocol information to its `Writer` during a battle update when
|
|
33
|
-
/// `options.
|
|
33
|
+
/// `options.log` is enabled.
|
|
34
34
|
pub const Log = @import("common/protocol.zig").Log;
|
|
35
35
|
/// Stripped down version of `std.io.FixedBufferStream` optimized for
|
|
36
36
|
/// efficiently writing the individual protocol bytes. Note that the
|
|
@@ -41,8 +41,8 @@ pub const protocol = if (options.internal) struct {
|
|
|
41
41
|
/// intialized with a `LOGS_SIZE`-sized buffer.
|
|
42
42
|
pub const FixedLog = @import("common/protocol.zig").FixedLog;
|
|
43
43
|
/// Null object pattern implementation of `Log` backed by a
|
|
44
|
-
/// `std.io.null_writer`. Ignores anything sent to it, though
|
|
45
|
-
/// should
|
|
44
|
+
/// `std.io.null_writer`. Ignores anything sent to it, though protocol
|
|
45
|
+
/// logging should additionally be turned off entirely with `options.log`.
|
|
46
46
|
pub const NULL = @import("common/protocol.zig").NULL;
|
|
47
47
|
};
|
|
48
48
|
|