@pkmn/engine 0.1.0-dev.a5a9f4b2 → 0.1.0-dev.af1565df
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 +14 -14
- 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/pkg/protocol.d.ts +4 -2
- package/build/pkg/protocol.js +4 -2
- package/build/pkg/protocol.js.map +1 -1
- package/build/test/display.js +29 -12
- package/build/test/display.js.map +1 -1
- package/build.zig +7 -7
- package/package.json +5 -4
- package/src/bin/install-pkmn-engine +3 -3
- package/src/lib/binding/node.zig +1 -1
- 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 +4 -15
- package/src/lib/gen1/mechanics.zig +156 -78
- 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,7 +489,7 @@ 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
495
|
const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
|
|
@@ -454,6 +497,7 @@ const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
|
|
|
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,14 +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... and doesn't clear Flinch
|
|
516
|
-
// until its imaginary "residual" phase, meaning Pokémon can sometimes flinch multiple times
|
|
517
|
-
// from the same original hit
|
|
518
|
-
if (showdown and volatiles.Flinch) {
|
|
519
|
-
try log.cant(ident, .Flinch);
|
|
520
|
-
return .done;
|
|
521
|
-
}
|
|
522
|
-
|
|
523
561
|
// This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
|
|
524
562
|
// before the side had a selected a move - we simply need to assume this leads to a desync
|
|
525
563
|
if (side.last_selected_move == .None) {
|
|
@@ -591,7 +629,14 @@ fn beforeMove(
|
|
|
591
629
|
if (volatiles.Bide) {
|
|
592
630
|
assert(!volatiles.Thrashing and !volatiles.Rage);
|
|
593
631
|
|
|
594
|
-
|
|
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
|
+
}
|
|
595
640
|
|
|
596
641
|
assert(volatiles.attacks > 0);
|
|
597
642
|
|
|
@@ -635,8 +680,13 @@ fn beforeMove(
|
|
|
635
680
|
try log.start(battle.active(player), .ConfusionSilent);
|
|
636
681
|
}
|
|
637
682
|
try log.move(ident, side.last_selected_move, battle.active(player.foe()), from);
|
|
638
|
-
if (showdown
|
|
639
|
-
|
|
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
|
+
}
|
|
640
690
|
}
|
|
641
691
|
return .skip_can;
|
|
642
692
|
}
|
|
@@ -651,7 +701,11 @@ fn beforeMove(
|
|
|
651
701
|
side.last_selected_move,
|
|
652
702
|
);
|
|
653
703
|
if (showdown or battle.last_damage != 0) {
|
|
704
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
654
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
|
+
}
|
|
655
709
|
}
|
|
656
710
|
return .done;
|
|
657
711
|
}
|
|
@@ -683,6 +737,7 @@ fn canMove(
|
|
|
683
737
|
side.active.volatiles.Invulnerable = false;
|
|
684
738
|
} else if (move.effect == .Charge) {
|
|
685
739
|
try log.move(player_ident, side.last_selected_move, .{}, from);
|
|
740
|
+
setCounterable(battle, player, side, move);
|
|
686
741
|
try Effects.charge(battle, player, log);
|
|
687
742
|
return false;
|
|
688
743
|
}
|
|
@@ -699,6 +754,7 @@ fn canMove(
|
|
|
699
754
|
// regrettable, but this information is more "nice to have" than required
|
|
700
755
|
const f = if (from != null and from.? == .None) null else from;
|
|
701
756
|
try log.move(player_ident, side.last_selected_move, battle.active(target), f);
|
|
757
|
+
setCounterable(battle, player, side, move);
|
|
702
758
|
|
|
703
759
|
if (move.effect.onBegin()) {
|
|
704
760
|
try onBegin(battle, player, move, mslot, residual, log);
|
|
@@ -709,12 +765,27 @@ fn canMove(
|
|
|
709
765
|
Effects.thrashing(battle, player);
|
|
710
766
|
} else if (!showdown and move.effect == .Binding) {
|
|
711
767
|
// Pokémon Showdown handles this after hit/miss checks and damage calculation
|
|
712
|
-
Effects.binding(battle, player);
|
|
768
|
+
Effects.binding(battle, player, false);
|
|
713
769
|
}
|
|
714
770
|
|
|
715
771
|
return true;
|
|
716
772
|
}
|
|
717
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
|
+
|
|
718
789
|
fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
|
|
719
790
|
if (side.last_selected_move == .Struggle) return;
|
|
720
791
|
|
|
@@ -750,6 +821,7 @@ fn doMove(
|
|
|
750
821
|
battle: anytype,
|
|
751
822
|
player: Player,
|
|
752
823
|
mslot: u4,
|
|
824
|
+
rewrap: bool,
|
|
753
825
|
auto: bool,
|
|
754
826
|
residual: *bool,
|
|
755
827
|
log: anytype,
|
|
@@ -841,9 +913,9 @@ fn doMove(
|
|
|
841
913
|
|
|
842
914
|
if (!showdown or !miss) {
|
|
843
915
|
if (move.effect == .MirrorMove) {
|
|
844
|
-
return mirrorMove(battle, player, mslot, auto, residual, log);
|
|
916
|
+
return mirrorMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
845
917
|
} else if (move.effect == .Metronome) {
|
|
846
|
-
return metronome(battle, player, mslot, auto, residual, log);
|
|
918
|
+
return metronome(battle, player, mslot, rewrap, auto, residual, log);
|
|
847
919
|
} else if (move.effect.onEnd()) {
|
|
848
920
|
try onEnd(battle, player, move, log);
|
|
849
921
|
return null;
|
|
@@ -858,7 +930,7 @@ fn doMove(
|
|
|
858
930
|
move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
|
|
859
931
|
if (ohko) {
|
|
860
932
|
try log.immune(foe_ident, .OHKO);
|
|
861
|
-
} else if (immune and !invulnerable) {
|
|
933
|
+
} else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
|
|
862
934
|
try log.immune(foe_ident, .None);
|
|
863
935
|
} else if (mist) {
|
|
864
936
|
if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
|
|
@@ -924,11 +996,17 @@ fn doMove(
|
|
|
924
996
|
} else if (showdown) {
|
|
925
997
|
// This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
|
|
926
998
|
if (move.effect == .Binding) {
|
|
927
|
-
Effects.binding(battle, player);
|
|
999
|
+
Effects.binding(battle, player, rewrap);
|
|
928
1000
|
if (immune) {
|
|
929
1001
|
battle.last_damage = 0;
|
|
1002
|
+
assert(foe.stored().hp > 0);
|
|
930
1003
|
// Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
|
|
931
|
-
|
|
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
|
+
}
|
|
932
1010
|
return null;
|
|
933
1011
|
}
|
|
934
1012
|
}
|
|
@@ -1125,19 +1203,17 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
|
|
|
1125
1203
|
return null;
|
|
1126
1204
|
}
|
|
1127
1205
|
|
|
1128
|
-
// Pretend
|
|
1129
|
-
const foe_last_used_move =
|
|
1130
|
-
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
|
|
1131
1207
|
const foe_last_selected_move =
|
|
1132
1208
|
Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
|
|
1133
|
-
.
|
|
1209
|
+
.Splash
|
|
1134
1210
|
else
|
|
1135
1211
|
foe.last_selected_move);
|
|
1136
1212
|
|
|
1137
|
-
const used =
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1213
|
+
const used = (if (player.foe() == .P1)
|
|
1214
|
+
battle.last_moves.p1_counterable
|
|
1215
|
+
else
|
|
1216
|
+
battle.last_moves.p2_counterable) != 0;
|
|
1141
1217
|
|
|
1142
1218
|
const selected = foe_last_selected_move.bp > 0 and
|
|
1143
1219
|
foe.last_selected_move != .Counter and
|
|
@@ -1156,12 +1232,14 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
|
|
|
1156
1232
|
return null;
|
|
1157
1233
|
}
|
|
1158
1234
|
|
|
1159
|
-
battle.last_damage
|
|
1235
|
+
battle.last_damage *|= 2;
|
|
1160
1236
|
|
|
1161
1237
|
// Pokémon Showdown calls checkHit before Counter
|
|
1162
1238
|
if (!showdown and !try checkHit(battle, player, move, log)) return null;
|
|
1163
1239
|
|
|
1240
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1164
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);
|
|
1165
1243
|
return null;
|
|
1166
1244
|
}
|
|
1167
1245
|
|
|
@@ -1205,6 +1283,7 @@ fn mirrorMove(
|
|
|
1205
1283
|
battle: anytype,
|
|
1206
1284
|
player: Player,
|
|
1207
1285
|
mslot: u4,
|
|
1286
|
+
rewrap: bool,
|
|
1208
1287
|
auto: bool,
|
|
1209
1288
|
residual: *bool,
|
|
1210
1289
|
log: anytype,
|
|
@@ -1217,18 +1296,19 @@ fn mirrorMove(
|
|
|
1217
1296
|
if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
|
|
1218
1297
|
try log.fail(battle.active(player), .None);
|
|
1219
1298
|
return null;
|
|
1299
|
+
} else if (!showdown or foe.last_used_move != .Struggle) {
|
|
1300
|
+
incrementPP(side, mslot);
|
|
1220
1301
|
}
|
|
1221
1302
|
|
|
1222
|
-
incrementPP(side, mslot);
|
|
1223
|
-
|
|
1224
1303
|
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
|
|
1225
|
-
return doMove(battle, player, mslot, auto, residual, log);
|
|
1304
|
+
return doMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
1226
1305
|
}
|
|
1227
1306
|
|
|
1228
1307
|
fn metronome(
|
|
1229
1308
|
battle: anytype,
|
|
1230
1309
|
player: Player,
|
|
1231
1310
|
mslot: u4,
|
|
1311
|
+
rewrap: bool,
|
|
1232
1312
|
auto: bool,
|
|
1233
1313
|
residual: *bool,
|
|
1234
1314
|
log: anytype,
|
|
@@ -1239,7 +1319,7 @@ fn metronome(
|
|
|
1239
1319
|
incrementPP(side, mslot);
|
|
1240
1320
|
|
|
1241
1321
|
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
|
|
1242
|
-
return doMove(battle, player, mslot, auto, residual, log);
|
|
1322
|
+
return doMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
1243
1323
|
}
|
|
1244
1324
|
|
|
1245
1325
|
fn checkHit(battle: anytype, player: Player, move: Move.Data, log: anytype) !bool {
|
|
@@ -1446,11 +1526,7 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
|
|
|
1446
1526
|
stored.hp -= amount;
|
|
1447
1527
|
|
|
1448
1528
|
// As above, Pokémon Showdown uses damageOf but its not relevant
|
|
1449
|
-
if (amount > 0)
|
|
1450
|
-
try log.damage(ident, stored, .LeechSeed);
|
|
1451
|
-
// Pokémon Showdown erroneously updates last damage with uncapped Leech Seed damage
|
|
1452
|
-
if (showdown) battle.last_damage = damage;
|
|
1453
|
-
}
|
|
1529
|
+
if (amount > 0) try log.damage(ident, stored, .LeechSeed);
|
|
1454
1530
|
|
|
1455
1531
|
const before = foe_stored.hp;
|
|
1456
1532
|
// Uncapped damage is added back to the foe
|
|
@@ -1464,13 +1540,13 @@ fn endTurn(battle: anytype, log: anytype) @TypeOf(log).Error!Result {
|
|
|
1464
1540
|
assert(!battle.side(.P1).active.volatiles.MultiHit);
|
|
1465
1541
|
assert(!battle.side(.P2).active.volatiles.MultiHit);
|
|
1466
1542
|
|
|
1543
|
+
battle.turn += 1;
|
|
1544
|
+
|
|
1467
1545
|
if (showdown and options.ebc and checkEBC(battle)) {
|
|
1468
1546
|
try log.tie();
|
|
1469
1547
|
return Result.Tie;
|
|
1470
1548
|
}
|
|
1471
1549
|
|
|
1472
|
-
battle.turn += 1;
|
|
1473
|
-
|
|
1474
1550
|
if (showdown and battle.turn >= 1000) {
|
|
1475
1551
|
try log.tie();
|
|
1476
1552
|
return Result.Tie;
|
|
@@ -1983,9 +2059,9 @@ pub const Effects = struct {
|
|
|
1983
2059
|
}
|
|
1984
2060
|
break :has_mimic false;
|
|
1985
2061
|
};
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
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;
|
|
1989
2065
|
return try log.fail(battle.active(player.foe()), .None);
|
|
1990
2066
|
}
|
|
1991
2067
|
}
|
|
@@ -2253,7 +2329,7 @@ pub const Effects = struct {
|
|
|
2253
2329
|
try log.transform(battle.active(player), foe_ident);
|
|
2254
2330
|
}
|
|
2255
2331
|
|
|
2256
|
-
fn binding(battle: anytype, player: Player) void {
|
|
2332
|
+
fn binding(battle: anytype, player: Player, rewrap: bool) void {
|
|
2257
2333
|
var side = battle.side(player);
|
|
2258
2334
|
var foe = battle.foe(player);
|
|
2259
2335
|
|
|
@@ -2261,7 +2337,9 @@ pub const Effects = struct {
|
|
|
2261
2337
|
side.active.volatiles.Binding = true;
|
|
2262
2338
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2263
2339
|
// (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
|
|
2264
|
-
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);
|
|
2265
2343
|
|
|
2266
2344
|
side.active.volatiles.attacks = Rolls.distribution(battle) - 1;
|
|
2267
2345
|
}
|
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
|
|