@pkmn/engine 0.1.0-dev.a324c50f → 0.1.0-dev.a93847b2
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 +45 -37
- package/build/pkg/data/ids.json +12 -12
- package/build/pkg/data/layout.json +2 -2
- package/build/pkg/data/protocol.json +69 -12
- package/build/pkg/data.d.ts +2 -2
- package/build/pkg/gen1.js +2 -2
- package/build/pkg/gen1.js.map +1 -1
- package/build/pkg/protocol.js +56 -37
- package/build/pkg/protocol.js.map +1 -1
- package/build.zig +53 -44
- package/package.json +40 -19
- package/src/bin/install-pkmn-engine +2 -2
- package/src/lib/binding/node.zig +1 -1
- package/src/lib/common/battle.zig +17 -0
- package/src/lib/common/data.zig +4 -16
- package/src/lib/common/js.zig +118 -0
- package/src/lib/common/optional.zig +67 -0
- package/src/lib/common/protocol.zig +561 -80
- package/src/lib/common/rational.zig +353 -0
- package/src/lib/common/rng.zig +6 -6
- package/src/lib/common/util.zig +12 -0
- package/src/lib/gen1/README.md +273 -193
- package/src/lib/gen1/calc.zig +426 -0
- package/src/lib/gen1/chance.zig +1060 -0
- package/src/lib/gen1/data/moves.zig +183 -171
- package/src/lib/gen1/data/species.zig +1 -1
- package/src/lib/gen1/data/types.zig +3 -3
- package/src/lib/gen1/data.zig +31 -38
- package/src/lib/gen1/helpers.zig +241 -103
- package/src/lib/gen1/mechanics.zig +111 -166
- package/src/lib/gen2/README.md +106 -85
- package/src/lib/gen2/calc.zig +143 -0
- package/src/lib/gen2/chance.zig +1314 -0
- package/src/lib/gen2/data/items.zig +11 -17
- package/src/lib/gen2/data/moves.zig +662 -568
- package/src/lib/gen2/data/species.zig +1 -1
- package/src/lib/gen2/data/types.zig +129 -26
- package/src/lib/gen2/data.zig +89 -64
- package/src/lib/gen2/generated.zig +857 -0
- package/src/lib/gen2/helpers.zig +8 -12
- package/src/lib/gen2/mechanics.zig +2994 -9
|
@@ -7,21 +7,19 @@ const DEBUG = @import("../common/debug.zig").print;
|
|
|
7
7
|
const protocol = @import("../common/protocol.zig");
|
|
8
8
|
const rng = @import("../common/rng.zig");
|
|
9
9
|
|
|
10
|
-
const Action = @import("chance.zig").Action;
|
|
11
|
-
|
|
12
10
|
const data = @import("data.zig");
|
|
13
11
|
|
|
14
12
|
const assert = std.debug.assert;
|
|
15
13
|
|
|
16
14
|
const expectEqual = std.testing.expectEqual;
|
|
17
15
|
|
|
16
|
+
const showdown = pkmn.options.showdown;
|
|
17
|
+
|
|
18
18
|
const Choice = common.Choice;
|
|
19
19
|
const ID = common.ID;
|
|
20
20
|
const Player = common.Player;
|
|
21
21
|
const Result = common.Result;
|
|
22
22
|
|
|
23
|
-
const showdown = pkmn.options.showdown;
|
|
24
|
-
|
|
25
23
|
const Boost = protocol.Boost;
|
|
26
24
|
const Damage = protocol.Damage;
|
|
27
25
|
const Heal = protocol.Heal;
|
|
@@ -59,23 +57,15 @@ const BOOSTS = &[_][2]u8{
|
|
|
59
57
|
|
|
60
58
|
const MAX_STAT_VALUE = 999;
|
|
61
59
|
|
|
62
|
-
const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
|
|
63
|
-
|
|
64
60
|
pub fn update(battle: anytype, c1: Choice, c2: Choice, options: anytype) !Result {
|
|
65
|
-
// FIXME
|
|
66
|
-
// std.debug.print("\n", .{});
|
|
67
|
-
// DEBUG(battle.turn);
|
|
68
|
-
// std.debug.print("// ", .{});
|
|
69
61
|
assert(c1.type != .Pass or c2.type != .Pass or battle.turn == 0);
|
|
70
62
|
if (battle.turn == 0) return start(battle, options);
|
|
71
63
|
|
|
72
64
|
var s1 = false;
|
|
73
65
|
var s2 = false;
|
|
74
|
-
var f1: ?Move = null;
|
|
75
|
-
var f2: ?Move = null;
|
|
76
66
|
|
|
77
|
-
if (selectMove(battle, .P1, c1, c2, &s1
|
|
78
|
-
if (selectMove(battle, .P2, c2, c1, &s2
|
|
67
|
+
if (selectMove(battle, .P1, c1, c2, &s1)) |r| return r;
|
|
68
|
+
if (selectMove(battle, .P2, c2, c1, &s2)) |r| return r;
|
|
79
69
|
|
|
80
70
|
var p1 = battle.side(.P1);
|
|
81
71
|
var p2 = battle.side(.P2);
|
|
@@ -84,9 +74,9 @@ pub fn update(battle: anytype, c1: Choice, c2: Choice, options: anytype) !Result
|
|
|
84
74
|
var r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
|
|
85
75
|
|
|
86
76
|
if (try turnOrder(battle, c1, c2, options) == .P1) {
|
|
87
|
-
if (try doTurn(battle, .P1, c1, r1, s1,
|
|
77
|
+
if (try doTurn(battle, .P1, c1, r1, s1, .P2, c2, r2, s2, options)) |r| return r;
|
|
88
78
|
} else {
|
|
89
|
-
if (try doTurn(battle, .P2, c2, r2, s2,
|
|
79
|
+
if (try doTurn(battle, .P2, c2, r2, s2, .P1, c1, r1, s1, options)) |r| return r;
|
|
90
80
|
}
|
|
91
81
|
|
|
92
82
|
if (p1.active.volatiles.attacks == 0) p1.active.volatiles.Binding = false;
|
|
@@ -124,7 +114,6 @@ fn selectMove(
|
|
|
124
114
|
choice: Choice,
|
|
125
115
|
foe_choice: Choice,
|
|
126
116
|
skip_turn: *bool,
|
|
127
|
-
from: *?Move,
|
|
128
117
|
) ?Result {
|
|
129
118
|
if (choice.type == .Pass) return null;
|
|
130
119
|
|
|
@@ -141,7 +130,6 @@ fn selectMove(
|
|
|
141
130
|
return null;
|
|
142
131
|
}
|
|
143
132
|
if (volatiles.Rage) {
|
|
144
|
-
from.* = side.last_used_move;
|
|
145
133
|
if (showdown) {
|
|
146
134
|
if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
|
|
147
135
|
saveMove(battle, player, null);
|
|
@@ -151,7 +139,6 @@ fn selectMove(
|
|
|
151
139
|
// Pokémon Showdown removes Flinch at the end-of-turn in its residual handler
|
|
152
140
|
if (!showdown) volatiles.Flinch = false;
|
|
153
141
|
if (volatiles.Thrashing or volatiles.Charging) {
|
|
154
|
-
from.* = side.last_selected_move;
|
|
155
142
|
if (showdown) {
|
|
156
143
|
if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
|
|
157
144
|
saveMove(battle, player, null);
|
|
@@ -172,9 +159,7 @@ fn selectMove(
|
|
|
172
159
|
return null;
|
|
173
160
|
}
|
|
174
161
|
if (volatiles.Binding) {
|
|
175
|
-
from.* = side.last_used_move;
|
|
176
162
|
if (showdown) {
|
|
177
|
-
if (foe_choice.type == .Switch) from.* = null;
|
|
178
163
|
// Pokémon Showdown overwrites Mirror Move with whatever was selected - really this
|
|
179
164
|
// should set side.last_selected_move = last.id to reuse Mirror Move and fail in order
|
|
180
165
|
// to satisfy the conditions of the Desync Clause Mod. However, because Binding is still
|
|
@@ -186,10 +171,7 @@ fn selectMove(
|
|
|
186
171
|
assert(choice.data == 0);
|
|
187
172
|
// GLITCH: https://glitchcity.wiki/Partial_trapping_move_Mirror_Move_link_battle_glitch
|
|
188
173
|
if (foe_choice.type == .Switch) {
|
|
189
|
-
const last = side.active.move(
|
|
190
|
-
battle.last_moves.p1_index
|
|
191
|
-
else
|
|
192
|
-
battle.last_moves.p2_index);
|
|
174
|
+
const last = side.active.move(battle.lastMove(player).index);
|
|
193
175
|
if (last.id == .Metronome) side.last_selected_move = last.id;
|
|
194
176
|
if (last.id == .MirrorMove) return Result.Error;
|
|
195
177
|
}
|
|
@@ -213,16 +195,13 @@ fn selectMove(
|
|
|
213
195
|
if (choice.data == 0) {
|
|
214
196
|
const struggle = ok: {
|
|
215
197
|
for (side.active.moves, 0..) |move, i| {
|
|
216
|
-
if (move.pp > 0 and volatiles.
|
|
198
|
+
if (move.pp > 0 and volatiles.disable_move != i + 1) break :ok false;
|
|
217
199
|
}
|
|
218
200
|
break :ok true;
|
|
219
201
|
};
|
|
220
|
-
|
|
221
202
|
assert(struggle);
|
|
222
|
-
saveMove(battle, player, choice);
|
|
223
|
-
} else {
|
|
224
|
-
saveMove(battle, player, choice);
|
|
225
203
|
}
|
|
204
|
+
saveMove(battle, player, choice);
|
|
226
205
|
|
|
227
206
|
return null;
|
|
228
207
|
}
|
|
@@ -235,18 +214,14 @@ fn saveMove(battle: anytype, player: Player, choice: ?Choice) void {
|
|
|
235
214
|
if (c.data == 0) {
|
|
236
215
|
side.last_selected_move = .Struggle;
|
|
237
216
|
} else {
|
|
238
|
-
assert(showdown or side.active.volatiles.
|
|
217
|
+
assert(showdown or side.active.volatiles.disable_move != c.data);
|
|
239
218
|
const move = side.active.move(c.data);
|
|
240
219
|
// You cannot *select* a move with 0 PP (except on Pokémon Showdown where that is
|
|
241
220
|
// sometimes required...), but a 0 PP move can be used automatically
|
|
242
221
|
assert(showdown or move.pp != 0);
|
|
243
222
|
|
|
244
223
|
side.last_selected_move = move.id;
|
|
245
|
-
|
|
246
|
-
battle.last_moves.p1_index = @intCast(c.data);
|
|
247
|
-
} else {
|
|
248
|
-
battle.last_moves.p2_index = @intCast(c.data);
|
|
249
|
-
}
|
|
224
|
+
battle.lastMove(player).index = @intCast(c.data);
|
|
250
225
|
}
|
|
251
226
|
}
|
|
252
227
|
}
|
|
@@ -264,11 +239,7 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: a
|
|
|
264
239
|
side.order[0] = side.order[slot - 1];
|
|
265
240
|
side.order[slot - 1] = out;
|
|
266
241
|
|
|
267
|
-
|
|
268
|
-
battle.last_moves.p1_index = 1;
|
|
269
|
-
} else {
|
|
270
|
-
battle.last_moves.p2_index = 1;
|
|
271
|
-
}
|
|
242
|
+
battle.lastMove(player).index = 1;
|
|
272
243
|
|
|
273
244
|
side.last_used_move = .None;
|
|
274
245
|
foe.last_used_move = .None;
|
|
@@ -354,12 +325,10 @@ fn doTurn(
|
|
|
354
325
|
player_choice: Choice,
|
|
355
326
|
player_rewrap: bool,
|
|
356
327
|
player_skip: bool,
|
|
357
|
-
player_from: ?Move,
|
|
358
328
|
foe_player: Player,
|
|
359
329
|
foe_choice: Choice,
|
|
360
330
|
foe_rewrap: bool,
|
|
361
331
|
foe_skip: bool,
|
|
362
|
-
foe_from: ?Move,
|
|
363
332
|
options: anytype,
|
|
364
333
|
) !?Result {
|
|
365
334
|
assert(player_choice.type != .Pass);
|
|
@@ -372,7 +341,6 @@ fn doTurn(
|
|
|
372
341
|
player_choice,
|
|
373
342
|
player_rewrap,
|
|
374
343
|
player_skip,
|
|
375
|
-
player_from,
|
|
376
344
|
&residual,
|
|
377
345
|
options,
|
|
378
346
|
)) |r| return r;
|
|
@@ -395,7 +363,6 @@ fn doTurn(
|
|
|
395
363
|
foe_choice,
|
|
396
364
|
foe_rewrap,
|
|
397
365
|
foe_skip,
|
|
398
|
-
foe_from,
|
|
399
366
|
&residual,
|
|
400
367
|
options,
|
|
401
368
|
)) |r| return r;
|
|
@@ -423,7 +390,6 @@ fn executeMove(
|
|
|
423
390
|
choice: Choice,
|
|
424
391
|
rewrap: bool,
|
|
425
392
|
skip: bool,
|
|
426
|
-
from: ?Move,
|
|
427
393
|
residual: *bool,
|
|
428
394
|
options: anytype,
|
|
429
395
|
) !?Result {
|
|
@@ -455,14 +421,13 @@ fn executeMove(
|
|
|
455
421
|
if (mslot == 0 and side.last_selected_move != .None and side.last_selected_move != .Struggle) {
|
|
456
422
|
// choice.data == 0 only happens with Struggle on Pokémon Showdown
|
|
457
423
|
assert(!showdown);
|
|
458
|
-
mslot = @intCast(
|
|
459
|
-
battle.last_moves.p1_index
|
|
460
|
-
else
|
|
461
|
-
battle.last_moves.p2_index);
|
|
424
|
+
mslot = @intCast(battle.lastMove(player).index);
|
|
462
425
|
const stored = side.stored();
|
|
463
426
|
// GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
|
|
464
|
-
auto = side.
|
|
465
|
-
|
|
427
|
+
auto = isForced(&side.active) or
|
|
428
|
+
side.active.volatiles.Binding or side.active.volatiles.Bide or
|
|
429
|
+
side.last_selected_move == .HyperBeam or
|
|
430
|
+
Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
|
|
466
431
|
// If it wasn't Hyper Beam or the continuation of a move effect then we must have just
|
|
467
432
|
// thawed, in which case we will desync unless the last_selected_move happened to be at
|
|
468
433
|
// index 1 and the current Pokémon has the same move in its first slot.
|
|
@@ -479,15 +444,12 @@ fn executeMove(
|
|
|
479
444
|
} else if (showdown and side.active.volatiles.Charging) {
|
|
480
445
|
// Incorrect mslot with Pokémon Showdown choice semantics so we need to recover from index
|
|
481
446
|
assert(mslot == 1);
|
|
482
|
-
mslot = @intCast(
|
|
483
|
-
battle.last_moves.p1_index
|
|
484
|
-
else
|
|
485
|
-
battle.last_moves.p2_index);
|
|
447
|
+
mslot = @intCast(battle.lastMove(player).index);
|
|
486
448
|
}
|
|
487
449
|
|
|
488
450
|
var skip_can = false;
|
|
489
451
|
var skip_pp = false;
|
|
490
|
-
switch (try beforeMove(battle, player, skip,
|
|
452
|
+
switch (try beforeMove(battle, player, skip, residual, options)) {
|
|
491
453
|
.done => return null,
|
|
492
454
|
.skip_can => skip_can = true,
|
|
493
455
|
.skip_pp => skip_pp = true,
|
|
@@ -495,9 +457,9 @@ fn executeMove(
|
|
|
495
457
|
.err => return @as(?Result, Result.Error),
|
|
496
458
|
}
|
|
497
459
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
460
|
+
const can = skip_can or
|
|
461
|
+
try canMove(battle, player, mslot, auto, skip_pp, .None, residual, options);
|
|
462
|
+
if (!can) return null;
|
|
501
463
|
|
|
502
464
|
return doMove(battle, player, mslot, rewrap, auto, residual, options);
|
|
503
465
|
}
|
|
@@ -508,7 +470,6 @@ fn beforeMove(
|
|
|
508
470
|
battle: anytype,
|
|
509
471
|
player: Player,
|
|
510
472
|
skip: bool,
|
|
511
|
-
from: ?Move,
|
|
512
473
|
residual: *bool,
|
|
513
474
|
options: anytype,
|
|
514
475
|
) !BeforeMove {
|
|
@@ -569,16 +530,16 @@ fn beforeMove(
|
|
|
569
530
|
return .done;
|
|
570
531
|
}
|
|
571
532
|
|
|
572
|
-
if (volatiles.
|
|
533
|
+
if (volatiles.disable_duration > 0) {
|
|
573
534
|
if (options.calc.modify(player, .disable)) |extend| {
|
|
574
|
-
if (!extend) volatiles.
|
|
535
|
+
if (!extend) volatiles.disable_duration = 0;
|
|
575
536
|
} else {
|
|
576
|
-
volatiles.
|
|
537
|
+
volatiles.disable_duration -= 1;
|
|
577
538
|
}
|
|
578
|
-
try options.chance.disable(player, volatiles.
|
|
539
|
+
try options.chance.disable(player, volatiles.disable_duration);
|
|
579
540
|
|
|
580
|
-
if (volatiles.
|
|
581
|
-
volatiles.
|
|
541
|
+
if (volatiles.disable_duration == 0) {
|
|
542
|
+
volatiles.disable_move = 0;
|
|
582
543
|
try log.end(ident, .Disable);
|
|
583
544
|
}
|
|
584
545
|
}
|
|
@@ -715,7 +676,6 @@ fn beforeMove(
|
|
|
715
676
|
|
|
716
677
|
if (volatiles.Thrashing) {
|
|
717
678
|
assert(volatiles.attacks > 0);
|
|
718
|
-
assert(from != null);
|
|
719
679
|
|
|
720
680
|
if (options.calc.modify(player, .attacking)) |extend| {
|
|
721
681
|
if (!extend) volatiles.attacks = 0;
|
|
@@ -727,7 +687,7 @@ fn beforeMove(
|
|
|
727
687
|
if (!showdown and handleThrashing(battle, active, player, options)) {
|
|
728
688
|
try log.start(battle.active(player), .ConfusionSilent);
|
|
729
689
|
}
|
|
730
|
-
try log.move(ident, side.last_selected_move, battle.active(player.foe())
|
|
690
|
+
try log.move(ident, side.last_selected_move, battle.active(player.foe()));
|
|
731
691
|
if (showdown) {
|
|
732
692
|
// This shouldn't actually set last_used_move, but Pokémon Showdown sets last
|
|
733
693
|
// used in useMove and doesn't have the notion of skipping canMove semantics
|
|
@@ -749,12 +709,7 @@ fn beforeMove(
|
|
|
749
709
|
}
|
|
750
710
|
try options.chance.binding(player, volatiles.attacks);
|
|
751
711
|
|
|
752
|
-
try log.move(
|
|
753
|
-
battle.active(player),
|
|
754
|
-
side.last_selected_move,
|
|
755
|
-
battle.active(player.foe()),
|
|
756
|
-
side.last_selected_move,
|
|
757
|
-
);
|
|
712
|
+
try log.move(battle.active(player), side.last_selected_move, battle.active(player.foe()));
|
|
758
713
|
if (showdown or battle.last_damage != 0) {
|
|
759
714
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
760
715
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
@@ -774,7 +729,7 @@ fn canMove(
|
|
|
774
729
|
mslot: u4,
|
|
775
730
|
auto: bool,
|
|
776
731
|
skip_pp: bool,
|
|
777
|
-
from:
|
|
732
|
+
from: Move,
|
|
778
733
|
residual: *bool,
|
|
779
734
|
options: anytype,
|
|
780
735
|
) !bool {
|
|
@@ -782,33 +737,22 @@ fn canMove(
|
|
|
782
737
|
const player_ident = battle.active(player);
|
|
783
738
|
const move = Move.get(side.last_selected_move);
|
|
784
739
|
|
|
785
|
-
const foe_last = battle.foe(player).last_used_move;
|
|
786
|
-
const special = from != null and (from.? == .Metronome and
|
|
787
|
-
(from.? == .MirrorMove and !(foe_last == .None or foe_last == .MirrorMove)));
|
|
788
|
-
|
|
789
740
|
var skip = skip_pp;
|
|
790
741
|
if (side.active.volatiles.Charging) {
|
|
791
742
|
side.active.volatiles.Charging = false;
|
|
792
743
|
side.active.volatiles.Invulnerable = false;
|
|
793
744
|
} else if (move.effect == .Charge) {
|
|
794
|
-
try options.log.
|
|
745
|
+
try options.log.moveFrom(player_ident, side.last_selected_move, .{}, from);
|
|
795
746
|
setCounterable(battle, player, side, move);
|
|
796
747
|
try Effects.charge(battle, player, options);
|
|
797
748
|
return false;
|
|
798
749
|
}
|
|
799
750
|
|
|
800
|
-
|
|
751
|
+
side.last_used_move = side.last_selected_move;
|
|
801
752
|
if (!skip) decrementPP(side, mslot, auto);
|
|
802
753
|
|
|
803
754
|
const target = if (move.target == .Self) player else player.foe();
|
|
804
|
-
|
|
805
|
-
// don't have space to be able to track it in all circumstances and thus usually try to infer it
|
|
806
|
-
// from side.last_used_move. However, this gets reset by the opponent switching or fainting,
|
|
807
|
-
// meaning sometimes we lose this information. from is still set because it not being null is
|
|
808
|
-
// relevant for control flow, but we don't have anything useful to put in the log. This is
|
|
809
|
-
// regrettable, but this information is more "nice to have" than required
|
|
810
|
-
const f = if (from != null and from.? == .None) null else from;
|
|
811
|
-
try options.log.move(player_ident, side.last_selected_move, battle.active(target), f);
|
|
755
|
+
try options.log.moveFrom(player_ident, side.last_selected_move, battle.active(target), from);
|
|
812
756
|
setCounterable(battle, player, side, move);
|
|
813
757
|
|
|
814
758
|
if (move.effect.onBegin()) {
|
|
@@ -834,11 +778,7 @@ fn setCounterable(battle: anytype, player: Player, side: *Side, move: Move.Data)
|
|
|
834
778
|
// as the desync results from actually having two separate battle states that subtly disagree
|
|
835
779
|
const counterable = side.last_selected_move != .Counter and move.bp > 0 and
|
|
836
780
|
(move.type == .Normal or move.type == .Fighting);
|
|
837
|
-
|
|
838
|
-
battle.last_moves.p1_counterable = @intFromBool(counterable);
|
|
839
|
-
} else {
|
|
840
|
-
battle.last_moves.p2_counterable = @intFromBool(counterable);
|
|
841
|
-
}
|
|
781
|
+
battle.lastMove(player).counterable = @intFromBool(counterable);
|
|
842
782
|
}
|
|
843
783
|
|
|
844
784
|
fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
|
|
@@ -850,12 +790,14 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
|
|
|
850
790
|
assert(!volatiles.Rage and !volatiles.Thrashing and !volatiles.MultiHit);
|
|
851
791
|
if (volatiles.Bide) return;
|
|
852
792
|
|
|
853
|
-
|
|
854
|
-
|
|
793
|
+
var move_slot = active.move(mslot);
|
|
794
|
+
assert(move_slot.pp > 0 or auto);
|
|
795
|
+
move_slot.pp = @as(u6, @intCast(move_slot.pp)) -% 1;
|
|
855
796
|
if (volatiles.Transform) return;
|
|
856
797
|
|
|
857
|
-
|
|
858
|
-
|
|
798
|
+
move_slot = side.stored().move(mslot);
|
|
799
|
+
assert(move_slot.pp > 0 or auto);
|
|
800
|
+
move_slot.pp = @as(u6, @intCast(move_slot.pp)) -% 1;
|
|
859
801
|
assert(active.move(mslot).pp == side.stored().move(mslot).pp);
|
|
860
802
|
}
|
|
861
803
|
|
|
@@ -1278,11 +1220,7 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1278
1220
|
else
|
|
1279
1221
|
foe.last_selected_move);
|
|
1280
1222
|
|
|
1281
|
-
const used = (
|
|
1282
|
-
battle.last_moves.p1_counterable
|
|
1283
|
-
else
|
|
1284
|
-
battle.last_moves.p2_counterable) != 0;
|
|
1285
|
-
|
|
1223
|
+
const used = battle.lastMove(player.foe()).counterable != 0;
|
|
1286
1224
|
const selected = foe_last_selected_move.bp > 0 and
|
|
1287
1225
|
foe.last_selected_move != .Counter and
|
|
1288
1226
|
(foe_last_selected_move.type == .Normal or
|
|
@@ -1357,7 +1295,7 @@ fn mirrorMove(
|
|
|
1357
1295
|
auto: bool,
|
|
1358
1296
|
residual: *bool,
|
|
1359
1297
|
options: anytype,
|
|
1360
|
-
) !?Result {
|
|
1298
|
+
) anyerror!?Result {
|
|
1361
1299
|
var side = battle.side(player);
|
|
1362
1300
|
const foe = battle.foe(player);
|
|
1363
1301
|
|
|
@@ -1384,7 +1322,7 @@ fn metronome(
|
|
|
1384
1322
|
auto: bool,
|
|
1385
1323
|
residual: *bool,
|
|
1386
1324
|
options: anytype,
|
|
1387
|
-
) !?Result {
|
|
1325
|
+
) anyerror!?Result {
|
|
1388
1326
|
var side = battle.side(player);
|
|
1389
1327
|
|
|
1390
1328
|
side.last_selected_move = try Rolls.metronome(battle, player, options);
|
|
@@ -1463,7 +1401,7 @@ fn moveHit(
|
|
|
1463
1401
|
const overwritten = overwrite and state > 0;
|
|
1464
1402
|
assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
|
|
1465
1403
|
|
|
1466
|
-
var accuracy = if (overwritten) state else @as(u16,
|
|
1404
|
+
var accuracy = if (overwritten) state else @as(u16, move.accuracy);
|
|
1467
1405
|
var boost = BOOSTS[@as(u4, @intCast(@as(i8, side.active.boosts.accuracy) + 6))];
|
|
1468
1406
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1469
1407
|
boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
|
|
@@ -1502,9 +1440,8 @@ fn checkFaint(
|
|
|
1502
1440
|
const foe_fainted = foe.stored().hp == 0;
|
|
1503
1441
|
|
|
1504
1442
|
const player_out = findFirstAlive(side) == 0;
|
|
1505
|
-
const foe_out = findFirstAlive(foe) == 0;
|
|
1506
|
-
const
|
|
1507
|
-
const more = tie or player_out or foe_out;
|
|
1443
|
+
const foe_out = foe_fainted and findFirstAlive(foe) == 0;
|
|
1444
|
+
const more = player_out or foe_out;
|
|
1508
1445
|
|
|
1509
1446
|
if (try faint(battle, player, !(more or foe_fainted), options)) |r| return r;
|
|
1510
1447
|
if (foe_fainted) if (try faint(battle, player.foe(), !more, options)) |r| return r;
|
|
@@ -1512,7 +1449,7 @@ fn checkFaint(
|
|
|
1512
1449
|
assert(!side.active.volatiles.MultiHit);
|
|
1513
1450
|
assert(!foe.active.volatiles.MultiHit);
|
|
1514
1451
|
|
|
1515
|
-
if (
|
|
1452
|
+
if (player_out and foe_out) {
|
|
1516
1453
|
try options.log.tie();
|
|
1517
1454
|
return Result.Tie;
|
|
1518
1455
|
} else if (player_out) {
|
|
@@ -1524,8 +1461,8 @@ fn checkFaint(
|
|
|
1524
1461
|
}
|
|
1525
1462
|
|
|
1526
1463
|
const foe_choice: Choice.Type = if (foe_fainted) .Switch else .Pass;
|
|
1527
|
-
if (player == .P1) return
|
|
1528
|
-
return
|
|
1464
|
+
if (player == .P1) return .{ .p1 = .Switch, .p2 = foe_choice };
|
|
1465
|
+
return .{ .p1 = foe_choice, .p2 = .Switch };
|
|
1529
1466
|
}
|
|
1530
1467
|
|
|
1531
1468
|
fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
|
|
@@ -1573,21 +1510,19 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
|
|
|
1573
1510
|
var volatiles = &side.active.volatiles;
|
|
1574
1511
|
|
|
1575
1512
|
const brn = Status.is(stored.status, .BRN);
|
|
1576
|
-
if (brn or Status.is(stored.status, .PSN))
|
|
1513
|
+
if (brn or Status.is(stored.status, .PSN)) {
|
|
1577
1514
|
var damage = @max(stored.stats.hp / 16, 1);
|
|
1578
|
-
|
|
1579
1515
|
if (volatiles.Toxic) {
|
|
1580
1516
|
volatiles.toxic += 1;
|
|
1581
1517
|
damage *= volatiles.toxic;
|
|
1582
1518
|
}
|
|
1519
|
+
damage = @min(damage, stored.hp);
|
|
1583
1520
|
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
// Pokémon Showdown uses damageOf here but its not relevant in Generation I
|
|
1590
|
-
try options.log.damage(ident, stored, if (brn) Damage.Burn else Damage.Poison);
|
|
1521
|
+
if (!showdown or damage > 0) {
|
|
1522
|
+
stored.hp -= damage;
|
|
1523
|
+
// Pokémon Showdown uses damageOf here but its not relevant in Generation I
|
|
1524
|
+
try options.log.damage(ident, stored, if (brn) Damage.Burn else Damage.Poison);
|
|
1525
|
+
}
|
|
1591
1526
|
}
|
|
1592
1527
|
|
|
1593
1528
|
if (volatiles.LeechSeed) {
|
|
@@ -1700,9 +1635,9 @@ fn checkEBC(battle: anytype) bool {
|
|
|
1700
1635
|
}
|
|
1701
1636
|
|
|
1702
1637
|
fn disabled(side: *Side, ident: ID, options: anytype) !bool {
|
|
1703
|
-
if (side.active.volatiles.
|
|
1638
|
+
if (side.active.volatiles.disable_move != 0) {
|
|
1704
1639
|
// A Pokémon that transforms after being disabled may end up with less move slots
|
|
1705
|
-
const m = side.active.moves[side.active.volatiles.
|
|
1640
|
+
const m = side.active.moves[side.active.volatiles.disable_move - 1].id;
|
|
1706
1641
|
// side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
|
|
1707
1642
|
const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
|
|
1708
1643
|
m
|
|
@@ -1930,7 +1865,7 @@ pub const Effects = struct {
|
|
|
1930
1865
|
try options.chance.commit(player, .hit);
|
|
1931
1866
|
}
|
|
1932
1867
|
|
|
1933
|
-
if (volatiles.
|
|
1868
|
+
if (volatiles.disable_move != 0) {
|
|
1934
1869
|
try options.log.fail(foe_ident, .None);
|
|
1935
1870
|
return null;
|
|
1936
1871
|
}
|
|
@@ -1955,18 +1890,17 @@ pub const Effects = struct {
|
|
|
1955
1890
|
return Result.Error;
|
|
1956
1891
|
}
|
|
1957
1892
|
|
|
1958
|
-
volatiles.
|
|
1893
|
+
volatiles.disable_move =
|
|
1959
1894
|
@intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
|
|
1960
|
-
volatiles.
|
|
1895
|
+
volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
|
|
1961
1896
|
|
|
1962
|
-
const id = foe.active.move(volatiles.
|
|
1897
|
+
const id = foe.active.move(volatiles.disable_move).id;
|
|
1963
1898
|
try options.log.startEffect(foe_ident, .Disable, id);
|
|
1964
1899
|
return null;
|
|
1965
1900
|
}
|
|
1966
1901
|
|
|
1967
1902
|
fn drainHP(battle: anytype, player: Player, options: anytype) !void {
|
|
1968
|
-
var
|
|
1969
|
-
var stored = side.stored();
|
|
1903
|
+
var stored = battle.side(player).stored();
|
|
1970
1904
|
|
|
1971
1905
|
if (battle.last_damage == 0) {
|
|
1972
1906
|
assert(showdown);
|
|
@@ -2420,12 +2354,17 @@ pub const Effects = struct {
|
|
|
2420
2354
|
}
|
|
2421
2355
|
|
|
2422
2356
|
fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
2423
|
-
if (!showdown or battle.side(player).last_selected_move == .Teleport) return;
|
|
2424
|
-
|
|
2425
2357
|
// Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
|
|
2426
|
-
if (
|
|
2427
|
-
|
|
2428
|
-
|
|
2358
|
+
if (showdown) {
|
|
2359
|
+
if (battle.side(player).last_selected_move == .Teleport) return;
|
|
2360
|
+
if (try checkHit(battle, player, move, options)) {
|
|
2361
|
+
try options.chance.commit(player, .hit);
|
|
2362
|
+
}
|
|
2363
|
+
battle.last_damage = 0;
|
|
2364
|
+
} else {
|
|
2365
|
+
try options.log.fail(battle.active(player), .None);
|
|
2366
|
+
try options.log.laststill();
|
|
2367
|
+
}
|
|
2429
2368
|
}
|
|
2430
2369
|
|
|
2431
2370
|
fn thrashing(battle: anytype, player: Player, options: anytype) void {
|
|
@@ -2705,7 +2644,7 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
|
|
|
2705
2644
|
}
|
|
2706
2645
|
}
|
|
2707
2646
|
|
|
2708
|
-
inline fn isForced(active:
|
|
2647
|
+
inline fn isForced(active: anytype) bool {
|
|
2709
2648
|
return active.volatiles.Recharging or active.volatiles.Rage or
|
|
2710
2649
|
active.volatiles.Thrashing or active.volatiles.Charging;
|
|
2711
2650
|
}
|
|
@@ -2718,9 +2657,9 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
|
|
|
2718
2657
|
|
|
2719
2658
|
options.chance.clearDurations(who, clear);
|
|
2720
2659
|
|
|
2721
|
-
if (volatiles.
|
|
2722
|
-
volatiles.
|
|
2723
|
-
volatiles.
|
|
2660
|
+
if (volatiles.disable_move != 0) {
|
|
2661
|
+
volatiles.disable_move = 0;
|
|
2662
|
+
volatiles.disable_duration = 0;
|
|
2724
2663
|
try log.end(ident, .DisableSilent);
|
|
2725
2664
|
}
|
|
2726
2665
|
if (volatiles.Confusion) {
|
|
@@ -2730,15 +2669,15 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
|
|
|
2730
2669
|
}
|
|
2731
2670
|
if (volatiles.Mist) {
|
|
2732
2671
|
volatiles.Mist = false;
|
|
2733
|
-
try log.end(ident, .
|
|
2672
|
+
try log.end(ident, .MistSilent);
|
|
2734
2673
|
}
|
|
2735
2674
|
if (volatiles.FocusEnergy) {
|
|
2736
2675
|
volatiles.FocusEnergy = false;
|
|
2737
|
-
try log.end(ident, .
|
|
2676
|
+
try log.end(ident, .FocusEnergySilent);
|
|
2738
2677
|
}
|
|
2739
2678
|
if (volatiles.LeechSeed) {
|
|
2740
2679
|
volatiles.LeechSeed = false;
|
|
2741
|
-
try log.end(ident, .
|
|
2680
|
+
try log.end(ident, .LeechSeedSilent);
|
|
2742
2681
|
}
|
|
2743
2682
|
if (!showdown and volatiles.Toxic) {
|
|
2744
2683
|
volatiles.Toxic = false;
|
|
@@ -2746,17 +2685,17 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
|
|
|
2746
2685
|
}
|
|
2747
2686
|
if (volatiles.LightScreen) {
|
|
2748
2687
|
volatiles.LightScreen = false;
|
|
2749
|
-
try log.end(ident, .
|
|
2688
|
+
try log.end(ident, .LightScreenSilent);
|
|
2750
2689
|
}
|
|
2751
2690
|
if (volatiles.Reflect) {
|
|
2752
2691
|
volatiles.Reflect = false;
|
|
2753
|
-
try log.end(ident, .
|
|
2692
|
+
try log.end(ident, .ReflectSilent);
|
|
2754
2693
|
}
|
|
2755
2694
|
if (showdown and volatiles.Toxic) {
|
|
2756
2695
|
volatiles.Toxic = false;
|
|
2757
2696
|
// Pokémon Showdown erroneously clears the toxic counter
|
|
2758
2697
|
volatiles.toxic = 0;
|
|
2759
|
-
try log.end(ident, .
|
|
2698
|
+
try log.end(ident, .ToxicSilent);
|
|
2760
2699
|
}
|
|
2761
2700
|
}
|
|
2762
2701
|
|
|
@@ -2785,7 +2724,7 @@ pub const Rolls = struct {
|
|
|
2785
2724
|
return crit;
|
|
2786
2725
|
}
|
|
2787
2726
|
|
|
2788
|
-
inline fn damage(battle: anytype, player:
|
|
2727
|
+
inline fn damage(battle: anytype, player: Player, options: anytype) !u8 {
|
|
2789
2728
|
const roll = if (options.calc.overridden(player, .damage)) |val|
|
|
2790
2729
|
val
|
|
2791
2730
|
else roll: {
|
|
@@ -2796,6 +2735,7 @@ pub const Rolls = struct {
|
|
|
2796
2735
|
}
|
|
2797
2736
|
};
|
|
2798
2737
|
|
|
2738
|
+
assert(roll >= 217 and roll <= 255);
|
|
2799
2739
|
try options.chance.damage(player, roll);
|
|
2800
2740
|
return roll;
|
|
2801
2741
|
}
|
|
@@ -2891,11 +2831,9 @@ pub const Rolls = struct {
|
|
|
2891
2831
|
inline fn metronome(battle: anytype, player: Player, options: anytype) !Move {
|
|
2892
2832
|
const move: Move = if (options.calc.overridden(player, .metronome)) |val|
|
|
2893
2833
|
val
|
|
2894
|
-
else if (showdown)
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
break :move @enumFromInt(r + mod);
|
|
2898
|
-
} else move: {
|
|
2834
|
+
else if (showdown)
|
|
2835
|
+
Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
|
|
2836
|
+
else move: {
|
|
2899
2837
|
while (true) {
|
|
2900
2838
|
const r = battle.rng.next();
|
|
2901
2839
|
if (r == 0 or r == @intFromEnum(Move.Metronome)) continue;
|
|
@@ -2920,6 +2858,7 @@ pub const Rolls = struct {
|
|
|
2920
2858
|
}
|
|
2921
2859
|
};
|
|
2922
2860
|
|
|
2861
|
+
assert(power < max);
|
|
2923
2862
|
try options.chance.psywave(player, power, max);
|
|
2924
2863
|
return power;
|
|
2925
2864
|
}
|
|
@@ -2928,7 +2867,7 @@ pub const Rolls = struct {
|
|
|
2928
2867
|
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2929
2868
|
@intCast(val)
|
|
2930
2869
|
else if (showdown)
|
|
2931
|
-
|
|
2870
|
+
battle.rng.range(u3, 1, 8)
|
|
2932
2871
|
else duration: {
|
|
2933
2872
|
while (true) {
|
|
2934
2873
|
const r = battle.rng.next() & 7;
|
|
@@ -2936,6 +2875,7 @@ pub const Rolls = struct {
|
|
|
2936
2875
|
}
|
|
2937
2876
|
};
|
|
2938
2877
|
|
|
2878
|
+
assert(duration >= 1 and duration <= 7);
|
|
2939
2879
|
options.chance.duration(.sleep, player, player.foe(), duration);
|
|
2940
2880
|
return duration;
|
|
2941
2881
|
}
|
|
@@ -2944,10 +2884,11 @@ pub const Rolls = struct {
|
|
|
2944
2884
|
const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
|
|
2945
2885
|
val
|
|
2946
2886
|
else if (showdown)
|
|
2947
|
-
|
|
2887
|
+
battle.rng.range(u4, 1, 9)
|
|
2948
2888
|
else
|
|
2949
2889
|
@intCast((battle.rng.next() & 7) + 1);
|
|
2950
2890
|
|
|
2891
|
+
assert(duration >= 1 and duration <= 8);
|
|
2951
2892
|
options.chance.duration(.disable, player, player.foe(), duration);
|
|
2952
2893
|
return duration;
|
|
2953
2894
|
}
|
|
@@ -2956,10 +2897,11 @@ pub const Rolls = struct {
|
|
|
2956
2897
|
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2957
2898
|
@intCast(val)
|
|
2958
2899
|
else if (showdown)
|
|
2959
|
-
|
|
2900
|
+
battle.rng.range(u3, 2, 6)
|
|
2960
2901
|
else
|
|
2961
2902
|
@intCast((battle.rng.next() & 3) + 2);
|
|
2962
2903
|
|
|
2904
|
+
assert(duration >= 2 and duration <= 5);
|
|
2963
2905
|
options.chance.duration(.confusion, player, if (self) player else player.foe(), duration);
|
|
2964
2906
|
return duration;
|
|
2965
2907
|
}
|
|
@@ -2968,14 +2910,17 @@ pub const Rolls = struct {
|
|
|
2968
2910
|
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2969
2911
|
@intCast(val)
|
|
2970
2912
|
else if (showdown)
|
|
2971
|
-
|
|
2913
|
+
battle.rng.range(u3, 2, 4)
|
|
2972
2914
|
else
|
|
2973
2915
|
@intCast((battle.rng.next() & 1) + 2);
|
|
2974
2916
|
|
|
2917
|
+
assert(duration >= 2 and duration <= 3);
|
|
2975
2918
|
options.chance.duration(.attacking, player, player, duration);
|
|
2976
2919
|
return duration;
|
|
2977
2920
|
}
|
|
2978
2921
|
|
|
2922
|
+
const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
|
|
2923
|
+
|
|
2979
2924
|
inline fn distribution(
|
|
2980
2925
|
battle: anytype,
|
|
2981
2926
|
comptime effect: Move.Effect,
|
|
@@ -2986,12 +2931,13 @@ pub const Rolls = struct {
|
|
|
2986
2931
|
const n: u3 = if (options.calc.overridden(player, roll)) |val|
|
|
2987
2932
|
@intCast(val)
|
|
2988
2933
|
else if (showdown)
|
|
2989
|
-
DISTRIBUTION[battle.rng.range(
|
|
2934
|
+
DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
|
|
2990
2935
|
else n: {
|
|
2991
2936
|
const r = (battle.rng.next() & 3);
|
|
2992
2937
|
break :n @intCast((if (r < 2) r else battle.rng.next() & 3) + 2);
|
|
2993
2938
|
};
|
|
2994
2939
|
|
|
2940
|
+
assert(n >= 2 and n <= 5);
|
|
2995
2941
|
if (effect == .Binding) {
|
|
2996
2942
|
options.chance.duration(.binding, player, player, n);
|
|
2997
2943
|
} else {
|
|
@@ -3032,10 +2978,9 @@ pub const Rolls = struct {
|
|
|
3032
2978
|
while (i < moves.len and r > 0) : (i += 1) {
|
|
3033
2979
|
if (moves[i].pp > 0) {
|
|
3034
2980
|
r -= 1;
|
|
3035
|
-
if (r == 0) break;
|
|
2981
|
+
if (r == 0) break :slot @intCast(i + 1);
|
|
3036
2982
|
}
|
|
3037
2983
|
}
|
|
3038
|
-
break :slot @intCast(i + 1);
|
|
3039
2984
|
}
|
|
3040
2985
|
}
|
|
3041
2986
|
|
|
@@ -3057,11 +3002,11 @@ test "RNG agreement" {
|
|
|
3057
3002
|
expected[i] = @intCast(i * 0x1000000);
|
|
3058
3003
|
}
|
|
3059
3004
|
|
|
3060
|
-
var spe
|
|
3061
|
-
var cfz
|
|
3062
|
-
var par
|
|
3063
|
-
var brn
|
|
3064
|
-
var eff
|
|
3005
|
+
var spe: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
|
|
3006
|
+
var cfz: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
|
|
3007
|
+
var par: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
|
|
3008
|
+
var brn: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
|
|
3009
|
+
var eff: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
|
|
3065
3010
|
|
|
3066
3011
|
for (0..expected.len) |i| {
|
|
3067
3012
|
try expectEqual(spe.range(u8, 0, 2) == 0, i < Gen12.percent(50) + 1);
|
|
@@ -3097,7 +3042,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3097
3042
|
const side = battle.side(player);
|
|
3098
3043
|
const foe = battle.foe(player);
|
|
3099
3044
|
|
|
3100
|
-
var active = side.active;
|
|
3045
|
+
var active = &side.active;
|
|
3101
3046
|
const stored = side.stored();
|
|
3102
3047
|
|
|
3103
3048
|
// While players are not given any input options on the cartridge in these cases,
|
|
@@ -3151,7 +3096,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3151
3096
|
if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
|
|
3152
3097
|
// Pokémon Showdown displays Struggle if limited to Bide but unable to pick
|
|
3153
3098
|
const struggle =
|
|
3154
|
-
m.id == .Bide and (m.pp == 0 or active.volatiles.
|
|
3099
|
+
m.id == .Bide and (m.pp == 0 or active.volatiles.disable_move == slot);
|
|
3155
3100
|
const s = if (struggle) 0 else slot;
|
|
3156
3101
|
out[n] = .{ .type = .Move, .data = s };
|
|
3157
3102
|
n += 1;
|
|
@@ -3166,7 +3111,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3166
3111
|
const m = active.moves[slot - 1];
|
|
3167
3112
|
if (m.id == .None) break;
|
|
3168
3113
|
if (m.pp == 0) continue;
|
|
3169
|
-
if (active.volatiles.
|
|
3114
|
+
if (active.volatiles.disable_move == slot) continue;
|
|
3170
3115
|
out[n] = .{ .type = .Move, .data = slot };
|
|
3171
3116
|
n += 1;
|
|
3172
3117
|
}
|