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