@pkmn/engine 0.1.0-dev.7cd30bd9 → 0.1.0-dev.7f2014fa
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 +34 -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 +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 +79 -62
- package/src/lib/gen1/data.zig +14 -12
- package/src/lib/gen1/helpers.zig +11 -13
- package/src/lib/gen1/mechanics.zig +138 -73
- 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;
|
|
@@ -327,9 +343,13 @@ fn doTurn(
|
|
|
327
343
|
battle: anytype,
|
|
328
344
|
player: Player,
|
|
329
345
|
player_choice: Choice,
|
|
346
|
+
player_rewrap: bool,
|
|
347
|
+
player_skip: bool,
|
|
330
348
|
player_from: ?Move,
|
|
331
349
|
foe_player: Player,
|
|
332
350
|
foe_choice: Choice,
|
|
351
|
+
foe_rewrap: bool,
|
|
352
|
+
foe_skip: bool,
|
|
333
353
|
foe_from: ?Move,
|
|
334
354
|
log: anytype,
|
|
335
355
|
) !?Result {
|
|
@@ -337,7 +357,16 @@ fn doTurn(
|
|
|
337
357
|
|
|
338
358
|
var residual = true;
|
|
339
359
|
var replace = battle.side(player).stored().hp == 0;
|
|
340
|
-
if (try executeMove(
|
|
360
|
+
if (try executeMove(
|
|
361
|
+
battle,
|
|
362
|
+
player,
|
|
363
|
+
player_choice,
|
|
364
|
+
player_rewrap,
|
|
365
|
+
player_skip,
|
|
366
|
+
player_from,
|
|
367
|
+
&residual,
|
|
368
|
+
log,
|
|
369
|
+
)) |r| return r;
|
|
341
370
|
if (!replace) {
|
|
342
371
|
if (player_choice.type != .Switch) {
|
|
343
372
|
if (try checkFaint(battle, foe_player, log)) |r| return r;
|
|
@@ -348,7 +377,16 @@ fn doTurn(
|
|
|
348
377
|
|
|
349
378
|
residual = true;
|
|
350
379
|
replace = battle.side(foe_player).stored().hp == 0;
|
|
351
|
-
if (try executeMove(
|
|
380
|
+
if (try executeMove(
|
|
381
|
+
battle,
|
|
382
|
+
foe_player,
|
|
383
|
+
foe_choice,
|
|
384
|
+
foe_rewrap,
|
|
385
|
+
foe_skip,
|
|
386
|
+
foe_from,
|
|
387
|
+
&residual,
|
|
388
|
+
log,
|
|
389
|
+
)) |r| return r;
|
|
352
390
|
if (!replace) {
|
|
353
391
|
if (foe_choice.type != .Switch) {
|
|
354
392
|
if (try checkFaint(battle, player, log)) |r| return r;
|
|
@@ -371,6 +409,8 @@ fn executeMove(
|
|
|
371
409
|
battle: anytype,
|
|
372
410
|
player: Player,
|
|
373
411
|
choice: Choice,
|
|
412
|
+
rewrap: bool,
|
|
413
|
+
skip: bool,
|
|
374
414
|
from: ?Move,
|
|
375
415
|
residual: *bool,
|
|
376
416
|
log: anytype,
|
|
@@ -382,9 +422,10 @@ fn executeMove(
|
|
|
382
422
|
return null;
|
|
383
423
|
}
|
|
384
424
|
|
|
425
|
+
// This is the correct place to check for SKIP_TURN and abort early, however since Pokémon
|
|
426
|
+
// Showdown overwrites the SKIP_TURN sentinel with its botched move select we need to add an
|
|
427
|
+
// additional skip boolean to accomplish the same thing in the Binding check of BeforeMove
|
|
385
428
|
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
429
|
assert(!showdown);
|
|
389
430
|
if (battle.foe(player).active.volatiles.Binding) {
|
|
390
431
|
try log.cant(battle.active(player), .Bound);
|
|
@@ -403,9 +444,9 @@ fn executeMove(
|
|
|
403
444
|
// choice.data == 0 only happens with Struggle on Pokémon Showdown
|
|
404
445
|
assert(!showdown);
|
|
405
446
|
mslot = @intCast(u4, if (player == .P1)
|
|
406
|
-
battle.
|
|
447
|
+
battle.last_moves.p1_index
|
|
407
448
|
else
|
|
408
|
-
battle.
|
|
449
|
+
battle.last_moves.p2_index);
|
|
409
450
|
const stored = side.stored();
|
|
410
451
|
// GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
|
|
411
452
|
auto = side.last_selected_move == .HyperBeam or side.active.volatiles.Bide or
|
|
@@ -427,14 +468,14 @@ fn executeMove(
|
|
|
427
468
|
// Incorrect mslot with Pokémon Showdown choice semantics so we need to recover from index
|
|
428
469
|
assert(mslot == 1);
|
|
429
470
|
mslot = @intCast(u4, if (player == .P1)
|
|
430
|
-
battle.
|
|
471
|
+
battle.last_moves.p1_index
|
|
431
472
|
else
|
|
432
|
-
battle.
|
|
473
|
+
battle.last_moves.p2_index);
|
|
433
474
|
}
|
|
434
475
|
|
|
435
476
|
var skip_can = false;
|
|
436
477
|
var skip_pp = false;
|
|
437
|
-
switch (try beforeMove(battle, player, from, residual, log)) {
|
|
478
|
+
switch (try beforeMove(battle, player, skip, from, residual, log)) {
|
|
438
479
|
.done => return null,
|
|
439
480
|
.skip_can => skip_can = true,
|
|
440
481
|
.skip_pp => skip_pp = true,
|
|
@@ -446,7 +487,7 @@ fn executeMove(
|
|
|
446
487
|
return null;
|
|
447
488
|
}
|
|
448
489
|
|
|
449
|
-
return doMove(battle, player, mslot, auto, residual, log);
|
|
490
|
+
return doMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
450
491
|
}
|
|
451
492
|
|
|
452
493
|
const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
|
|
@@ -454,6 +495,7 @@ const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
|
|
|
454
495
|
fn beforeMove(
|
|
455
496
|
battle: anytype,
|
|
456
497
|
player: Player,
|
|
498
|
+
skip: bool,
|
|
457
499
|
from: ?Move,
|
|
458
500
|
residual: *bool,
|
|
459
501
|
log: anytype,
|
|
@@ -485,13 +527,15 @@ fn beforeMove(
|
|
|
485
527
|
return .done;
|
|
486
528
|
}
|
|
487
529
|
|
|
488
|
-
if (foe.active.volatiles.Binding) {
|
|
530
|
+
if (skip or foe.active.volatiles.Binding) {
|
|
489
531
|
try log.cant(ident, .Bound);
|
|
490
532
|
return .done;
|
|
491
533
|
}
|
|
492
534
|
|
|
493
|
-
if (
|
|
494
|
-
|
|
535
|
+
if (volatiles.Flinch) {
|
|
536
|
+
// Pokémon Showdown doesn't clear Flinch until its imaginary "residual" phase, meaning
|
|
537
|
+
// Pokémon can sometimes flinch multiple times from the same original hit
|
|
538
|
+
if (!showdown) volatiles.Flinch = false;
|
|
495
539
|
try log.cant(ident, .Flinch);
|
|
496
540
|
return .done;
|
|
497
541
|
}
|
|
@@ -512,14 +556,6 @@ fn beforeMove(
|
|
|
512
556
|
// Pokémon Showdown's disable condition has a single onBeforeMove handler
|
|
513
557
|
if (showdown and try disabled(side, ident, log)) return .done;
|
|
514
558
|
|
|
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
559
|
// This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
|
|
524
560
|
// before the side had a selected a move - we simply need to assume this leads to a desync
|
|
525
561
|
if (side.last_selected_move == .None) {
|
|
@@ -635,8 +671,13 @@ fn beforeMove(
|
|
|
635
671
|
try log.start(battle.active(player), .ConfusionSilent);
|
|
636
672
|
}
|
|
637
673
|
try log.move(ident, side.last_selected_move, battle.active(player.foe()), from);
|
|
638
|
-
if (showdown
|
|
639
|
-
|
|
674
|
+
if (showdown) {
|
|
675
|
+
// This shouldn't actually set last_used_move, but Pokémon Showdown sets last
|
|
676
|
+
// used in useMove and doesn't have the notion of skipping canMove semantics
|
|
677
|
+
side.last_used_move = side.last_selected_move;
|
|
678
|
+
if (handleThrashing(battle, active)) {
|
|
679
|
+
try log.start(battle.active(player), .ConfusionSilent);
|
|
680
|
+
}
|
|
640
681
|
}
|
|
641
682
|
return .skip_can;
|
|
642
683
|
}
|
|
@@ -651,7 +692,11 @@ fn beforeMove(
|
|
|
651
692
|
side.last_selected_move,
|
|
652
693
|
);
|
|
653
694
|
if (showdown or battle.last_damage != 0) {
|
|
695
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
654
696
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
697
|
+
if (battle.foe(player).stored().hp > 0 and !sub) {
|
|
698
|
+
try buildRage(battle, player.foe(), log);
|
|
699
|
+
}
|
|
655
700
|
}
|
|
656
701
|
return .done;
|
|
657
702
|
}
|
|
@@ -700,6 +745,19 @@ fn canMove(
|
|
|
700
745
|
const f = if (from != null and from.? == .None) null else from;
|
|
701
746
|
try log.move(player_ident, side.last_selected_move, battle.active(target), f);
|
|
702
747
|
|
|
748
|
+
// The Counter desync is caused by the cartridge not calling GetCurrentMove until now, meaning
|
|
749
|
+
// in cases where an early return happens the data for a players last selected move does not get
|
|
750
|
+
// reloaded and HandleCounterMove actually bases its success/failure off of stale information.
|
|
751
|
+
// This boolean state we track here doesn't exist on the cartridge because it instead manifests
|
|
752
|
+
// as the desync results from actually having two separate battle states that subtly disagree.
|
|
753
|
+
const counterable = side.last_selected_move != .Counter and move.bp > 0 and
|
|
754
|
+
(move.type == .Normal or move.type == .Fighting);
|
|
755
|
+
if (player == .P1) {
|
|
756
|
+
battle.last_moves.p1_counterable = @boolToInt(counterable);
|
|
757
|
+
} else {
|
|
758
|
+
battle.last_moves.p2_counterable = @boolToInt(counterable);
|
|
759
|
+
}
|
|
760
|
+
|
|
703
761
|
if (move.effect.onBegin()) {
|
|
704
762
|
try onBegin(battle, player, move, mslot, residual, log);
|
|
705
763
|
return false;
|
|
@@ -709,7 +767,7 @@ fn canMove(
|
|
|
709
767
|
Effects.thrashing(battle, player);
|
|
710
768
|
} else if (!showdown and move.effect == .Binding) {
|
|
711
769
|
// Pokémon Showdown handles this after hit/miss checks and damage calculation
|
|
712
|
-
Effects.binding(battle, player);
|
|
770
|
+
Effects.binding(battle, player, false);
|
|
713
771
|
}
|
|
714
772
|
|
|
715
773
|
return true;
|
|
@@ -750,6 +808,7 @@ fn doMove(
|
|
|
750
808
|
battle: anytype,
|
|
751
809
|
player: Player,
|
|
752
810
|
mslot: u4,
|
|
811
|
+
rewrap: bool,
|
|
753
812
|
auto: bool,
|
|
754
813
|
residual: *bool,
|
|
755
814
|
log: anytype,
|
|
@@ -841,9 +900,9 @@ fn doMove(
|
|
|
841
900
|
|
|
842
901
|
if (!showdown or !miss) {
|
|
843
902
|
if (move.effect == .MirrorMove) {
|
|
844
|
-
return mirrorMove(battle, player, mslot, auto, residual, log);
|
|
903
|
+
return mirrorMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
845
904
|
} else if (move.effect == .Metronome) {
|
|
846
|
-
return metronome(battle, player, mslot, auto, residual, log);
|
|
905
|
+
return metronome(battle, player, mslot, rewrap, auto, residual, log);
|
|
847
906
|
} else if (move.effect.onEnd()) {
|
|
848
907
|
try onEnd(battle, player, move, log);
|
|
849
908
|
return null;
|
|
@@ -858,7 +917,7 @@ fn doMove(
|
|
|
858
917
|
move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
|
|
859
918
|
if (ohko) {
|
|
860
919
|
try log.immune(foe_ident, .OHKO);
|
|
861
|
-
} else if (immune and !invulnerable) {
|
|
920
|
+
} else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
|
|
862
921
|
try log.immune(foe_ident, .None);
|
|
863
922
|
} else if (mist) {
|
|
864
923
|
if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
|
|
@@ -924,11 +983,17 @@ fn doMove(
|
|
|
924
983
|
} else if (showdown) {
|
|
925
984
|
// This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
|
|
926
985
|
if (move.effect == .Binding) {
|
|
927
|
-
Effects.binding(battle, player);
|
|
986
|
+
Effects.binding(battle, player, rewrap);
|
|
928
987
|
if (immune) {
|
|
929
988
|
battle.last_damage = 0;
|
|
989
|
+
assert(foe.stored().hp > 0);
|
|
930
990
|
// Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
|
|
931
|
-
|
|
991
|
+
if (sub) {
|
|
992
|
+
try log.activate(battle.active(player.foe()), .Substitute);
|
|
993
|
+
} else {
|
|
994
|
+
try log.damage(battle.active(player.foe()), foe.stored(), .None);
|
|
995
|
+
try buildRage(battle, player.foe(), log);
|
|
996
|
+
}
|
|
932
997
|
return null;
|
|
933
998
|
}
|
|
934
999
|
}
|
|
@@ -1125,19 +1190,17 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
|
|
|
1125
1190
|
return null;
|
|
1126
1191
|
}
|
|
1127
1192
|
|
|
1128
|
-
// Pretend
|
|
1129
|
-
const foe_last_used_move =
|
|
1130
|
-
Move.get(if (foe.last_used_move == .None) .Counter else foe.last_used_move);
|
|
1193
|
+
// Pretend Splash was used as a stand-in when no move has been used to fail below with 0 BP
|
|
1131
1194
|
const foe_last_selected_move =
|
|
1132
1195
|
Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
|
|
1133
|
-
.
|
|
1196
|
+
.Splash
|
|
1134
1197
|
else
|
|
1135
1198
|
foe.last_selected_move);
|
|
1136
1199
|
|
|
1137
|
-
const used =
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1200
|
+
const used = (if (player.foe() == .P1)
|
|
1201
|
+
battle.last_moves.p1_counterable
|
|
1202
|
+
else
|
|
1203
|
+
battle.last_moves.p2_counterable) != 0;
|
|
1141
1204
|
|
|
1142
1205
|
const selected = foe_last_selected_move.bp > 0 and
|
|
1143
1206
|
foe.last_selected_move != .Counter and
|
|
@@ -1161,7 +1224,9 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
|
|
|
1161
1224
|
// Pokémon Showdown calls checkHit before Counter
|
|
1162
1225
|
if (!showdown and !try checkHit(battle, player, move, log)) return null;
|
|
1163
1226
|
|
|
1227
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1164
1228
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
1229
|
+
if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
|
|
1165
1230
|
return null;
|
|
1166
1231
|
}
|
|
1167
1232
|
|
|
@@ -1205,6 +1270,7 @@ fn mirrorMove(
|
|
|
1205
1270
|
battle: anytype,
|
|
1206
1271
|
player: Player,
|
|
1207
1272
|
mslot: u4,
|
|
1273
|
+
rewrap: bool,
|
|
1208
1274
|
auto: bool,
|
|
1209
1275
|
residual: *bool,
|
|
1210
1276
|
log: anytype,
|
|
@@ -1217,18 +1283,19 @@ fn mirrorMove(
|
|
|
1217
1283
|
if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
|
|
1218
1284
|
try log.fail(battle.active(player), .None);
|
|
1219
1285
|
return null;
|
|
1286
|
+
} else if (!showdown or foe.last_used_move != .Struggle) {
|
|
1287
|
+
incrementPP(side, mslot);
|
|
1220
1288
|
}
|
|
1221
1289
|
|
|
1222
|
-
incrementPP(side, mslot);
|
|
1223
|
-
|
|
1224
1290
|
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
|
|
1225
|
-
return doMove(battle, player, mslot, auto, residual, log);
|
|
1291
|
+
return doMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
1226
1292
|
}
|
|
1227
1293
|
|
|
1228
1294
|
fn metronome(
|
|
1229
1295
|
battle: anytype,
|
|
1230
1296
|
player: Player,
|
|
1231
1297
|
mslot: u4,
|
|
1298
|
+
rewrap: bool,
|
|
1232
1299
|
auto: bool,
|
|
1233
1300
|
residual: *bool,
|
|
1234
1301
|
log: anytype,
|
|
@@ -1239,7 +1306,7 @@ fn metronome(
|
|
|
1239
1306
|
incrementPP(side, mslot);
|
|
1240
1307
|
|
|
1241
1308
|
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
|
|
1242
|
-
return doMove(battle, player, mslot, auto, residual, log);
|
|
1309
|
+
return doMove(battle, player, mslot, rewrap, auto, residual, log);
|
|
1243
1310
|
}
|
|
1244
1311
|
|
|
1245
1312
|
fn checkHit(battle: anytype, player: Player, move: Move.Data, log: anytype) !bool {
|
|
@@ -1446,11 +1513,7 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
|
|
|
1446
1513
|
stored.hp -= amount;
|
|
1447
1514
|
|
|
1448
1515
|
// 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
|
-
}
|
|
1516
|
+
if (amount > 0) try log.damage(ident, stored, .LeechSeed);
|
|
1454
1517
|
|
|
1455
1518
|
const before = foe_stored.hp;
|
|
1456
1519
|
// Uncapped damage is added back to the foe
|
|
@@ -1983,9 +2046,9 @@ pub const Effects = struct {
|
|
|
1983
2046
|
}
|
|
1984
2047
|
break :has_mimic false;
|
|
1985
2048
|
};
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
2049
|
+
if (!has_mimic) {
|
|
2050
|
+
// Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
|
|
2051
|
+
if (!try checkHit(battle, player, move, log)) return;
|
|
1989
2052
|
return try log.fail(battle.active(player.foe()), .None);
|
|
1990
2053
|
}
|
|
1991
2054
|
}
|
|
@@ -2253,7 +2316,7 @@ pub const Effects = struct {
|
|
|
2253
2316
|
try log.transform(battle.active(player), foe_ident);
|
|
2254
2317
|
}
|
|
2255
2318
|
|
|
2256
|
-
fn binding(battle: anytype, player: Player) void {
|
|
2319
|
+
fn binding(battle: anytype, player: Player, rewrap: bool) void {
|
|
2257
2320
|
var side = battle.side(player);
|
|
2258
2321
|
var foe = battle.foe(player);
|
|
2259
2322
|
|
|
@@ -2261,7 +2324,9 @@ pub const Effects = struct {
|
|
|
2261
2324
|
side.active.volatiles.Binding = true;
|
|
2262
2325
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2263
2326
|
// (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
|
|
2264
|
-
if (!showdown)
|
|
2327
|
+
if (!showdown) {
|
|
2328
|
+
foe.active.volatiles.Recharging = false;
|
|
2329
|
+
} else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
|
|
2265
2330
|
|
|
2266
2331
|
side.active.volatiles.attacks = Rolls.distribution(battle) - 1;
|
|
2267
2332
|
}
|
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
|
|