@pkmn/engine 0.1.0-dev.4005e495 → 0.1.0-dev.4eeb346e
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 +4 -5
- package/build/pkg/gen1.js +23 -42
- 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/test/display.js +12 -29
- package/build/test/display.js.map +1 -1
- package/build.zig +7 -7
- package/package.json +9 -10
- 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 +0 -1
- package/src/lib/common/options.zig +3 -3
- package/src/lib/common/protocol.zig +38 -38
- package/src/lib/gen1/README.md +69 -90
- package/src/lib/gen1/data.zig +12 -14
- package/src/lib/gen1/helpers.zig +14 -3
- package/src/lib/gen1/mechanics.zig +77 -156
- package/src/lib/pkmn.zig +4 -4
|
@@ -62,28 +62,26 @@ 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;
|
|
67
65
|
var f1: ?Move = null;
|
|
68
66
|
var f2: ?Move = null;
|
|
69
67
|
|
|
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;
|
|
68
|
+
if (selectMove(battle, .P1, c1, c2, &f1)) |r| return r;
|
|
69
|
+
if (selectMove(battle, .P2, c2, c1, &f2)) |r| return r;
|
|
78
70
|
|
|
79
71
|
if (turnOrder(battle, c1, c2) == .P1) {
|
|
80
|
-
if (try doTurn(battle, .P1, c1,
|
|
72
|
+
if (try doTurn(battle, .P1, c1, f1, .P2, c2, f2, log)) |r| return r;
|
|
81
73
|
} else {
|
|
82
|
-
if (try doTurn(battle, .P2, c2,
|
|
74
|
+
if (try doTurn(battle, .P2, c2, f2, .P1, c1, f1, log)) |r| return r;
|
|
83
75
|
}
|
|
84
76
|
|
|
85
|
-
|
|
86
|
-
if (
|
|
77
|
+
var p1 = battle.side(.P1);
|
|
78
|
+
if (p1.active.volatiles.attacks == 0) {
|
|
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
|
+
}
|
|
87
85
|
|
|
88
86
|
return endTurn(battle, log);
|
|
89
87
|
}
|
|
@@ -116,7 +114,6 @@ fn selectMove(
|
|
|
116
114
|
player: Player,
|
|
117
115
|
choice: Choice,
|
|
118
116
|
foe_choice: Choice,
|
|
119
|
-
skip_turn: *bool,
|
|
120
117
|
from: *?Move,
|
|
121
118
|
) ?Result {
|
|
122
119
|
if (choice.type == .Pass) return null;
|
|
@@ -129,26 +126,17 @@ fn selectMove(
|
|
|
129
126
|
(choice.type == .Move and choice.data == @boolToInt(showdown)));
|
|
130
127
|
|
|
131
128
|
// pre-battle menu
|
|
132
|
-
if (volatiles.Recharging)
|
|
133
|
-
if (showdown and battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
|
|
134
|
-
return null;
|
|
135
|
-
}
|
|
129
|
+
if (volatiles.Recharging) return null;
|
|
136
130
|
if (volatiles.Rage) {
|
|
137
131
|
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
|
-
}
|
|
132
|
+
if (showdown) saveMove(battle, player, null);
|
|
142
133
|
return null;
|
|
143
134
|
}
|
|
144
135
|
// Pokémon Showdown removes Flinch at the end-of-turn in its residual handler
|
|
145
136
|
if (!showdown) volatiles.Flinch = false;
|
|
146
137
|
if (volatiles.Thrashing or volatiles.Charging) {
|
|
147
138
|
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
|
-
}
|
|
139
|
+
if (showdown) saveMove(battle, player, null);
|
|
152
140
|
return null;
|
|
153
141
|
}
|
|
154
142
|
|
|
@@ -158,10 +146,7 @@ fn selectMove(
|
|
|
158
146
|
// pre-move select
|
|
159
147
|
if (Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP) or volatiles.Bide) {
|
|
160
148
|
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
|
-
}
|
|
149
|
+
if (showdown) saveMove(battle, player, choice);
|
|
165
150
|
return null;
|
|
166
151
|
}
|
|
167
152
|
if (volatiles.Binding) {
|
|
@@ -180,9 +165,9 @@ fn selectMove(
|
|
|
180
165
|
// GLITCH: https://glitchcity.wiki/Partial_trapping_move_Mirror_Move_link_battle_glitch
|
|
181
166
|
if (foe_choice.type == .Switch) {
|
|
182
167
|
const slot = if (player == .P1)
|
|
183
|
-
battle.
|
|
168
|
+
battle.last_selected_indexes.p1
|
|
184
169
|
else
|
|
185
|
-
battle.
|
|
170
|
+
battle.last_selected_indexes.p2;
|
|
186
171
|
const last = side.active.move(@intCast(u4, slot));
|
|
187
172
|
if (last.id == .Metronome) side.last_selected_move = last.id;
|
|
188
173
|
if (last.id == .MirrorMove) return Result.Error;
|
|
@@ -192,7 +177,6 @@ fn selectMove(
|
|
|
192
177
|
}
|
|
193
178
|
|
|
194
179
|
if (battle.foe(player).active.volatiles.Binding) {
|
|
195
|
-
skip_turn.* = true;
|
|
196
180
|
if (showdown) {
|
|
197
181
|
saveMove(battle, player, choice);
|
|
198
182
|
} else {
|
|
@@ -237,9 +221,9 @@ fn saveMove(battle: anytype, player: Player, choice: ?Choice) void {
|
|
|
237
221
|
|
|
238
222
|
side.last_selected_move = move.id;
|
|
239
223
|
if (player == .P1) {
|
|
240
|
-
battle.
|
|
224
|
+
battle.last_selected_indexes.p1 = @intCast(u4, c.data);
|
|
241
225
|
} else {
|
|
242
|
-
battle.
|
|
226
|
+
battle.last_selected_indexes.p2 = @intCast(u4, c.data);
|
|
243
227
|
}
|
|
244
228
|
}
|
|
245
229
|
}
|
|
@@ -259,9 +243,9 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, log: anyty
|
|
|
259
243
|
side.order[slot - 1] = out;
|
|
260
244
|
|
|
261
245
|
if (player == .P1) {
|
|
262
|
-
battle.
|
|
246
|
+
battle.last_selected_indexes.p1 = 1;
|
|
263
247
|
} else {
|
|
264
|
-
battle.
|
|
248
|
+
battle.last_selected_indexes.p2 = 1;
|
|
265
249
|
}
|
|
266
250
|
|
|
267
251
|
side.last_used_move = .None;
|
|
@@ -318,9 +302,7 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
|
|
|
318
302
|
const spe2 = battle.side(.P2).active.stats.spe;
|
|
319
303
|
if (spe1 == spe2) {
|
|
320
304
|
// Pokémon Showdown's beforeTurnCallback shenanigans
|
|
321
|
-
if (showdown and
|
|
322
|
-
battle.rng.advance(1);
|
|
323
|
-
}
|
|
305
|
+
if (showdown and m1 == .Counter and m2 == .Counter) battle.rng.advance(1);
|
|
324
306
|
|
|
325
307
|
const p1 = Rolls.speedTie(battle);
|
|
326
308
|
if (!showdown) return if (p1) .P1 else .P2;
|
|
@@ -345,13 +327,9 @@ fn doTurn(
|
|
|
345
327
|
battle: anytype,
|
|
346
328
|
player: Player,
|
|
347
329
|
player_choice: Choice,
|
|
348
|
-
player_rewrap: bool,
|
|
349
|
-
player_skip: bool,
|
|
350
330
|
player_from: ?Move,
|
|
351
331
|
foe_player: Player,
|
|
352
332
|
foe_choice: Choice,
|
|
353
|
-
foe_rewrap: bool,
|
|
354
|
-
foe_skip: bool,
|
|
355
333
|
foe_from: ?Move,
|
|
356
334
|
log: anytype,
|
|
357
335
|
) !?Result {
|
|
@@ -359,16 +337,7 @@ fn doTurn(
|
|
|
359
337
|
|
|
360
338
|
var residual = true;
|
|
361
339
|
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;
|
|
340
|
+
if (try executeMove(battle, player, player_choice, player_from, &residual, log)) |r| return r;
|
|
372
341
|
if (!replace) {
|
|
373
342
|
if (player_choice.type != .Switch) {
|
|
374
343
|
if (try checkFaint(battle, foe_player, log)) |r| return r;
|
|
@@ -379,16 +348,7 @@ fn doTurn(
|
|
|
379
348
|
|
|
380
349
|
residual = true;
|
|
381
350
|
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;
|
|
351
|
+
if (try executeMove(battle, foe_player, foe_choice, foe_from, &residual, log)) |r| return r;
|
|
392
352
|
if (!replace) {
|
|
393
353
|
if (foe_choice.type != .Switch) {
|
|
394
354
|
if (try checkFaint(battle, player, log)) |r| return r;
|
|
@@ -411,8 +371,6 @@ fn executeMove(
|
|
|
411
371
|
battle: anytype,
|
|
412
372
|
player: Player,
|
|
413
373
|
choice: Choice,
|
|
414
|
-
rewrap: bool,
|
|
415
|
-
skip: bool,
|
|
416
374
|
from: ?Move,
|
|
417
375
|
residual: *bool,
|
|
418
376
|
log: anytype,
|
|
@@ -424,10 +382,9 @@ fn executeMove(
|
|
|
424
382
|
return null;
|
|
425
383
|
}
|
|
426
384
|
|
|
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
385
|
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
|
|
431
388
|
assert(!showdown);
|
|
432
389
|
if (battle.foe(player).active.volatiles.Binding) {
|
|
433
390
|
try log.cant(battle.active(player), .Bound);
|
|
@@ -446,9 +403,9 @@ fn executeMove(
|
|
|
446
403
|
// choice.data == 0 only happens with Struggle on Pokémon Showdown
|
|
447
404
|
assert(!showdown);
|
|
448
405
|
mslot = @intCast(u4, if (player == .P1)
|
|
449
|
-
battle.
|
|
406
|
+
battle.last_selected_indexes.p1
|
|
450
407
|
else
|
|
451
|
-
battle.
|
|
408
|
+
battle.last_selected_indexes.p2);
|
|
452
409
|
const stored = side.stored();
|
|
453
410
|
// GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
|
|
454
411
|
auto = side.last_selected_move == .HyperBeam or side.active.volatiles.Bide or
|
|
@@ -470,14 +427,14 @@ fn executeMove(
|
|
|
470
427
|
// Incorrect mslot with Pokémon Showdown choice semantics so we need to recover from index
|
|
471
428
|
assert(mslot == 1);
|
|
472
429
|
mslot = @intCast(u4, if (player == .P1)
|
|
473
|
-
battle.
|
|
430
|
+
battle.last_selected_indexes.p1
|
|
474
431
|
else
|
|
475
|
-
battle.
|
|
432
|
+
battle.last_selected_indexes.p2);
|
|
476
433
|
}
|
|
477
434
|
|
|
478
435
|
var skip_can = false;
|
|
479
436
|
var skip_pp = false;
|
|
480
|
-
switch (try beforeMove(battle, player,
|
|
437
|
+
switch (try beforeMove(battle, player, from, residual, log)) {
|
|
481
438
|
.done => return null,
|
|
482
439
|
.skip_can => skip_can = true,
|
|
483
440
|
.skip_pp => skip_pp = true,
|
|
@@ -489,7 +446,7 @@ fn executeMove(
|
|
|
489
446
|
return null;
|
|
490
447
|
}
|
|
491
448
|
|
|
492
|
-
return doMove(battle, player, mslot,
|
|
449
|
+
return doMove(battle, player, mslot, auto, residual, log);
|
|
493
450
|
}
|
|
494
451
|
|
|
495
452
|
const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
|
|
@@ -497,7 +454,6 @@ const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
|
|
|
497
454
|
fn beforeMove(
|
|
498
455
|
battle: anytype,
|
|
499
456
|
player: Player,
|
|
500
|
-
skip: bool,
|
|
501
457
|
from: ?Move,
|
|
502
458
|
residual: *bool,
|
|
503
459
|
log: anytype,
|
|
@@ -529,15 +485,13 @@ fn beforeMove(
|
|
|
529
485
|
return .done;
|
|
530
486
|
}
|
|
531
487
|
|
|
532
|
-
if (
|
|
488
|
+
if (foe.active.volatiles.Binding) {
|
|
533
489
|
try log.cant(ident, .Bound);
|
|
534
490
|
return .done;
|
|
535
491
|
}
|
|
536
492
|
|
|
537
|
-
if (volatiles.Flinch) {
|
|
538
|
-
|
|
539
|
-
// Pokémon can sometimes flinch multiple times from the same original hit
|
|
540
|
-
if (!showdown) volatiles.Flinch = false;
|
|
493
|
+
if (!showdown and volatiles.Flinch) {
|
|
494
|
+
volatiles.Flinch = false;
|
|
541
495
|
try log.cant(ident, .Flinch);
|
|
542
496
|
return .done;
|
|
543
497
|
}
|
|
@@ -558,6 +512,13 @@ fn beforeMove(
|
|
|
558
512
|
// Pokémon Showdown's disable condition has a single onBeforeMove handler
|
|
559
513
|
if (showdown and try disabled(side, ident, log)) return .done;
|
|
560
514
|
|
|
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
|
+
|
|
561
522
|
// This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
|
|
562
523
|
// before the side had a selected a move - we simply need to assume this leads to a desync
|
|
563
524
|
if (side.last_selected_move == .None) {
|
|
@@ -629,14 +590,7 @@ fn beforeMove(
|
|
|
629
590
|
if (volatiles.Bide) {
|
|
630
591
|
assert(!volatiles.Thrashing and !volatiles.Rage);
|
|
631
592
|
|
|
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
|
-
}
|
|
593
|
+
volatiles.state +%= battle.last_damage;
|
|
640
594
|
|
|
641
595
|
assert(volatiles.attacks > 0);
|
|
642
596
|
|
|
@@ -680,13 +634,8 @@ fn beforeMove(
|
|
|
680
634
|
try log.start(battle.active(player), .ConfusionSilent);
|
|
681
635
|
}
|
|
682
636
|
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
|
-
}
|
|
637
|
+
if (showdown and handleThrashing(battle, active)) {
|
|
638
|
+
try log.start(battle.active(player), .ConfusionSilent);
|
|
690
639
|
}
|
|
691
640
|
return .skip_can;
|
|
692
641
|
}
|
|
@@ -701,11 +650,7 @@ fn beforeMove(
|
|
|
701
650
|
side.last_selected_move,
|
|
702
651
|
);
|
|
703
652
|
if (showdown or battle.last_damage != 0) {
|
|
704
|
-
const sub = showdown and foe.active.volatiles.Substitute;
|
|
705
653
|
_ = 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
654
|
}
|
|
710
655
|
return .done;
|
|
711
656
|
}
|
|
@@ -737,7 +682,6 @@ fn canMove(
|
|
|
737
682
|
side.active.volatiles.Invulnerable = false;
|
|
738
683
|
} else if (move.effect == .Charge) {
|
|
739
684
|
try log.move(player_ident, side.last_selected_move, .{}, from);
|
|
740
|
-
setCounterable(battle, player, side, move);
|
|
741
685
|
try Effects.charge(battle, player, log);
|
|
742
686
|
return false;
|
|
743
687
|
}
|
|
@@ -754,7 +698,6 @@ fn canMove(
|
|
|
754
698
|
// regrettable, but this information is more "nice to have" than required
|
|
755
699
|
const f = if (from != null and from.? == .None) null else from;
|
|
756
700
|
try log.move(player_ident, side.last_selected_move, battle.active(target), f);
|
|
757
|
-
setCounterable(battle, player, side, move);
|
|
758
701
|
|
|
759
702
|
if (move.effect.onBegin()) {
|
|
760
703
|
try onBegin(battle, player, move, mslot, residual, log);
|
|
@@ -765,27 +708,12 @@ fn canMove(
|
|
|
765
708
|
Effects.thrashing(battle, player);
|
|
766
709
|
} else if (!showdown and move.effect == .Binding) {
|
|
767
710
|
// Pokémon Showdown handles this after hit/miss checks and damage calculation
|
|
768
|
-
Effects.binding(battle, player
|
|
711
|
+
Effects.binding(battle, player);
|
|
769
712
|
}
|
|
770
713
|
|
|
771
714
|
return true;
|
|
772
715
|
}
|
|
773
716
|
|
|
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
717
|
fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
|
|
790
718
|
if (side.last_selected_move == .Struggle) return;
|
|
791
719
|
|
|
@@ -821,7 +749,6 @@ fn doMove(
|
|
|
821
749
|
battle: anytype,
|
|
822
750
|
player: Player,
|
|
823
751
|
mslot: u4,
|
|
824
|
-
rewrap: bool,
|
|
825
752
|
auto: bool,
|
|
826
753
|
residual: *bool,
|
|
827
754
|
log: anytype,
|
|
@@ -913,9 +840,9 @@ fn doMove(
|
|
|
913
840
|
|
|
914
841
|
if (!showdown or !miss) {
|
|
915
842
|
if (move.effect == .MirrorMove) {
|
|
916
|
-
return mirrorMove(battle, player, mslot,
|
|
843
|
+
return mirrorMove(battle, player, mslot, auto, residual, log);
|
|
917
844
|
} else if (move.effect == .Metronome) {
|
|
918
|
-
return metronome(battle, player, mslot,
|
|
845
|
+
return metronome(battle, player, mslot, auto, residual, log);
|
|
919
846
|
} else if (move.effect.onEnd()) {
|
|
920
847
|
try onEnd(battle, player, move, log);
|
|
921
848
|
return null;
|
|
@@ -930,7 +857,7 @@ fn doMove(
|
|
|
930
857
|
move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
|
|
931
858
|
if (ohko) {
|
|
932
859
|
try log.immune(foe_ident, .OHKO);
|
|
933
|
-
} else if (immune and !invulnerable
|
|
860
|
+
} else if (immune and !invulnerable) {
|
|
934
861
|
try log.immune(foe_ident, .None);
|
|
935
862
|
} else if (mist) {
|
|
936
863
|
if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
|
|
@@ -996,17 +923,11 @@ fn doMove(
|
|
|
996
923
|
} else if (showdown) {
|
|
997
924
|
// This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
|
|
998
925
|
if (move.effect == .Binding) {
|
|
999
|
-
Effects.binding(battle, player
|
|
926
|
+
Effects.binding(battle, player);
|
|
1000
927
|
if (immune) {
|
|
1001
928
|
battle.last_damage = 0;
|
|
1002
|
-
assert(foe.stored().hp > 0);
|
|
1003
929
|
// 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
|
-
}
|
|
930
|
+
try log.damage(battle.active(player.foe()), foe.stored(), .None);
|
|
1010
931
|
return null;
|
|
1011
932
|
}
|
|
1012
933
|
}
|
|
@@ -1203,17 +1124,19 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
|
|
|
1203
1124
|
return null;
|
|
1204
1125
|
}
|
|
1205
1126
|
|
|
1206
|
-
// Pretend
|
|
1127
|
+
// Pretend Counter was used as a stand-in when no move has been used to fail below with 0 BP
|
|
1128
|
+
const foe_last_used_move =
|
|
1129
|
+
Move.get(if (foe.last_used_move == .None) .Counter else foe.last_used_move);
|
|
1207
1130
|
const foe_last_selected_move =
|
|
1208
1131
|
Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
|
|
1209
|
-
.
|
|
1132
|
+
.Counter
|
|
1210
1133
|
else
|
|
1211
1134
|
foe.last_selected_move);
|
|
1212
1135
|
|
|
1213
|
-
const used =
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1136
|
+
const used = foe_last_used_move.bp > 0 and
|
|
1137
|
+
foe.last_used_move != .Counter and
|
|
1138
|
+
(foe_last_used_move.type == .Normal or
|
|
1139
|
+
foe_last_used_move.type == .Fighting);
|
|
1217
1140
|
|
|
1218
1141
|
const selected = foe_last_selected_move.bp > 0 and
|
|
1219
1142
|
foe.last_selected_move != .Counter and
|
|
@@ -1232,14 +1155,12 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
|
|
|
1232
1155
|
return null;
|
|
1233
1156
|
}
|
|
1234
1157
|
|
|
1235
|
-
battle.last_damage
|
|
1158
|
+
battle.last_damage = if (battle.last_damage > 0x7FFF) 0xFFFF else battle.last_damage * 2;
|
|
1236
1159
|
|
|
1237
1160
|
// Pokémon Showdown calls checkHit before Counter
|
|
1238
1161
|
if (!showdown and !try checkHit(battle, player, move, log)) return null;
|
|
1239
1162
|
|
|
1240
|
-
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1241
1163
|
_ = 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
1164
|
return null;
|
|
1244
1165
|
}
|
|
1245
1166
|
|
|
@@ -1283,7 +1204,6 @@ fn mirrorMove(
|
|
|
1283
1204
|
battle: anytype,
|
|
1284
1205
|
player: Player,
|
|
1285
1206
|
mslot: u4,
|
|
1286
|
-
rewrap: bool,
|
|
1287
1207
|
auto: bool,
|
|
1288
1208
|
residual: *bool,
|
|
1289
1209
|
log: anytype,
|
|
@@ -1296,19 +1216,18 @@ fn mirrorMove(
|
|
|
1296
1216
|
if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
|
|
1297
1217
|
try log.fail(battle.active(player), .None);
|
|
1298
1218
|
return null;
|
|
1299
|
-
} else if (!showdown or foe.last_used_move != .Struggle) {
|
|
1300
|
-
incrementPP(side, mslot);
|
|
1301
1219
|
}
|
|
1302
1220
|
|
|
1221
|
+
incrementPP(side, mslot);
|
|
1222
|
+
|
|
1303
1223
|
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
|
|
1304
|
-
return doMove(battle, player, mslot,
|
|
1224
|
+
return doMove(battle, player, mslot, auto, residual, log);
|
|
1305
1225
|
}
|
|
1306
1226
|
|
|
1307
1227
|
fn metronome(
|
|
1308
1228
|
battle: anytype,
|
|
1309
1229
|
player: Player,
|
|
1310
1230
|
mslot: u4,
|
|
1311
|
-
rewrap: bool,
|
|
1312
1231
|
auto: bool,
|
|
1313
1232
|
residual: *bool,
|
|
1314
1233
|
log: anytype,
|
|
@@ -1319,7 +1238,7 @@ fn metronome(
|
|
|
1319
1238
|
incrementPP(side, mslot);
|
|
1320
1239
|
|
|
1321
1240
|
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
|
|
1322
|
-
return doMove(battle, player, mslot,
|
|
1241
|
+
return doMove(battle, player, mslot, auto, residual, log);
|
|
1323
1242
|
}
|
|
1324
1243
|
|
|
1325
1244
|
fn checkHit(battle: anytype, player: Player, move: Move.Data, log: anytype) !bool {
|
|
@@ -1526,7 +1445,11 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
|
|
|
1526
1445
|
stored.hp -= amount;
|
|
1527
1446
|
|
|
1528
1447
|
// As above, Pokémon Showdown uses damageOf but its not relevant
|
|
1529
|
-
if (amount > 0)
|
|
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
|
+
}
|
|
1530
1453
|
|
|
1531
1454
|
const before = foe_stored.hp;
|
|
1532
1455
|
// Uncapped damage is added back to the foe
|
|
@@ -1540,13 +1463,13 @@ fn endTurn(battle: anytype, log: anytype) @TypeOf(log).Error!Result {
|
|
|
1540
1463
|
assert(!battle.side(.P1).active.volatiles.MultiHit);
|
|
1541
1464
|
assert(!battle.side(.P2).active.volatiles.MultiHit);
|
|
1542
1465
|
|
|
1543
|
-
battle.turn += 1;
|
|
1544
|
-
|
|
1545
1466
|
if (showdown and options.ebc and checkEBC(battle)) {
|
|
1546
1467
|
try log.tie();
|
|
1547
1468
|
return Result.Tie;
|
|
1548
1469
|
}
|
|
1549
1470
|
|
|
1471
|
+
battle.turn += 1;
|
|
1472
|
+
|
|
1550
1473
|
if (showdown and battle.turn >= 1000) {
|
|
1551
1474
|
try log.tie();
|
|
1552
1475
|
return Result.Tie;
|
|
@@ -2059,9 +1982,9 @@ pub const Effects = struct {
|
|
|
2059
1982
|
}
|
|
2060
1983
|
break :has_mimic false;
|
|
2061
1984
|
};
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
1985
|
+
// If the foe is Invulnerable we still want to fall through to checkHit to be able to
|
|
1986
|
+
// trigger |-miss| instead of |-fail|
|
|
1987
|
+
if (!has_mimic and !foe.active.volatiles.Invulnerable) {
|
|
2065
1988
|
return try log.fail(battle.active(player.foe()), .None);
|
|
2066
1989
|
}
|
|
2067
1990
|
}
|
|
@@ -2329,7 +2252,7 @@ pub const Effects = struct {
|
|
|
2329
2252
|
try log.transform(battle.active(player), foe_ident);
|
|
2330
2253
|
}
|
|
2331
2254
|
|
|
2332
|
-
fn binding(battle: anytype, player: Player
|
|
2255
|
+
fn binding(battle: anytype, player: Player) void {
|
|
2333
2256
|
var side = battle.side(player);
|
|
2334
2257
|
var foe = battle.foe(player);
|
|
2335
2258
|
|
|
@@ -2337,9 +2260,7 @@ pub const Effects = struct {
|
|
|
2337
2260
|
side.active.volatiles.Binding = true;
|
|
2338
2261
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2339
2262
|
// (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);
|
|
2263
|
+
if (!showdown) foe.active.volatiles.Recharging = false;
|
|
2343
2264
|
|
|
2344
2265
|
side.active.volatiles.attacks = Rolls.distribution(battle) - 1;
|
|
2345
2266
|
}
|
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 trace 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.trace` 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
|
-
///
|
|
44
|
+
/// `std.io.null_writer`. Ignores anything sent to it, though trace logging
|
|
45
|
+
/// should addtionally be turned off entirely by using `options.trace`.
|
|
46
46
|
pub const NULL = @import("common/protocol.zig").NULL;
|
|
47
47
|
};
|
|
48
48
|
|