@pkmn/engine 0.1.0-dev.c5fe6d07 → 0.1.0-dev.cb3978dd
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/LICENSE +1 -3
- package/README.md +46 -19
- package/build/pkg/addon/browser.d.ts +3 -0
- package/build/pkg/addon/browser.js +44 -0
- package/build/pkg/addon/browser.js.map +1 -0
- package/build/pkg/addon/node.d.ts +3 -0
- package/build/pkg/addon/node.js +97 -0
- package/build/pkg/addon/node.js.map +1 -0
- package/build/pkg/addon/wasm.d.ts +2 -0
- package/build/pkg/addon/wasm.js +21 -0
- package/build/pkg/addon/wasm.js.map +1 -0
- package/build/pkg/addon.d.ts +22 -0
- package/build/pkg/addon.js +35 -58
- package/build/pkg/addon.js.map +1 -1
- package/build/pkg/data/ids.json +12 -12
- package/build/pkg/data/layout.json +17 -4
- package/build/pkg/data/protocol.json +84 -13
- package/build/pkg/data.d.ts +27 -5
- package/build/pkg/data.js.map +1 -1
- package/build/pkg/gen1.d.ts +10 -13
- package/build/pkg/gen1.js +3 -3
- package/build/pkg/gen1.js.map +1 -1
- package/build/pkg/index.d.ts +55 -13
- package/build/pkg/index.js +15 -10
- package/build/pkg/index.js.map +1 -1
- package/build/pkg/protocol.d.ts +20 -1
- package/build/pkg/protocol.js +123 -102
- package/build/pkg/protocol.js.map +1 -1
- package/build/tools/debug.js +36 -68
- package/build/tools/debug.js.map +1 -1
- package/build/tools/display/debug.jsx +141 -0
- package/build/tools/display/debug.jsx.map +1 -0
- package/build/tools/display/index.js +141 -0
- package/build/tools/display/index.js.map +1 -0
- package/build/tools/display/ui.jsx +349 -0
- package/build/tools/display/ui.jsx.map +1 -0
- package/build/tools/display/util.js +18 -0
- package/build/tools/display/util.js.map +1 -0
- package/build.zig +277 -107
- package/package.json +72 -32
- package/src/bin/install-pkmn-engine +29 -19
- package/src/bin/pkmn-debug +1 -1
- package/src/lib/common/array.zig +77 -0
- package/src/lib/common/data.zig +12 -24
- package/src/lib/common/debug.zig +39 -8
- package/src/lib/common/js.zig +4 -2
- package/src/lib/common/optional.zig +21 -10
- package/src/lib/common/options.zig +6 -0
- package/src/lib/common/protocol.zig +808 -332
- package/src/lib/common/rational.zig +132 -36
- package/src/lib/common/rng.zig +22 -15
- package/src/lib/common/util.zig +50 -1
- package/src/lib/gen1/calc.zig +688 -162
- package/src/lib/gen1/chance.zig +436 -295
- package/src/lib/gen1/data/moves.zig +193 -183
- package/src/lib/gen1/data/species.zig +4 -6
- package/src/lib/gen1/data/types.zig +8 -9
- package/src/lib/gen1/data.zig +55 -70
- package/src/lib/gen1/helpers.zig +91 -562
- package/src/lib/gen1/mechanics.zig +653 -578
- package/src/lib/gen2/calc.zig +153 -0
- package/src/lib/gen2/chance.zig +902 -0
- package/src/lib/gen2/data/items.zig +13 -21
- package/src/lib/gen2/data/moves.zig +664 -572
- package/src/lib/gen2/data/species.zig +2 -4
- package/src/lib/gen2/data/types.zig +135 -33
- package/src/lib/gen2/data.zig +115 -91
- package/src/lib/gen2/generated.zig +801 -0
- package/src/lib/gen2/helpers.zig +54 -37
- package/src/lib/gen2/mechanics.zig +3569 -29
- package/src/lib/{binding/node.zig → node.zig} +9 -14
- package/src/lib/pkmn.zig +36 -15
- package/src/lib/{binding/wasm.zig → wasm.zig} +1 -1
- package/src/tools/display/dom.ts +96 -0
- package/src/tools/display/ui.css +354 -0
- package/build/test/display.js +0 -400
- package/build/test/display.js.map +0 -1
- package/src/lib/gen1/README.md +0 -892
- package/src/lib/gen2/README.md +0 -308
- package/src/test/display/pkmn.html.tmpl +0 -368
|
@@ -1,38 +1,28 @@
|
|
|
1
|
-
const
|
|
2
|
-
|
|
3
|
-
const pkmn = @import("../pkmn.zig");
|
|
1
|
+
const DEBUG = @import("../common/debug.zig").print;
|
|
4
2
|
|
|
3
|
+
const chance = @import("chance.zig");
|
|
5
4
|
const common = @import("../common/data.zig");
|
|
6
|
-
const
|
|
5
|
+
const data = @import("data.zig");
|
|
6
|
+
const pkmn = @import("../pkmn.zig");
|
|
7
7
|
const protocol = @import("../common/protocol.zig");
|
|
8
8
|
const rng = @import("../common/rng.zig");
|
|
9
|
-
|
|
10
|
-
const Action = @import("chance.zig").Action;
|
|
11
|
-
|
|
12
|
-
const data = @import("data.zig");
|
|
9
|
+
const std = @import("std");
|
|
13
10
|
|
|
14
11
|
const assert = std.debug.assert;
|
|
15
|
-
|
|
16
|
-
const expectEqual = std.testing.expectEqual;
|
|
17
|
-
|
|
18
|
-
const Choice = common.Choice;
|
|
19
|
-
const ID = common.ID;
|
|
20
|
-
const Player = common.Player;
|
|
21
|
-
const Result = common.Result;
|
|
22
|
-
|
|
23
|
-
const showdown = pkmn.options.showdown;
|
|
24
|
-
|
|
25
12
|
const Boost = protocol.Boost;
|
|
13
|
+
const Choice = common.Choice;
|
|
26
14
|
const Damage = protocol.Damage;
|
|
27
|
-
const Heal = protocol.Heal;
|
|
28
|
-
|
|
29
|
-
const Gen12 = rng.Gen12;
|
|
30
|
-
|
|
31
|
-
const ActivePokemon = data.ActivePokemon;
|
|
32
15
|
const Effectiveness = data.Effectiveness;
|
|
16
|
+
const expectEqual = std.testing.expectEqual;
|
|
17
|
+
const Fail = protocol.Fail;
|
|
18
|
+
const Gen12 = rng.Gen12;
|
|
19
|
+
const Heal = protocol.Heal;
|
|
20
|
+
const ID = common.ID;
|
|
33
21
|
const Move = data.Move;
|
|
34
22
|
const MoveSlot = data.MoveSlot;
|
|
35
|
-
const
|
|
23
|
+
const Player = common.Player;
|
|
24
|
+
const Result = common.Result;
|
|
25
|
+
const showdown = pkmn.options.showdown;
|
|
36
26
|
const Side = data.Side;
|
|
37
27
|
const Species = data.Species;
|
|
38
28
|
const Stats = data.Stats;
|
|
@@ -59,34 +49,26 @@ const BOOSTS = &[_][2]u8{
|
|
|
59
49
|
|
|
60
50
|
const MAX_STAT_VALUE = 999;
|
|
61
51
|
|
|
62
|
-
const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
|
|
63
|
-
|
|
64
52
|
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
53
|
assert(c1.type != .Pass or c2.type != .Pass or battle.turn == 0);
|
|
70
54
|
if (battle.turn == 0) return start(battle, options);
|
|
71
55
|
|
|
72
56
|
var s1 = false;
|
|
73
57
|
var s2 = false;
|
|
74
|
-
var f1: ?Move = null;
|
|
75
|
-
var f2: ?Move = null;
|
|
76
58
|
|
|
77
|
-
if (selectMove(battle, .P1, c1, c2, &s1
|
|
78
|
-
if (selectMove(battle, .P2, c2, c1, &s2
|
|
59
|
+
if (selectMove(battle, .P1, c1, c2, &s1)) |r| return r;
|
|
60
|
+
if (selectMove(battle, .P2, c2, c1, &s2)) |r| return r;
|
|
79
61
|
|
|
80
62
|
var p1 = battle.side(.P1);
|
|
81
63
|
var p2 = battle.side(.P2);
|
|
82
64
|
|
|
83
|
-
|
|
84
|
-
|
|
65
|
+
const r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
|
|
66
|
+
const r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
|
|
85
67
|
|
|
86
68
|
if (try turnOrder(battle, c1, c2, options) == .P1) {
|
|
87
|
-
if (try doTurn(battle, .P1, c1, r1, s1,
|
|
69
|
+
if (try doTurn(battle, .P1, c1, r1, s1, .P2, c2, r2, s2, options)) |r| return r;
|
|
88
70
|
} else {
|
|
89
|
-
if (try doTurn(battle, .P2, c2, r2, s2,
|
|
71
|
+
if (try doTurn(battle, .P2, c2, r2, s2, .P1, c1, r1, s1, options)) |r| return r;
|
|
90
72
|
}
|
|
91
73
|
|
|
92
74
|
if (p1.active.volatiles.attacks == 0) p1.active.volatiles.Binding = false;
|
|
@@ -99,11 +81,11 @@ fn start(battle: anytype, options: anytype) !Result {
|
|
|
99
81
|
const p1 = battle.side(.P1);
|
|
100
82
|
const p2 = battle.side(.P2);
|
|
101
83
|
|
|
102
|
-
|
|
84
|
+
const p1_slot = findFirstAlive(p1);
|
|
103
85
|
assert(!showdown or p1_slot == 1);
|
|
104
86
|
if (p1_slot == 0) return if (findFirstAlive(p2) == 0) Result.Tie else Result.Lose;
|
|
105
87
|
|
|
106
|
-
|
|
88
|
+
const p2_slot = findFirstAlive(p2);
|
|
107
89
|
assert(!showdown or p2_slot == 1);
|
|
108
90
|
if (p2_slot == 0) return Result.Win;
|
|
109
91
|
|
|
@@ -124,7 +106,6 @@ fn selectMove(
|
|
|
124
106
|
choice: Choice,
|
|
125
107
|
foe_choice: Choice,
|
|
126
108
|
skip_turn: *bool,
|
|
127
|
-
from: *?Move,
|
|
128
109
|
) ?Result {
|
|
129
110
|
if (choice.type == .Pass) return null;
|
|
130
111
|
|
|
@@ -141,7 +122,6 @@ fn selectMove(
|
|
|
141
122
|
return null;
|
|
142
123
|
}
|
|
143
124
|
if (volatiles.Rage) {
|
|
144
|
-
from.* = side.last_used_move;
|
|
145
125
|
if (showdown) {
|
|
146
126
|
if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
|
|
147
127
|
saveMove(battle, player, null);
|
|
@@ -151,7 +131,6 @@ fn selectMove(
|
|
|
151
131
|
// Pokémon Showdown removes Flinch at the end-of-turn in its residual handler
|
|
152
132
|
if (!showdown) volatiles.Flinch = false;
|
|
153
133
|
if (volatiles.Thrashing or volatiles.Charging) {
|
|
154
|
-
from.* = side.last_selected_move;
|
|
155
134
|
if (showdown) {
|
|
156
135
|
if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
|
|
157
136
|
saveMove(battle, player, null);
|
|
@@ -172,15 +151,13 @@ fn selectMove(
|
|
|
172
151
|
return null;
|
|
173
152
|
}
|
|
174
153
|
if (volatiles.Binding) {
|
|
175
|
-
from.* = side.last_used_move;
|
|
176
154
|
if (showdown) {
|
|
177
|
-
if (foe_choice.type == .Switch) from.* = null;
|
|
178
155
|
// Pokémon Showdown overwrites Mirror Move with whatever was selected - really this
|
|
179
156
|
// should set side.last_selected_move = last.id to reuse Mirror Move and fail in order
|
|
180
157
|
// to satisfy the conditions of the Desync Clause Mod. However, because Binding is still
|
|
181
158
|
// set the selected move will not actually be used, it will just be reported as having
|
|
182
159
|
// been used (this differs from how Pokémon Showdown works, but its impossible to
|
|
183
|
-
// replicate the incorrect behavior with the correct mechanisms)
|
|
160
|
+
// replicate the incorrect behavior with the correct mechanisms)
|
|
184
161
|
saveMove(battle, player, choice);
|
|
185
162
|
} else {
|
|
186
163
|
assert(choice.data == 0);
|
|
@@ -188,7 +165,10 @@ fn selectMove(
|
|
|
188
165
|
if (foe_choice.type == .Switch) {
|
|
189
166
|
const last = side.active.move(battle.lastMove(player).index);
|
|
190
167
|
if (last.id == .Metronome) side.last_selected_move = last.id;
|
|
191
|
-
if (last.id == .MirrorMove)
|
|
168
|
+
if (last.id == .MirrorMove) {
|
|
169
|
+
if (!pkmn.options.mod) return Result.Error;
|
|
170
|
+
side.last_selected_move = last.id;
|
|
171
|
+
}
|
|
192
172
|
}
|
|
193
173
|
}
|
|
194
174
|
return null;
|
|
@@ -210,16 +190,13 @@ fn selectMove(
|
|
|
210
190
|
if (choice.data == 0) {
|
|
211
191
|
const struggle = ok: {
|
|
212
192
|
for (side.active.moves, 0..) |move, i| {
|
|
213
|
-
if (move.pp > 0 and volatiles.
|
|
193
|
+
if (move.pp > 0 and volatiles.disable_move != i + 1) break :ok false;
|
|
214
194
|
}
|
|
215
195
|
break :ok true;
|
|
216
196
|
};
|
|
217
|
-
|
|
218
197
|
assert(struggle);
|
|
219
|
-
saveMove(battle, player, choice);
|
|
220
|
-
} else {
|
|
221
|
-
saveMove(battle, player, choice);
|
|
222
198
|
}
|
|
199
|
+
saveMove(battle, player, choice);
|
|
223
200
|
|
|
224
201
|
return null;
|
|
225
202
|
}
|
|
@@ -232,7 +209,7 @@ fn saveMove(battle: anytype, player: Player, choice: ?Choice) void {
|
|
|
232
209
|
if (c.data == 0) {
|
|
233
210
|
side.last_selected_move = .Struggle;
|
|
234
211
|
} else {
|
|
235
|
-
assert(showdown or side.active.volatiles.
|
|
212
|
+
assert(showdown or side.active.volatiles.disable_move != c.data);
|
|
236
213
|
const move = side.active.move(c.data);
|
|
237
214
|
// You cannot *select* a move with 0 PP (except on Pokémon Showdown where that is
|
|
238
215
|
// sometimes required...), but a 0 PP move can be used automatically
|
|
@@ -270,17 +247,17 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: a
|
|
|
270
247
|
active.moves = incoming.moves;
|
|
271
248
|
|
|
272
249
|
statusModify(incoming.status, &active.stats);
|
|
250
|
+
options.chance.switched(player, slot);
|
|
273
251
|
|
|
274
252
|
foe.active.volatiles.Binding = false;
|
|
275
253
|
|
|
276
|
-
try options.log.switched(battle.active(player), incoming);
|
|
277
|
-
options.chance.switched(player, side.order[0], out);
|
|
254
|
+
try options.log.switched(.{ battle.active(player), incoming });
|
|
278
255
|
|
|
279
256
|
if (showdown and incoming.status == Status.TOX) {
|
|
280
257
|
incoming.status = Status.init(.PSN);
|
|
281
258
|
// Technically, Pokémon Showdown adds these after *both* Pokémon have switched, but we'd
|
|
282
259
|
// rather not clutter up turnOrder just for this (incorrect) log message
|
|
283
|
-
try options.log.status(battle.active(player), incoming.status, .Silent);
|
|
260
|
+
try options.log.status(.{ battle.active(player), incoming.status, .Silent });
|
|
284
261
|
}
|
|
285
262
|
}
|
|
286
263
|
|
|
@@ -343,12 +320,10 @@ fn doTurn(
|
|
|
343
320
|
player_choice: Choice,
|
|
344
321
|
player_rewrap: bool,
|
|
345
322
|
player_skip: bool,
|
|
346
|
-
player_from: ?Move,
|
|
347
323
|
foe_player: Player,
|
|
348
324
|
foe_choice: Choice,
|
|
349
325
|
foe_rewrap: bool,
|
|
350
326
|
foe_skip: bool,
|
|
351
|
-
foe_from: ?Move,
|
|
352
327
|
options: anytype,
|
|
353
328
|
) !?Result {
|
|
354
329
|
assert(player_choice.type != .Pass);
|
|
@@ -361,19 +336,18 @@ fn doTurn(
|
|
|
361
336
|
player_choice,
|
|
362
337
|
player_rewrap,
|
|
363
338
|
player_skip,
|
|
364
|
-
player_from,
|
|
365
339
|
&residual,
|
|
366
340
|
options,
|
|
367
341
|
)) |r| return r;
|
|
368
342
|
if (!replace) {
|
|
369
343
|
if (player_choice.type != .Switch) {
|
|
370
|
-
if (try checkFaint(battle, foe_player, options)) |r| return r;
|
|
344
|
+
if (try checkFaint(battle, foe_player, true, options)) |r| return r;
|
|
371
345
|
}
|
|
372
346
|
if (residual) try handleResidual(battle, player, options);
|
|
373
|
-
if (try checkFaint(battle, player, options)) |r| return r;
|
|
347
|
+
if (try checkFaint(battle, player, false, options)) |r| return r;
|
|
374
348
|
} else if (foe_choice.type == .Pass) return null;
|
|
375
349
|
|
|
376
|
-
options.chance.clearPending();
|
|
350
|
+
if (!showdown) options.chance.clearPending();
|
|
377
351
|
|
|
378
352
|
residual = true;
|
|
379
353
|
replace = battle.side(foe_player).stored().hp == 0;
|
|
@@ -384,16 +358,15 @@ fn doTurn(
|
|
|
384
358
|
foe_choice,
|
|
385
359
|
foe_rewrap,
|
|
386
360
|
foe_skip,
|
|
387
|
-
foe_from,
|
|
388
361
|
&residual,
|
|
389
362
|
options,
|
|
390
363
|
)) |r| return r;
|
|
391
364
|
if (!replace) {
|
|
392
365
|
if (!calc and foe_choice.type != .Switch) {
|
|
393
|
-
if (try checkFaint(battle, player, options)) |r| return r;
|
|
366
|
+
if (try checkFaint(battle, player, true, options)) |r| return r;
|
|
394
367
|
}
|
|
395
368
|
if (residual) try handleResidual(battle, foe_player, options);
|
|
396
|
-
if (try checkFaint(battle, foe_player, options)) |r| return r;
|
|
369
|
+
if (try checkFaint(battle, foe_player, false, options)) |r| return r;
|
|
397
370
|
}
|
|
398
371
|
|
|
399
372
|
// Flinch is bugged on Pokémon Showdown because it gets implemented with a duration which causes
|
|
@@ -412,7 +385,6 @@ fn executeMove(
|
|
|
412
385
|
choice: Choice,
|
|
413
386
|
rewrap: bool,
|
|
414
387
|
skip: bool,
|
|
415
|
-
from: ?Move,
|
|
416
388
|
residual: *bool,
|
|
417
389
|
options: anytype,
|
|
418
390
|
) !?Result {
|
|
@@ -429,7 +401,7 @@ fn executeMove(
|
|
|
429
401
|
if (side.last_selected_move == .SKIP_TURN) {
|
|
430
402
|
assert(!showdown);
|
|
431
403
|
if (battle.foe(player).active.volatiles.Binding) {
|
|
432
|
-
try options.log.cant(battle.active(player), .Bound);
|
|
404
|
+
try options.log.cant(.{ battle.active(player), .Bound });
|
|
433
405
|
}
|
|
434
406
|
return null;
|
|
435
407
|
}
|
|
@@ -447,11 +419,13 @@ fn executeMove(
|
|
|
447
419
|
mslot = @intCast(battle.lastMove(player).index);
|
|
448
420
|
const stored = side.stored();
|
|
449
421
|
// GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
|
|
450
|
-
auto =
|
|
451
|
-
|
|
422
|
+
auto = pkmn.options.mod or isForced(&side.active) or
|
|
423
|
+
side.active.volatiles.Binding or side.active.volatiles.Bide or
|
|
424
|
+
side.last_selected_move == .HyperBeam or
|
|
425
|
+
Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
|
|
452
426
|
// If it wasn't Hyper Beam or the continuation of a move effect then we must have just
|
|
453
427
|
// thawed, in which case we will desync unless the last_selected_move happened to be at
|
|
454
|
-
// index 1 and the current Pokémon has the same move in its first slot
|
|
428
|
+
// index 1 and the current Pokémon has the same move in its first slot
|
|
455
429
|
if (!auto) {
|
|
456
430
|
// side.active.moves(slot) is safe to check even though the slot in question might not
|
|
457
431
|
// technically be from this Pokémon because it must be exactly 1 to not desync and
|
|
@@ -470,7 +444,7 @@ fn executeMove(
|
|
|
470
444
|
|
|
471
445
|
var skip_can = false;
|
|
472
446
|
var skip_pp = false;
|
|
473
|
-
switch (try beforeMove(battle, player, skip,
|
|
447
|
+
switch (try beforeMove(battle, player, skip, residual, options)) {
|
|
474
448
|
.done => return null,
|
|
475
449
|
.skip_can => skip_can = true,
|
|
476
450
|
.skip_pp => skip_pp = true,
|
|
@@ -478,10 +452,29 @@ fn executeMove(
|
|
|
478
452
|
.err => return @as(?Result, Result.Error),
|
|
479
453
|
}
|
|
480
454
|
|
|
481
|
-
|
|
482
|
-
|
|
455
|
+
// Pokémon Showdown incorrectly implements PP deduction when handling the Hyper Beam automatic
|
|
456
|
+
// selection glitch - if the move was proced due to Metronome / Mirror Move it either subtracts
|
|
457
|
+
// from the Hyper Beam slot if present or skips PP deduction entirely...
|
|
458
|
+
if (showdown and !skip_pp and side.last_selected_move == .HyperBeam and
|
|
459
|
+
side.last_selected_move != side.active.move(battle.lastMove(player).index).id)
|
|
460
|
+
{
|
|
461
|
+
assert(mslot == 1);
|
|
462
|
+
const has_beam = has_beam: {
|
|
463
|
+
for (side.active.moves, 0..) |m, i| {
|
|
464
|
+
if (m.id == .HyperBeam) {
|
|
465
|
+
mslot = @intCast(i + 1);
|
|
466
|
+
break :has_beam true;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
break :has_beam false;
|
|
470
|
+
};
|
|
471
|
+
if (!has_beam) skip_pp = true;
|
|
483
472
|
}
|
|
484
473
|
|
|
474
|
+
const can = skip_can or
|
|
475
|
+
try canMove(battle, player, mslot, auto, skip_pp, .None, residual, options);
|
|
476
|
+
if (!can) return null;
|
|
477
|
+
|
|
485
478
|
return doMove(battle, player, mslot, rewrap, auto, residual, options);
|
|
486
479
|
}
|
|
487
480
|
|
|
@@ -491,7 +484,6 @@ fn beforeMove(
|
|
|
491
484
|
battle: anytype,
|
|
492
485
|
player: Player,
|
|
493
486
|
skip: bool,
|
|
494
|
-
from: ?Move,
|
|
495
487
|
residual: *bool,
|
|
496
488
|
options: anytype,
|
|
497
489
|
) !BeforeMove {
|
|
@@ -505,36 +497,41 @@ fn beforeMove(
|
|
|
505
497
|
|
|
506
498
|
if (Status.is(stored.status, .SLP)) {
|
|
507
499
|
const before = stored.status;
|
|
500
|
+
const slf = Status.is(stored.status, .EXT);
|
|
508
501
|
// Even if the EXT bit is set this will still correctly modify the sleep duration
|
|
509
|
-
if (options.calc.
|
|
510
|
-
|
|
502
|
+
if (options.calc.overridden(player, .sleep)) |obs| switch (obs) {
|
|
503
|
+
.started, .ended => stored.status = 0,
|
|
504
|
+
.continuing => if (Status.duration(stored.status) > 1) {
|
|
505
|
+
stored.status -= 1;
|
|
506
|
+
},
|
|
507
|
+
else => unreachable,
|
|
511
508
|
} else {
|
|
512
509
|
stored.status -= 1;
|
|
513
510
|
}
|
|
514
511
|
|
|
515
512
|
const duration = Status.duration(stored.status);
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
513
|
+
try options.chance.sleep(player, if (slf)
|
|
514
|
+
.None
|
|
515
|
+
else if (duration == 0) .ended else .continuing);
|
|
519
516
|
|
|
520
517
|
if (duration == 0) {
|
|
521
|
-
try log.curestatus(ident, before, .Message);
|
|
518
|
+
try log.curestatus(.{ ident, before, .Message });
|
|
522
519
|
stored.status = 0; // clears EXT if present
|
|
523
520
|
} else {
|
|
524
|
-
try log.cant(ident, .Sleep);
|
|
521
|
+
try log.cant(.{ ident, .Sleep });
|
|
525
522
|
}
|
|
526
523
|
side.last_used_move = .None;
|
|
527
524
|
return .done;
|
|
528
525
|
}
|
|
529
526
|
|
|
530
527
|
if (Status.is(stored.status, .FRZ)) {
|
|
531
|
-
try log.cant(ident, .Freeze);
|
|
528
|
+
try log.cant(.{ ident, .Freeze });
|
|
532
529
|
side.last_used_move = .None;
|
|
533
530
|
return .done;
|
|
534
531
|
}
|
|
535
532
|
|
|
536
533
|
if (skip or foe.active.volatiles.Binding) {
|
|
537
|
-
try log.cant(ident, .Bound);
|
|
534
|
+
try log.cant(.{ ident, .Bound });
|
|
538
535
|
return .done;
|
|
539
536
|
}
|
|
540
537
|
|
|
@@ -542,27 +539,27 @@ fn beforeMove(
|
|
|
542
539
|
// Pokémon Showdown doesn't clear Flinch until its imaginary "residual" phase, meaning
|
|
543
540
|
// Pokémon can sometimes flinch multiple times from the same original hit
|
|
544
541
|
if (!showdown) volatiles.Flinch = false;
|
|
545
|
-
try log.cant(ident, .Flinch);
|
|
542
|
+
try log.cant(.{ ident, .Flinch });
|
|
546
543
|
return .done;
|
|
547
544
|
}
|
|
548
545
|
|
|
549
546
|
if (volatiles.Recharging) {
|
|
550
547
|
volatiles.Recharging = false;
|
|
551
|
-
try log.cant(ident, .Recharge);
|
|
548
|
+
try log.cant(.{ ident, .Recharge });
|
|
552
549
|
return .done;
|
|
553
550
|
}
|
|
554
551
|
|
|
555
|
-
if (volatiles.
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
552
|
+
if (volatiles.disable_duration > 0) {
|
|
553
|
+
volatiles.disable_duration =
|
|
554
|
+
decrement(.disable, player, options, volatiles.disable_duration);
|
|
555
|
+
try options.chance.disable(
|
|
556
|
+
player,
|
|
557
|
+
if (volatiles.disable_duration == 0) .ended else .continuing,
|
|
558
|
+
);
|
|
562
559
|
|
|
563
|
-
if (volatiles.
|
|
564
|
-
volatiles.
|
|
565
|
-
try log.end(ident, .Disable);
|
|
560
|
+
if (volatiles.disable_duration == 0) {
|
|
561
|
+
volatiles.disable_move = 0;
|
|
562
|
+
try log.end(.{ ident, .Disable });
|
|
566
563
|
}
|
|
567
564
|
}
|
|
568
565
|
// Pokémon Showdown's disable condition has a single onBeforeMove handler
|
|
@@ -571,25 +568,30 @@ fn beforeMove(
|
|
|
571
568
|
// This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
|
|
572
569
|
// before the side had a selected a move - we simply need to assume this leads to a desync
|
|
573
570
|
if (side.last_selected_move == .None) {
|
|
574
|
-
assert(!
|
|
571
|
+
assert(!pkmn.options.mod);
|
|
575
572
|
return .err;
|
|
576
573
|
}
|
|
577
574
|
|
|
578
575
|
if (volatiles.Confusion) {
|
|
579
576
|
assert(volatiles.confusion > 0);
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
if (
|
|
577
|
+
if (options.calc.overridden(player, .confusion)) |obs| switch (obs) {
|
|
578
|
+
.started, .ended => volatiles.confusion = 0,
|
|
579
|
+
.continuing => if (volatiles.confusion > 1) {
|
|
580
|
+
volatiles.confusion -= 1;
|
|
581
|
+
},
|
|
582
|
+
.overwritten => {},
|
|
583
|
+
else => unreachable,
|
|
583
584
|
} else {
|
|
584
585
|
volatiles.confusion -= 1;
|
|
585
586
|
}
|
|
586
|
-
|
|
587
|
+
|
|
588
|
+
try options.chance.confusion(player, if (volatiles.confusion == 0) .ended else .continuing);
|
|
587
589
|
|
|
588
590
|
if (volatiles.confusion == 0) {
|
|
589
591
|
volatiles.Confusion = false;
|
|
590
|
-
try log.end(ident, .Confusion);
|
|
592
|
+
try log.end(.{ ident, .Confusion });
|
|
591
593
|
} else {
|
|
592
|
-
try log.activate(ident, .Confusion);
|
|
594
|
+
try log.activate(.{ ident, .Confusion });
|
|
593
595
|
|
|
594
596
|
if (try Rolls.confused(battle, player, options)) {
|
|
595
597
|
assert(!volatiles.MultiHit);
|
|
@@ -601,7 +603,9 @@ fn beforeMove(
|
|
|
601
603
|
volatiles.Charging = false;
|
|
602
604
|
volatiles.Binding = false;
|
|
603
605
|
volatiles.Invulnerable = false;
|
|
604
|
-
|
|
606
|
+
|
|
607
|
+
options.chance.observe(.attacking, player, .None);
|
|
608
|
+
options.chance.observe(.binding, player, .None);
|
|
605
609
|
{
|
|
606
610
|
// This feels (and is) disgusting but the cartridge literally just overwrites
|
|
607
611
|
// the opponent's defense with the user's defense and resets it after. As a
|
|
@@ -609,7 +613,9 @@ fn beforeMove(
|
|
|
609
613
|
const def = foe.active.stats.def;
|
|
610
614
|
foe.active.stats.def = active.stats.def;
|
|
611
615
|
defer foe.active.stats.def = def;
|
|
612
|
-
if (!calcDamage(battle, player, player.foe(), null, false))
|
|
616
|
+
if (!calcDamage(battle, player, player.foe(), null, false, options)) {
|
|
617
|
+
return .err;
|
|
618
|
+
}
|
|
613
619
|
}
|
|
614
620
|
// Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
|
|
615
621
|
// on the targeting behavior of the confused Pokémon's selected move which results
|
|
@@ -638,9 +644,12 @@ fn beforeMove(
|
|
|
638
644
|
volatiles.Thrashing = false;
|
|
639
645
|
volatiles.Charging = false;
|
|
640
646
|
volatiles.Binding = false;
|
|
641
|
-
|
|
647
|
+
|
|
648
|
+
options.chance.observe(.attacking, player, .None);
|
|
649
|
+
options.chance.observe(.binding, player, .None);
|
|
650
|
+
|
|
642
651
|
// GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
|
|
643
|
-
try log.cant(ident, .Paralysis);
|
|
652
|
+
try log.cant(.{ ident, .Paralysis });
|
|
644
653
|
return .done;
|
|
645
654
|
}
|
|
646
655
|
|
|
@@ -656,28 +665,22 @@ fn beforeMove(
|
|
|
656
665
|
volatiles.state +%= battle.last_damage;
|
|
657
666
|
}
|
|
658
667
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
if (options.calc.modify(player, .attacking)) |extend| {
|
|
662
|
-
if (!extend) volatiles.attacks = 0;
|
|
663
|
-
} else {
|
|
664
|
-
volatiles.attacks -= 1;
|
|
665
|
-
}
|
|
666
|
-
try options.chance.attacking(player, volatiles.attacks);
|
|
668
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
669
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
667
670
|
|
|
668
671
|
if (volatiles.attacks != 0) {
|
|
669
|
-
try log.activate(ident, .Bide);
|
|
672
|
+
try log.activate(.{ ident, .Bide });
|
|
670
673
|
return .done;
|
|
671
674
|
}
|
|
672
675
|
|
|
673
676
|
volatiles.Bide = false;
|
|
674
|
-
try log.end(ident, .Bide);
|
|
677
|
+
try log.end(.{ ident, .Bide });
|
|
675
678
|
|
|
676
679
|
battle.last_damage = volatiles.state *% 2;
|
|
677
680
|
volatiles.state = 0;
|
|
678
681
|
|
|
679
682
|
if (battle.last_damage == 0) {
|
|
680
|
-
try log.fail(ident, .None);
|
|
683
|
+
try log.fail(.{ ident, .None });
|
|
681
684
|
return .done;
|
|
682
685
|
}
|
|
683
686
|
|
|
@@ -697,47 +700,42 @@ fn beforeMove(
|
|
|
697
700
|
}
|
|
698
701
|
|
|
699
702
|
if (volatiles.Thrashing) {
|
|
700
|
-
|
|
701
|
-
assert(from != null);
|
|
703
|
+
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
702
704
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
} else {
|
|
706
|
-
volatiles.attacks -= 1;
|
|
707
|
-
}
|
|
708
|
-
try options.chance.attacking(player, volatiles.attacks);
|
|
705
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
706
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
709
707
|
|
|
710
|
-
if (
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
708
|
+
if (volatiles.attacks == 0) {
|
|
709
|
+
const overwritten = volatiles.Confusion;
|
|
710
|
+
|
|
711
|
+
volatiles.Thrashing = false;
|
|
712
|
+
volatiles.Confusion = true;
|
|
713
|
+
volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
714
|
+
|
|
715
|
+
// On Pokémon Showdown this could leak information to the opponent that thrashing has
|
|
716
|
+
// ended (the engine's protocol is always from the omniscient perspective and clients
|
|
717
|
+
// can be expected to recognize this pattern and pass the information only to the
|
|
718
|
+
// correct player)
|
|
719
|
+
try log.start(.{ battle.active(player), .ConfusionSilent });
|
|
720
|
+
options.chance.observe(.confusion, player, if (overwritten)
|
|
721
|
+
if (pkmn.options.overwrite) .overwritten else .continuing
|
|
722
|
+
else
|
|
723
|
+
.started);
|
|
724
|
+
if (!overwritten) options.calc.confusion(player);
|
|
721
725
|
}
|
|
726
|
+
|
|
727
|
+
// This shouldn't actually set last_used_move, but Pokémon Showdown sets last
|
|
728
|
+
// used in useMove and doesn't have the notion of skipping canMove semantics
|
|
729
|
+
if (showdown) side.last_used_move = side.last_selected_move;
|
|
730
|
+
|
|
722
731
|
return .skip_can;
|
|
723
732
|
}
|
|
724
733
|
|
|
725
734
|
if (volatiles.Binding) {
|
|
726
|
-
|
|
735
|
+
volatiles.attacks = decrement(.binding, player, options, volatiles.attacks);
|
|
736
|
+
try options.chance.binding(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
727
737
|
|
|
728
|
-
|
|
729
|
-
if (!extend) volatiles.attacks = 0;
|
|
730
|
-
} else {
|
|
731
|
-
volatiles.attacks -= 1;
|
|
732
|
-
}
|
|
733
|
-
try options.chance.binding(player, volatiles.attacks);
|
|
734
|
-
|
|
735
|
-
try log.move(
|
|
736
|
-
battle.active(player),
|
|
737
|
-
side.last_selected_move,
|
|
738
|
-
battle.active(player.foe()),
|
|
739
|
-
side.last_selected_move,
|
|
740
|
-
);
|
|
738
|
+
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
741
739
|
if (showdown or battle.last_damage != 0) {
|
|
742
740
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
743
741
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
@@ -757,7 +755,7 @@ fn canMove(
|
|
|
757
755
|
mslot: u4,
|
|
758
756
|
auto: bool,
|
|
759
757
|
skip_pp: bool,
|
|
760
|
-
from:
|
|
758
|
+
from: Move,
|
|
761
759
|
residual: *bool,
|
|
762
760
|
options: anytype,
|
|
763
761
|
) !bool {
|
|
@@ -765,33 +763,21 @@ fn canMove(
|
|
|
765
763
|
const player_ident = battle.active(player);
|
|
766
764
|
const move = Move.get(side.last_selected_move);
|
|
767
765
|
|
|
768
|
-
const foe_last = battle.foe(player).last_used_move;
|
|
769
|
-
const special = from != null and (from.? == .Metronome and
|
|
770
|
-
(from.? == .MirrorMove and !(foe_last == .None or foe_last == .MirrorMove)));
|
|
771
|
-
|
|
772
|
-
var skip = skip_pp;
|
|
773
766
|
if (side.active.volatiles.Charging) {
|
|
774
767
|
side.active.volatiles.Charging = false;
|
|
775
768
|
side.active.volatiles.Invulnerable = false;
|
|
776
769
|
} else if (move.effect == .Charge) {
|
|
777
|
-
try options.log.move(player_ident, side.last_selected_move,
|
|
770
|
+
try options.log.move(.{ player_ident, side.last_selected_move, ID{}, from });
|
|
778
771
|
setCounterable(battle, player, side, move);
|
|
779
772
|
try Effects.charge(battle, player, options);
|
|
780
773
|
return false;
|
|
781
774
|
}
|
|
782
775
|
|
|
783
|
-
|
|
784
|
-
if (!
|
|
776
|
+
side.last_used_move = side.last_selected_move;
|
|
777
|
+
if (!skip_pp) decrementPP(side, mslot, auto);
|
|
785
778
|
|
|
786
779
|
const target = if (move.target == .Self) player else player.foe();
|
|
787
|
-
|
|
788
|
-
// don't have space to be able to track it in all circumstances and thus usually try to infer it
|
|
789
|
-
// from side.last_used_move. However, this gets reset by the opponent switching or fainting,
|
|
790
|
-
// meaning sometimes we lose this information. from is still set because it not being null is
|
|
791
|
-
// relevant for control flow, but we don't have anything useful to put in the log. This is
|
|
792
|
-
// regrettable, but this information is more "nice to have" than required
|
|
793
|
-
const f = if (from != null and from.? == .None) null else from;
|
|
794
|
-
try options.log.move(player_ident, side.last_selected_move, battle.active(target), f);
|
|
780
|
+
try options.log.move(.{ player_ident, side.last_selected_move, battle.active(target), from });
|
|
795
781
|
setCounterable(battle, player, side, move);
|
|
796
782
|
|
|
797
783
|
if (move.effect.onBegin()) {
|
|
@@ -801,9 +787,14 @@ fn canMove(
|
|
|
801
787
|
|
|
802
788
|
if (move.effect == .Thrashing) {
|
|
803
789
|
Effects.thrashing(battle, player, options);
|
|
804
|
-
} else if (
|
|
805
|
-
// Pokémon Showdown handles this after hit/miss checks and damage calculation
|
|
806
|
-
|
|
790
|
+
} else if (move.effect == .Binding) {
|
|
791
|
+
// Pokémon Showdown handles this after hit/miss checks and damage calculation, though clears
|
|
792
|
+
// Recharging in onTryMove (required for the Hyper Beam automatic selection glitch)
|
|
793
|
+
if (showdown) {
|
|
794
|
+
battle.foe(player).active.volatiles.Recharging = false;
|
|
795
|
+
} else {
|
|
796
|
+
try Effects.binding(battle, player, false, options);
|
|
797
|
+
}
|
|
807
798
|
}
|
|
808
799
|
|
|
809
800
|
return true;
|
|
@@ -829,16 +820,18 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
|
|
|
829
820
|
assert(!volatiles.Rage and !volatiles.Thrashing and !volatiles.MultiHit);
|
|
830
821
|
if (volatiles.Bide) return;
|
|
831
822
|
|
|
832
|
-
|
|
833
|
-
|
|
823
|
+
var move_slot = active.move(mslot);
|
|
824
|
+
assert(move_slot.pp > 0 or auto);
|
|
825
|
+
move_slot.pp = @as(u6, @intCast(move_slot.pp)) -% 1;
|
|
834
826
|
if (volatiles.Transform) return;
|
|
835
827
|
|
|
836
|
-
|
|
837
|
-
|
|
828
|
+
move_slot = side.stored().move(mslot);
|
|
829
|
+
assert(move_slot.pp > 0 or auto);
|
|
830
|
+
move_slot.pp = @as(u6, @intCast(move_slot.pp)) -% 1;
|
|
838
831
|
assert(active.move(mslot).pp == side.stored().move(mslot).pp);
|
|
839
832
|
}
|
|
840
833
|
|
|
841
|
-
fn incrementPP(side: *Side, mslot: u4) void {
|
|
834
|
+
fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
|
|
842
835
|
var active = &side.active;
|
|
843
836
|
const volatiles = &active.volatiles;
|
|
844
837
|
|
|
@@ -846,8 +839,11 @@ fn incrementPP(side: *Side, mslot: u4) void {
|
|
|
846
839
|
// GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
|
|
847
840
|
if (showdown and volatiles.Transform) return;
|
|
848
841
|
|
|
842
|
+
const n = options.calc.overridden(player, .pp) orelse 1;
|
|
843
|
+
if (volatiles.Transform and !options.chance.pp(player, n)) return;
|
|
844
|
+
|
|
849
845
|
assert(mslot > 0 and mslot <= 4);
|
|
850
|
-
side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +%
|
|
846
|
+
side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% n;
|
|
851
847
|
}
|
|
852
848
|
|
|
853
849
|
// Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
|
|
@@ -895,13 +891,13 @@ fn doMove(
|
|
|
895
891
|
break :miss true;
|
|
896
892
|
}
|
|
897
893
|
break :miss (move.target != .Self and
|
|
898
|
-
!moveHit(battle, player, move, &immune, &mist, options));
|
|
894
|
+
!try moveHit(battle, player, move, &immune, &mist, options));
|
|
899
895
|
};
|
|
900
896
|
assert(!immune or miss or (showdown and move.effect == .Binding));
|
|
901
897
|
|
|
902
898
|
var late = showdown and move.effect != .Explode;
|
|
903
899
|
const skip = status or immune;
|
|
904
|
-
if ((!showdown or (!skip or counter)) and !miss)
|
|
900
|
+
if ((!showdown or (!skip or counter)) and !miss) damage: {
|
|
905
901
|
if (showdown and move.effect.isMulti()) {
|
|
906
902
|
try Effects.multiHit(battle, player, move, options);
|
|
907
903
|
hits = side.active.volatiles.attacks;
|
|
@@ -923,8 +919,9 @@ fn doMove(
|
|
|
923
919
|
ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
|
|
924
920
|
// This can overflow after adjustDamage, but will still be sufficient to OHKO
|
|
925
921
|
battle.last_damage = if (ohko) 65535 else 0;
|
|
926
|
-
if (showdown) break :
|
|
927
|
-
} else if (!calcDamage(battle, player, player.foe(), move, crit)) {
|
|
922
|
+
if (showdown) break :damage; // skip adjustDamage / randomizeDamage
|
|
923
|
+
} else if (!calcDamage(battle, player, player.foe(), move, crit, options)) {
|
|
924
|
+
if (!showdown) try options.chance.commit(player, .err);
|
|
928
925
|
return @as(?Result, Result.Error);
|
|
929
926
|
}
|
|
930
927
|
if (battle.last_damage == 0) {
|
|
@@ -938,11 +935,12 @@ fn doMove(
|
|
|
938
935
|
}
|
|
939
936
|
}
|
|
940
937
|
|
|
941
|
-
var
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
938
|
+
var missed: bool = undefined;
|
|
939
|
+
const zero = battle.last_damage == 0;
|
|
940
|
+
if (!showdown and !skip) {
|
|
941
|
+
missed = !try moveHit(battle, player, move, &immune, &mist, options);
|
|
942
|
+
miss = missed or zero;
|
|
943
|
+
}
|
|
946
944
|
|
|
947
945
|
assert(showdown or miss or battle.last_damage > 0 or skip);
|
|
948
946
|
assert((!showdown and miss) or !(ohko and immune));
|
|
@@ -966,16 +964,37 @@ fn doMove(
|
|
|
966
964
|
ohko = (!showdown or (!immune and !invulnerable)) and
|
|
967
965
|
move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
|
|
968
966
|
if (ohko) {
|
|
969
|
-
try log.immune(foe_ident, .OHKO);
|
|
967
|
+
try log.immune(.{ foe_ident, .OHKO });
|
|
970
968
|
} else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
|
|
971
|
-
try log.immune(foe_ident, .None);
|
|
969
|
+
try log.immune(.{ foe_ident, .None });
|
|
970
|
+
if (!showdown) {
|
|
971
|
+
if (move.effect == .Binding) {
|
|
972
|
+
// Rather obnoxiously, the cartridge sets the Binding volatile in canMove and
|
|
973
|
+
// then simply clears it later on if the move happens to miss in moveHit.
|
|
974
|
+
// However, moveHit can return true and we'll still end up in the outer miss
|
|
975
|
+
// handling block because attacking into an immune Pokemon does zero damage
|
|
976
|
+
// (considered missing)
|
|
977
|
+
assert(zero);
|
|
978
|
+
// Thus we still need to actually save the hit result (but not the crit roll).
|
|
979
|
+
// Using .miss for this commit would accomplish what we want, but is somewhat
|
|
980
|
+
// misleading as its entirely possible the move actually did hit (and the target
|
|
981
|
+
// is now trapped) so we use the .binding value instead which is effectively
|
|
982
|
+
// identical, just doesn't claim to know about the actual results of the hit
|
|
983
|
+
// roll)
|
|
984
|
+
try options.chance.commit(player, .binding);
|
|
985
|
+
} else {
|
|
986
|
+
// The .glitch option allows for conditionally committing if we are in a
|
|
987
|
+
// division-by-zero freeze alternative timeline scenario
|
|
988
|
+
try options.chance.commit(player, .glitch);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
972
991
|
} else if (mist) {
|
|
973
|
-
if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
|
|
974
|
-
try log.fail(foe_ident, .None);
|
|
992
|
+
if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
|
|
993
|
+
try log.fail(.{ foe_ident, .None });
|
|
975
994
|
} else {
|
|
976
|
-
if (showdown
|
|
977
|
-
try log.lastmiss();
|
|
978
|
-
try log.miss(battle.active(player));
|
|
995
|
+
if (!showdown) try options.chance.commit(player, if (zero and !missed) .hit else .miss);
|
|
996
|
+
try log.lastmiss(.{});
|
|
997
|
+
try log.miss(.{battle.active(player)});
|
|
979
998
|
}
|
|
980
999
|
if (move.effect == .JumpKick) {
|
|
981
1000
|
// Recoil is supposed to be damage/8 but damage will always be 0 here
|
|
@@ -988,17 +1007,15 @@ fn doMove(
|
|
|
988
1007
|
try buildRage(battle, player.foe(), options);
|
|
989
1008
|
} else if (showdown and move.effect == .Disable) {
|
|
990
1009
|
try buildRage(battle, player.foe(), options);
|
|
991
|
-
} else if (!showdown and immune and move.effect == .Binding) {
|
|
992
|
-
try options.chance.commit(player, .binding);
|
|
993
1010
|
}
|
|
994
1011
|
return null;
|
|
995
|
-
} else {
|
|
1012
|
+
} else if (!showdown) {
|
|
996
1013
|
try options.chance.commit(player, .hit);
|
|
997
1014
|
}
|
|
998
1015
|
|
|
999
1016
|
// On the cartridge MultiHit doesn't get set up until after damage has been applied for the
|
|
1000
1017
|
// first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
|
|
1001
|
-
// it up above before damage calculation)
|
|
1018
|
+
// it up above before damage calculation)
|
|
1002
1019
|
if (!showdown and move.effect.isMulti()) {
|
|
1003
1020
|
try Effects.multiHit(battle, player, move, options);
|
|
1004
1021
|
hits = side.active.volatiles.attacks;
|
|
@@ -1011,15 +1028,15 @@ fn doMove(
|
|
|
1011
1028
|
var hit: u4 = 0;
|
|
1012
1029
|
while (hit < hits) {
|
|
1013
1030
|
if (hit == 0) {
|
|
1014
|
-
if (crit) try log.crit(battle.active(player.foe()));
|
|
1031
|
+
if (crit) try log.crit(.{battle.active(player.foe())});
|
|
1015
1032
|
if (effectiveness > Effectiveness.neutral) {
|
|
1016
|
-
try log.supereffective(battle.active(player.foe()));
|
|
1033
|
+
try log.supereffective(.{battle.active(player.foe())});
|
|
1017
1034
|
} else if (effectiveness < Effectiveness.neutral) {
|
|
1018
|
-
try log.resisted(battle.active(player.foe()));
|
|
1035
|
+
try log.resisted(.{battle.active(player.foe())});
|
|
1019
1036
|
}
|
|
1020
1037
|
}
|
|
1021
1038
|
if (!skip) nullified = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
1022
|
-
if (hit == 0 and ohko) try log.ohko();
|
|
1039
|
+
if (hit == 0 and ohko) try log.ohko(.{});
|
|
1023
1040
|
hit += 1;
|
|
1024
1041
|
if (foe.stored().hp == 0) break;
|
|
1025
1042
|
if (!late and (!sub or move.effect == .Explode)) {
|
|
@@ -1036,33 +1053,31 @@ fn doMove(
|
|
|
1036
1053
|
if (showdown and move.effect == .Twineedle and !nullified and foe.stored().hp > 0) {
|
|
1037
1054
|
try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
1038
1055
|
}
|
|
1039
|
-
try log.hitcount(battle.active(player.foe()), hit);
|
|
1040
|
-
} else if (showdown) {
|
|
1056
|
+
try log.hitcount(.{ battle.active(player.foe()), hit });
|
|
1057
|
+
} else if (showdown and move.effect == .Binding) {
|
|
1041
1058
|
// This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
try buildRage(battle, player.foe(), options);
|
|
1053
|
-
}
|
|
1054
|
-
return null;
|
|
1059
|
+
try Effects.binding(battle, player, rewrap, options);
|
|
1060
|
+
if (immune) {
|
|
1061
|
+
battle.last_damage = 0;
|
|
1062
|
+
assert(foe.stored().hp > 0);
|
|
1063
|
+
// Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
|
|
1064
|
+
if (sub) {
|
|
1065
|
+
try log.activate(.{ battle.active(player.foe()), .Substitute });
|
|
1066
|
+
} else {
|
|
1067
|
+
try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
|
|
1068
|
+
try buildRage(battle, player.foe(), options);
|
|
1055
1069
|
}
|
|
1070
|
+
return null;
|
|
1056
1071
|
}
|
|
1057
1072
|
}
|
|
1058
1073
|
|
|
1059
1074
|
// Substitute being broken nullifies the move's effect completely so even
|
|
1060
|
-
// if an effect was intended to "always happen" it will still get skipped
|
|
1075
|
+
// if an effect was intended to "always happen" it will still get skipped
|
|
1061
1076
|
if (nullified) return null;
|
|
1062
1077
|
|
|
1063
1078
|
// On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
|
|
1064
1079
|
// but this is only done to setup the MultiHit looping in the first place. Moving the MultiHit
|
|
1065
|
-
// setup before the loop means we can avoid having to waste time doing no-op handler searches
|
|
1080
|
+
// setup before the loop means we can avoid having to waste time doing no-op handler searches
|
|
1066
1081
|
if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
|
|
1067
1082
|
|
|
1068
1083
|
if (foe.stored().hp == 0) return null;
|
|
@@ -1077,7 +1092,7 @@ fn doMove(
|
|
|
1077
1092
|
// below). For Twineedle we change the data to that of one with PoisonChance1 given its
|
|
1078
1093
|
// MultiHit behavior is complete after the loop above, though Pokémon Showdown handles the
|
|
1079
1094
|
// Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
|
|
1080
|
-
// puts the |-status| message before |-hitcount| instead of after
|
|
1095
|
+
// puts the |-status| message before |-hitcount| instead of after
|
|
1081
1096
|
if (move.effect == .Twineedle) {
|
|
1082
1097
|
if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
1083
1098
|
} else if (move.effect == .Disable) {
|
|
@@ -1096,12 +1111,14 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
|
|
|
1096
1111
|
const side = battle.side(player);
|
|
1097
1112
|
|
|
1098
1113
|
// Base speed is used for the critical hit calculation, even when Transform-ed
|
|
1099
|
-
var rate =
|
|
1114
|
+
var rate: u16 = Species.chance(side.stored().species);
|
|
1100
1115
|
// GLITCH: Focus Energy reduces critical hit rate instead of increasing it
|
|
1101
1116
|
rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
|
|
1102
1117
|
rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
|
|
1103
1118
|
|
|
1104
|
-
|
|
1119
|
+
const spurious =
|
|
1120
|
+
!showdown and ((move.bp == 0 and move.effect != .OHKO) or move.target == .Depends);
|
|
1121
|
+
return Rolls.criticalHit(battle, player, @intCast(rate), spurious, options);
|
|
1105
1122
|
}
|
|
1106
1123
|
|
|
1107
1124
|
fn calcDamage(
|
|
@@ -1110,6 +1127,7 @@ fn calcDamage(
|
|
|
1110
1127
|
target_player: Player,
|
|
1111
1128
|
m: ?Move.Data,
|
|
1112
1129
|
crit: bool,
|
|
1130
|
+
options: anytype,
|
|
1113
1131
|
) bool {
|
|
1114
1132
|
// Confusion (indicated when m == null) just needs a 40 BP physical move
|
|
1115
1133
|
const cfz = m == null;
|
|
@@ -1121,28 +1139,8 @@ fn calcDamage(
|
|
|
1121
1139
|
|
|
1122
1140
|
const special = move.type.special();
|
|
1123
1141
|
|
|
1124
|
-
|
|
1125
|
-
var
|
|
1126
|
-
if (crit)
|
|
1127
|
-
if (special) side.stored().stats.spc
|
|
1128
|
-
else side.stored().stats.atk
|
|
1129
|
-
else
|
|
1130
|
-
if (special) side.active.stats.spc
|
|
1131
|
-
else side.active.stats.atk;
|
|
1132
|
-
var def: u32 =
|
|
1133
|
-
if (crit)
|
|
1134
|
-
if (special) target.stored().stats.spc
|
|
1135
|
-
else target.stored().stats.def
|
|
1136
|
-
else
|
|
1137
|
-
// GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
|
|
1138
|
-
if (special)
|
|
1139
|
-
target.active.stats.spc *
|
|
1140
|
-
@as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
|
|
1141
|
-
// Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
|
|
1142
|
-
else
|
|
1143
|
-
target.active.stats.def *
|
|
1144
|
-
@as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
|
|
1145
|
-
// zig fmt: on
|
|
1142
|
+
var atk = attack(side, crit, special);
|
|
1143
|
+
var def = defense(target, crit, special, cfz);
|
|
1146
1144
|
|
|
1147
1145
|
// Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
|
|
1148
1146
|
// will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
|
|
@@ -1150,12 +1148,12 @@ fn calcDamage(
|
|
|
1150
1148
|
if ((!showdown or !cfz) and (atk > 255 or def > 255)) {
|
|
1151
1149
|
atk = @max((atk / 4) & 255, 1);
|
|
1152
1150
|
// GLITCH: not adjusted to be a min of 1 on cartridge (can lead to division-by-zero freeze)
|
|
1153
|
-
def = @max((def / 4) & 255, if (
|
|
1151
|
+
def = @max((def / 4) & 255, if (pkmn.options.mod) 1 else 0);
|
|
1154
1152
|
}
|
|
1155
1153
|
|
|
1156
|
-
const lvl =
|
|
1154
|
+
const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
|
|
1157
1155
|
|
|
1158
|
-
def =
|
|
1156
|
+
def = if (move.effect == .Explode) @max(def / 2, 1) else def;
|
|
1159
1157
|
|
|
1160
1158
|
if (def == 0) return false;
|
|
1161
1159
|
|
|
@@ -1167,11 +1165,41 @@ fn calcDamage(
|
|
|
1167
1165
|
d = @min(997, d);
|
|
1168
1166
|
d += 2;
|
|
1169
1167
|
|
|
1168
|
+
// Whether or not a critical hit occurred can change whether a division-by-zero freeze is
|
|
1169
|
+
// encountered so we need to make sure that in the event of a miss we commit the status of the
|
|
1170
|
+
// crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
|
|
1171
|
+
// that results in counter intuitive probabilities in the common case
|
|
1172
|
+
if (pkmn.options.chance and !pkmn.options.mod) {
|
|
1173
|
+
atk = attack(side, !crit, special);
|
|
1174
|
+
def = defense(target, !crit, special, false);
|
|
1175
|
+
if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1170
1178
|
battle.last_damage = @intCast(d);
|
|
1171
1179
|
|
|
1172
1180
|
return true;
|
|
1173
1181
|
}
|
|
1174
1182
|
|
|
1183
|
+
fn attack(side: *Side, crit: bool, special: bool) u32 {
|
|
1184
|
+
return if (crit)
|
|
1185
|
+
if (special) side.stored().stats.spc else side.stored().stats.atk
|
|
1186
|
+
else if (special) side.active.stats.spc else side.active.stats.atk;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
fn defense(target: *Side, crit: bool, special: bool, cfz: bool) u32 {
|
|
1190
|
+
return if (crit)
|
|
1191
|
+
if (special) target.stored().stats.spc else target.stored().stats.def
|
|
1192
|
+
else
|
|
1193
|
+
// GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
|
|
1194
|
+
if (special)
|
|
1195
|
+
target.active.stats.spc *
|
|
1196
|
+
@as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
|
|
1197
|
+
else
|
|
1198
|
+
// Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
|
|
1199
|
+
target.active.stats.def *
|
|
1200
|
+
@as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1175
1203
|
fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
1176
1204
|
const side = battle.side(player);
|
|
1177
1205
|
const foe = battle.foe(player);
|
|
@@ -1216,7 +1244,6 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1216
1244
|
const foe = battle.foe(player);
|
|
1217
1245
|
|
|
1218
1246
|
if (!try checkHit(battle, player, move, options)) return null;
|
|
1219
|
-
try options.chance.commit(player, .hit);
|
|
1220
1247
|
|
|
1221
1248
|
battle.last_damage = switch (side.last_selected_move) {
|
|
1222
1249
|
.SuperFang => @max(foe.stored().hp / 2, 1),
|
|
@@ -1227,13 +1254,13 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1227
1254
|
.Psywave => power: {
|
|
1228
1255
|
const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
|
|
1229
1256
|
// GLITCH: Psywave infinite glitch loop
|
|
1230
|
-
if (!
|
|
1257
|
+
if (!pkmn.options.mod and max <= 1) return Result.Error;
|
|
1231
1258
|
break :power try Rolls.psywave(battle, player, max, options);
|
|
1232
1259
|
},
|
|
1233
1260
|
else => unreachable,
|
|
1234
1261
|
};
|
|
1235
1262
|
|
|
1236
|
-
if (battle.last_damage == 0) return if (
|
|
1263
|
+
if (battle.last_damage == 0) return if (pkmn.options.mod) null else Result.Error;
|
|
1237
1264
|
|
|
1238
1265
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1239
1266
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
@@ -1246,7 +1273,7 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1246
1273
|
const foe = battle.foe(player);
|
|
1247
1274
|
|
|
1248
1275
|
if (battle.last_damage == 0) {
|
|
1249
|
-
try options.log.fail(battle.active(player), .None);
|
|
1276
|
+
try options.log.fail(.{ battle.active(player), .None });
|
|
1250
1277
|
return null;
|
|
1251
1278
|
}
|
|
1252
1279
|
|
|
@@ -1264,22 +1291,21 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1264
1291
|
foe_last_selected_move.type == .Fighting);
|
|
1265
1292
|
|
|
1266
1293
|
if (!used and !selected) {
|
|
1267
|
-
try options.log.fail(battle.active(player), .None);
|
|
1294
|
+
try options.log.fail(.{ battle.active(player), .None });
|
|
1268
1295
|
return null;
|
|
1269
1296
|
}
|
|
1270
1297
|
|
|
1271
1298
|
if (!used or !selected) {
|
|
1272
1299
|
// GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
|
|
1273
|
-
if (!
|
|
1274
|
-
try options.log.fail(battle.active(player), .None);
|
|
1300
|
+
if (!pkmn.options.mod) return Result.Error;
|
|
1301
|
+
try options.log.fail(.{ battle.active(player), .None });
|
|
1275
1302
|
return null;
|
|
1276
1303
|
}
|
|
1277
1304
|
|
|
1278
1305
|
battle.last_damage *|= 2;
|
|
1279
1306
|
|
|
1280
|
-
// Pokémon Showdown calls
|
|
1307
|
+
// Pokémon Showdown calls moveHit before Counter
|
|
1281
1308
|
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
1282
|
-
try options.chance.commit(player, .hit);
|
|
1283
1309
|
|
|
1284
1310
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1285
1311
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
@@ -1307,20 +1333,20 @@ fn applyDamage(
|
|
|
1307
1333
|
subbed.active.volatiles.substitute = 0;
|
|
1308
1334
|
subbed.active.volatiles.Substitute = false;
|
|
1309
1335
|
// battle.last_damage is not updated with the amount of HP the Substitute had
|
|
1310
|
-
try options.log.end(battle.active(sub_player), .Substitute);
|
|
1311
|
-
options.calc.capped(
|
|
1336
|
+
try options.log.end(.{ battle.active(sub_player), .Substitute });
|
|
1337
|
+
if (reason != .Confusion) options.calc.capped(sub_player);
|
|
1312
1338
|
return true;
|
|
1313
1339
|
} else {
|
|
1314
1340
|
// Safe to truncate since less than subbed.volatiles.substitute which is a u8
|
|
1315
1341
|
subbed.active.volatiles.substitute -= @intCast(battle.last_damage);
|
|
1316
|
-
try options.log.activate(battle.active(sub_player), .Substitute);
|
|
1342
|
+
try options.log.activate(.{ battle.active(sub_player), .Substitute });
|
|
1317
1343
|
return false;
|
|
1318
1344
|
}
|
|
1319
1345
|
}
|
|
1320
1346
|
|
|
1321
1347
|
if (battle.last_damage > target.stored().hp) battle.last_damage = target.stored().hp;
|
|
1322
1348
|
target.stored().hp -= battle.last_damage;
|
|
1323
|
-
try options.log.damage(battle.active(target_player), target.stored(), reason);
|
|
1349
|
+
try options.log.damage(.{ battle.active(target_player), target.stored(), reason });
|
|
1324
1350
|
return false;
|
|
1325
1351
|
}
|
|
1326
1352
|
|
|
@@ -1339,10 +1365,10 @@ fn mirrorMove(
|
|
|
1339
1365
|
side.last_selected_move = foe.last_used_move;
|
|
1340
1366
|
|
|
1341
1367
|
if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
|
|
1342
|
-
try options.log.fail(battle.active(player), .None);
|
|
1368
|
+
try options.log.fail(.{ battle.active(player), .None });
|
|
1343
1369
|
return null;
|
|
1344
1370
|
} else if (!showdown or foe.last_used_move != .Struggle) {
|
|
1345
|
-
incrementPP(side, mslot);
|
|
1371
|
+
incrementPP(side, player, mslot, options);
|
|
1346
1372
|
}
|
|
1347
1373
|
|
|
1348
1374
|
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
|
|
@@ -1363,7 +1389,7 @@ fn metronome(
|
|
|
1363
1389
|
var side = battle.side(player);
|
|
1364
1390
|
|
|
1365
1391
|
side.last_selected_move = try Rolls.metronome(battle, player, options);
|
|
1366
|
-
incrementPP(side, mslot);
|
|
1392
|
+
incrementPP(side, player, mslot, options);
|
|
1367
1393
|
|
|
1368
1394
|
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
|
|
1369
1395
|
return null;
|
|
@@ -1375,20 +1401,23 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
|
|
|
1375
1401
|
var immune = false;
|
|
1376
1402
|
var mist = false;
|
|
1377
1403
|
|
|
1378
|
-
if (moveHit(battle, player, move, &immune, &mist, options))
|
|
1404
|
+
if (try moveHit(battle, player, move, &immune, &mist, options)) {
|
|
1405
|
+
if (!showdown) try options.chance.commit(player, .hit);
|
|
1406
|
+
return true;
|
|
1407
|
+
}
|
|
1379
1408
|
|
|
1380
1409
|
assert(!immune);
|
|
1381
1410
|
if (mist) {
|
|
1382
1411
|
assert(!showdown);
|
|
1383
1412
|
const foe_ident = battle.active(player.foe());
|
|
1384
|
-
try options.log.activate(foe_ident, .Mist);
|
|
1385
|
-
try options.log.fail(foe_ident, .None);
|
|
1413
|
+
try options.log.activate(.{ foe_ident, .Mist });
|
|
1414
|
+
try options.log.fail(.{ foe_ident, .None });
|
|
1386
1415
|
} else {
|
|
1387
|
-
try options.
|
|
1388
|
-
try options.log.
|
|
1389
|
-
try options.log.miss(battle.active(player));
|
|
1416
|
+
try options.log.lastmiss(.{});
|
|
1417
|
+
try options.log.miss(.{battle.active(player)});
|
|
1390
1418
|
}
|
|
1391
1419
|
|
|
1420
|
+
if (!showdown) try options.chance.commit(player, .miss);
|
|
1392
1421
|
return false;
|
|
1393
1422
|
}
|
|
1394
1423
|
|
|
@@ -1399,7 +1428,7 @@ fn moveHit(
|
|
|
1399
1428
|
immune: *bool,
|
|
1400
1429
|
mist: *bool,
|
|
1401
1430
|
options: anytype,
|
|
1402
|
-
) bool {
|
|
1431
|
+
) !bool {
|
|
1403
1432
|
var side = battle.side(player);
|
|
1404
1433
|
const foe = battle.foe(player);
|
|
1405
1434
|
|
|
@@ -1438,7 +1467,7 @@ fn moveHit(
|
|
|
1438
1467
|
const overwritten = overwrite and state > 0;
|
|
1439
1468
|
assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
|
|
1440
1469
|
|
|
1441
|
-
var accuracy = if (overwritten) state else
|
|
1470
|
+
var accuracy: u16 = if (overwritten) state else move.accuracy;
|
|
1442
1471
|
var boost = BOOSTS[@as(u4, @intCast(@as(i8, side.active.boosts.accuracy) + 6))];
|
|
1443
1472
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1444
1473
|
boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
|
|
@@ -1450,7 +1479,7 @@ fn moveHit(
|
|
|
1450
1479
|
if (overwrite) side.active.volatiles.state = accuracy;
|
|
1451
1480
|
|
|
1452
1481
|
// GLITCH: max accuracy is 255 so 1/256 chance of miss
|
|
1453
|
-
break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
|
|
1482
|
+
break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
|
|
1454
1483
|
};
|
|
1455
1484
|
|
|
1456
1485
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1461,13 +1490,17 @@ fn moveHit(
|
|
|
1461
1490
|
|
|
1462
1491
|
if (!miss) return true;
|
|
1463
1492
|
if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
|
|
1493
|
+
|
|
1464
1494
|
side.active.volatiles.Binding = false;
|
|
1495
|
+
options.chance.observe(.binding, player, .None);
|
|
1496
|
+
|
|
1465
1497
|
return false;
|
|
1466
1498
|
}
|
|
1467
1499
|
|
|
1468
1500
|
fn checkFaint(
|
|
1469
1501
|
battle: anytype,
|
|
1470
1502
|
player: Player,
|
|
1503
|
+
cap: bool,
|
|
1471
1504
|
options: anytype,
|
|
1472
1505
|
) @TypeOf(options.log).Error!?Result {
|
|
1473
1506
|
const side = battle.side(player);
|
|
@@ -1477,33 +1510,32 @@ fn checkFaint(
|
|
|
1477
1510
|
const foe_fainted = foe.stored().hp == 0;
|
|
1478
1511
|
|
|
1479
1512
|
const player_out = findFirstAlive(side) == 0;
|
|
1480
|
-
const foe_out = findFirstAlive(foe) == 0;
|
|
1481
|
-
const
|
|
1482
|
-
const more = tie or player_out or foe_out;
|
|
1513
|
+
const foe_out = foe_fainted and findFirstAlive(foe) == 0;
|
|
1514
|
+
const more = player_out or foe_out;
|
|
1483
1515
|
|
|
1484
|
-
if (try faint(battle, player, !(more or foe_fainted), options)) |r| return r;
|
|
1485
|
-
if (foe_fainted) if (try faint(battle, player.foe(), !more, options)) |r| return r;
|
|
1516
|
+
if (try faint(battle, player, !(more or foe_fainted), cap, options)) |r| return r;
|
|
1517
|
+
if (foe_fainted) if (try faint(battle, player.foe(), !more, false, options)) |r| return r;
|
|
1486
1518
|
|
|
1487
1519
|
assert(!side.active.volatiles.MultiHit);
|
|
1488
1520
|
assert(!foe.active.volatiles.MultiHit);
|
|
1489
1521
|
|
|
1490
|
-
if (
|
|
1491
|
-
try options.log.tie();
|
|
1522
|
+
if (player_out and foe_out) {
|
|
1523
|
+
try options.log.tie(.{});
|
|
1492
1524
|
return Result.Tie;
|
|
1493
1525
|
} else if (player_out) {
|
|
1494
|
-
try options.log.win(player.foe());
|
|
1526
|
+
try options.log.win(.{player.foe()});
|
|
1495
1527
|
return if (player == .P1) Result.Lose else Result.Win;
|
|
1496
1528
|
} else if (foe_out) {
|
|
1497
|
-
try options.log.win(player);
|
|
1529
|
+
try options.log.win(.{player});
|
|
1498
1530
|
return if (player == .P1) Result.Win else Result.Lose;
|
|
1499
1531
|
}
|
|
1500
1532
|
|
|
1501
1533
|
const foe_choice: Choice.Type = if (foe_fainted) .Switch else .Pass;
|
|
1502
|
-
if (player == .P1) return
|
|
1503
|
-
return
|
|
1534
|
+
if (player == .P1) return .{ .p1 = .Switch, .p2 = foe_choice };
|
|
1535
|
+
return .{ .p1 = foe_choice, .p2 = .Switch };
|
|
1504
1536
|
}
|
|
1505
1537
|
|
|
1506
|
-
fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
|
|
1538
|
+
fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytype) !?Result {
|
|
1507
1539
|
var side = battle.side(player);
|
|
1508
1540
|
var foe = battle.foe(player);
|
|
1509
1541
|
assert(side.stored().hp == 0);
|
|
@@ -1512,7 +1544,7 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
|
|
|
1512
1544
|
assert(!foe_volatiles.MultiHit);
|
|
1513
1545
|
if (foe_volatiles.Bide) {
|
|
1514
1546
|
assert(!foe_volatiles.Thrashing and !foe_volatiles.Rage);
|
|
1515
|
-
foe_volatiles.state = if (
|
|
1547
|
+
foe_volatiles.state = if (pkmn.options.mod) 0 else foe_volatiles.state & 255;
|
|
1516
1548
|
if (foe_volatiles.state != 0) return Result.Error;
|
|
1517
1549
|
}
|
|
1518
1550
|
|
|
@@ -1536,8 +1568,10 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
|
|
|
1536
1568
|
side.stored().stats.spe;
|
|
1537
1569
|
}
|
|
1538
1570
|
|
|
1539
|
-
try options.log.faint(battle.active(player), done);
|
|
1540
|
-
|
|
1571
|
+
try options.log.faint(.{ battle.active(player), done });
|
|
1572
|
+
// We can only safely set capped if the faint occured as the direct result of an opponent's
|
|
1573
|
+
// move (as opposed to due to residual damage, recoil, Explosion, etc)
|
|
1574
|
+
if (cap) options.calc.capped(player);
|
|
1541
1575
|
return null;
|
|
1542
1576
|
}
|
|
1543
1577
|
|
|
@@ -1548,21 +1582,19 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
|
|
|
1548
1582
|
var volatiles = &side.active.volatiles;
|
|
1549
1583
|
|
|
1550
1584
|
const brn = Status.is(stored.status, .BRN);
|
|
1551
|
-
if (brn or Status.is(stored.status, .PSN))
|
|
1585
|
+
if (brn or Status.is(stored.status, .PSN)) {
|
|
1552
1586
|
var damage = @max(stored.stats.hp / 16, 1);
|
|
1553
|
-
|
|
1554
1587
|
if (volatiles.Toxic) {
|
|
1555
1588
|
volatiles.toxic += 1;
|
|
1556
1589
|
damage *= volatiles.toxic;
|
|
1557
1590
|
}
|
|
1591
|
+
damage = @min(damage, stored.hp);
|
|
1558
1592
|
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
// Pokémon Showdown uses damageOf here but its not relevant in Generation I
|
|
1565
|
-
try options.log.damage(ident, stored, if (brn) Damage.Burn else Damage.Poison);
|
|
1593
|
+
if (!showdown or damage > 0) {
|
|
1594
|
+
stored.hp -= damage;
|
|
1595
|
+
// Pokémon Showdown uses damage [of] here but its not relevant in Generation I
|
|
1596
|
+
try options.log.damage(.{ ident, stored, if (brn) Damage.Burn else Damage.Poison });
|
|
1597
|
+
}
|
|
1566
1598
|
}
|
|
1567
1599
|
|
|
1568
1600
|
if (volatiles.LeechSeed) {
|
|
@@ -1586,14 +1618,14 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
|
|
|
1586
1618
|
const amount = @min(damage, stored.hp);
|
|
1587
1619
|
stored.hp -= amount;
|
|
1588
1620
|
|
|
1589
|
-
// As above, Pokémon Showdown uses
|
|
1590
|
-
if (amount > 0) try options.log.damage(ident, stored, .LeechSeed);
|
|
1621
|
+
// As above, Pokémon Showdown uses damage [of] but its not relevant
|
|
1622
|
+
if (amount > 0) try options.log.damage(.{ ident, stored, .LeechSeed });
|
|
1591
1623
|
|
|
1592
1624
|
const before = foe_stored.hp;
|
|
1593
1625
|
// Uncapped damage is added back to the foe
|
|
1594
1626
|
foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
|
|
1595
1627
|
// Pokémon Showdown uses the less specific heal here instead of drain... because reasons?
|
|
1596
|
-
if (foe_stored.hp > before) try options.log.heal(foe_ident, foe_stored, .Silent);
|
|
1628
|
+
if (foe_stored.hp > before) try options.log.heal(.{ foe_ident, foe_stored, .Silent });
|
|
1597
1629
|
}
|
|
1598
1630
|
}
|
|
1599
1631
|
|
|
@@ -1603,19 +1635,19 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
|
|
|
1603
1635
|
|
|
1604
1636
|
battle.turn += 1;
|
|
1605
1637
|
|
|
1606
|
-
if (
|
|
1607
|
-
try options.log.tie();
|
|
1638
|
+
if (pkmn.options.mod and pkmn.options.ebc and checkEBC(battle)) {
|
|
1639
|
+
try options.log.tie(.{});
|
|
1608
1640
|
return Result.Tie;
|
|
1609
1641
|
}
|
|
1610
1642
|
|
|
1611
|
-
if (
|
|
1612
|
-
try options.log.tie();
|
|
1643
|
+
if (pkmn.options.mod and battle.turn >= 1000) {
|
|
1644
|
+
try options.log.tie(.{});
|
|
1613
1645
|
return Result.Tie;
|
|
1614
1646
|
} else if (battle.turn >= 65535) {
|
|
1615
1647
|
return Result.Error;
|
|
1616
1648
|
}
|
|
1617
1649
|
|
|
1618
|
-
try options.log.turn(battle.turn);
|
|
1650
|
+
try options.log.turn(.{battle.turn});
|
|
1619
1651
|
|
|
1620
1652
|
return Result.Default;
|
|
1621
1653
|
}
|
|
@@ -1675,9 +1707,9 @@ fn checkEBC(battle: anytype) bool {
|
|
|
1675
1707
|
}
|
|
1676
1708
|
|
|
1677
1709
|
fn disabled(side: *Side, ident: ID, options: anytype) !bool {
|
|
1678
|
-
if (side.active.volatiles.
|
|
1710
|
+
if (side.active.volatiles.disable_move != 0) {
|
|
1679
1711
|
// A Pokémon that transforms after being disabled may end up with less move slots
|
|
1680
|
-
const m = side.active.moves[side.active.volatiles.
|
|
1712
|
+
const m = side.active.moves[side.active.volatiles.disable_move - 1].id;
|
|
1681
1713
|
// side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
|
|
1682
1714
|
const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
|
|
1683
1715
|
m
|
|
@@ -1685,32 +1717,21 @@ fn disabled(side: *Side, ident: ID, options: anytype) !bool {
|
|
|
1685
1717
|
side.last_selected_move;
|
|
1686
1718
|
if (m != .None and m == last) {
|
|
1687
1719
|
side.active.volatiles.Charging = false;
|
|
1688
|
-
try options.log.
|
|
1720
|
+
try options.log.cant(.{ ident, .Disable, last });
|
|
1689
1721
|
return true;
|
|
1690
1722
|
}
|
|
1691
1723
|
}
|
|
1692
1724
|
return false;
|
|
1693
1725
|
}
|
|
1694
1726
|
|
|
1695
|
-
|
|
1727
|
+
fn buildRage(battle: anytype, who: Player, options: anytype) !void {
|
|
1696
1728
|
const side = battle.side(who);
|
|
1697
1729
|
if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
|
|
1698
1730
|
try Effects.boost(battle, who, Move.get(.Rage), options);
|
|
1699
1731
|
}
|
|
1700
1732
|
}
|
|
1701
1733
|
|
|
1702
|
-
fn
|
|
1703
|
-
var volatiles = &active.volatiles;
|
|
1704
|
-
assert(volatiles.Thrashing);
|
|
1705
|
-
if (volatiles.attacks > 0) return false;
|
|
1706
|
-
|
|
1707
|
-
volatiles.Thrashing = false;
|
|
1708
|
-
volatiles.Confusion = true;
|
|
1709
|
-
volatiles.confusion = Rolls.confusionDuration(battle, player, true, options);
|
|
1710
|
-
return true;
|
|
1711
|
-
}
|
|
1712
|
-
|
|
1713
|
-
inline fn onBegin(
|
|
1734
|
+
fn onBegin(
|
|
1714
1735
|
battle: anytype,
|
|
1715
1736
|
player: Player,
|
|
1716
1737
|
move: Move.Data,
|
|
@@ -1718,7 +1739,6 @@ inline fn onBegin(
|
|
|
1718
1739
|
residual: *bool,
|
|
1719
1740
|
options: anytype,
|
|
1720
1741
|
) !void {
|
|
1721
|
-
assert(move.effect.onBegin());
|
|
1722
1742
|
return switch (move.effect) {
|
|
1723
1743
|
.Confusion => Effects.confusion(battle, player, move, options),
|
|
1724
1744
|
.Conversion => Effects.conversion(battle, player, options),
|
|
@@ -1740,8 +1760,7 @@ inline fn onBegin(
|
|
|
1740
1760
|
};
|
|
1741
1761
|
}
|
|
1742
1762
|
|
|
1743
|
-
|
|
1744
|
-
assert(move.effect.onEnd());
|
|
1763
|
+
fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1745
1764
|
return switch (move.effect) {
|
|
1746
1765
|
.Bide => Effects.bide(battle, player, options),
|
|
1747
1766
|
// zig fmt: off
|
|
@@ -1756,14 +1775,13 @@ inline fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anyty
|
|
|
1756
1775
|
};
|
|
1757
1776
|
}
|
|
1758
1777
|
|
|
1759
|
-
|
|
1778
|
+
fn alwaysHappens(
|
|
1760
1779
|
battle: anytype,
|
|
1761
1780
|
player: Player,
|
|
1762
1781
|
move: Move.Data,
|
|
1763
1782
|
residual: *bool,
|
|
1764
1783
|
options: anytype,
|
|
1765
1784
|
) !void {
|
|
1766
|
-
assert(move.effect.alwaysHappens());
|
|
1767
1785
|
return switch (move.effect) {
|
|
1768
1786
|
.DrainHP, .DreamEater => Effects.drainHP(battle, player, options),
|
|
1769
1787
|
.Explode => Effects.explode(battle, player),
|
|
@@ -1775,7 +1793,7 @@ inline fn alwaysHappens(
|
|
|
1775
1793
|
};
|
|
1776
1794
|
}
|
|
1777
1795
|
|
|
1778
|
-
|
|
1796
|
+
fn moveEffect(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1779
1797
|
return switch (move.effect) {
|
|
1780
1798
|
.BurnChance1, .BurnChance2 => Effects.burnChance(battle, player, move, options),
|
|
1781
1799
|
.ConfusionChance => Effects.confusion(battle, player, move, options),
|
|
@@ -1799,10 +1817,12 @@ pub const Effects = struct {
|
|
|
1799
1817
|
|
|
1800
1818
|
side.active.volatiles.Bide = true;
|
|
1801
1819
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1820
|
+
|
|
1802
1821
|
side.active.volatiles.state = 0;
|
|
1803
1822
|
side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
1823
|
+
options.chance.observe(.attacking, player, .started);
|
|
1804
1824
|
|
|
1805
|
-
try options.log.start(battle.active(player), .Bide);
|
|
1825
|
+
try options.log.start(.{ battle.active(player), .Bide });
|
|
1806
1826
|
}
|
|
1807
1827
|
|
|
1808
1828
|
fn burnChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -1816,11 +1836,11 @@ pub const Effects = struct {
|
|
|
1816
1836
|
// GLITCH: Freeze top move selection desync can occur if thawed Pokémon is slower
|
|
1817
1837
|
if (Status.is(foe_stored.status, .FRZ)) {
|
|
1818
1838
|
assert(move.type == .Fire);
|
|
1819
|
-
try options.log.curestatus(
|
|
1839
|
+
try options.log.curestatus(.{
|
|
1820
1840
|
battle.active(player.foe()),
|
|
1821
1841
|
foe_stored.status,
|
|
1822
1842
|
.Message,
|
|
1823
|
-
);
|
|
1843
|
+
});
|
|
1824
1844
|
foe_stored.status = 0;
|
|
1825
1845
|
}
|
|
1826
1846
|
return;
|
|
@@ -1834,7 +1854,7 @@ pub const Effects = struct {
|
|
|
1834
1854
|
foe_stored.status = Status.init(.BRN);
|
|
1835
1855
|
foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
|
|
1836
1856
|
|
|
1837
|
-
try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
|
|
1857
|
+
try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
|
|
1838
1858
|
}
|
|
1839
1859
|
|
|
1840
1860
|
fn charge(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -1844,8 +1864,8 @@ pub const Effects = struct {
|
|
|
1844
1864
|
volatiles.Charging = true;
|
|
1845
1865
|
const move = side.last_selected_move;
|
|
1846
1866
|
if (move == .Fly or move == .Dig) volatiles.Invulnerable = true;
|
|
1847
|
-
try options.log.laststill();
|
|
1848
|
-
try options.log.prepare(battle.active(player), move);
|
|
1867
|
+
try options.log.laststill(.{});
|
|
1868
|
+
try options.log.prepare(.{ battle.active(player), move });
|
|
1849
1869
|
}
|
|
1850
1870
|
|
|
1851
1871
|
fn confusion(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -1856,42 +1876,39 @@ pub const Effects = struct {
|
|
|
1856
1876
|
if (!try Rolls.confusionChance(battle, player, options)) return;
|
|
1857
1877
|
} else {
|
|
1858
1878
|
if (showdown) {
|
|
1859
|
-
if (!try checkHit(battle, player, move, options))
|
|
1860
|
-
|
|
1861
|
-
} else if (sub) {
|
|
1862
|
-
return options.log.fail(battle.active(player.foe()), .None);
|
|
1863
|
-
}
|
|
1879
|
+
if (!try checkHit(battle, player, move, options)) return;
|
|
1880
|
+
if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
1864
1881
|
} else {
|
|
1865
|
-
if (sub) {
|
|
1866
|
-
|
|
1867
|
-
} else if (!try checkHit(battle, player, move, options)) {
|
|
1868
|
-
return;
|
|
1869
|
-
}
|
|
1882
|
+
if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
1883
|
+
if (!try checkHit(battle, player, move, options)) return;
|
|
1870
1884
|
}
|
|
1871
|
-
try options.chance.commit(player, .hit);
|
|
1872
1885
|
}
|
|
1873
1886
|
|
|
1874
1887
|
if (foe.active.volatiles.Confusion) return;
|
|
1875
1888
|
foe.active.volatiles.Confusion = true;
|
|
1876
|
-
foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
|
|
1877
1889
|
|
|
1878
|
-
|
|
1890
|
+
foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
1891
|
+
options.chance.observe(.confusion, player.foe(), .started);
|
|
1892
|
+
options.calc.confusion(player.foe());
|
|
1893
|
+
|
|
1894
|
+
try options.log.start(.{ battle.active(player.foe()), .Confusion });
|
|
1879
1895
|
}
|
|
1880
1896
|
|
|
1881
1897
|
fn conversion(battle: anytype, player: Player, options: anytype) !void {
|
|
1882
1898
|
const foe = battle.foe(player);
|
|
1883
1899
|
|
|
1884
1900
|
if (foe.active.volatiles.Invulnerable) {
|
|
1885
|
-
try options.log.lastmiss();
|
|
1886
|
-
return options.log.miss(battle.active(player));
|
|
1901
|
+
try options.log.lastmiss(.{});
|
|
1902
|
+
return options.log.miss(.{battle.active(player)});
|
|
1887
1903
|
}
|
|
1888
1904
|
|
|
1889
1905
|
battle.side(player).active.types = foe.active.types;
|
|
1890
|
-
return options.log.
|
|
1906
|
+
return options.log.start(.{
|
|
1891
1907
|
battle.active(player),
|
|
1908
|
+
.TypeChange,
|
|
1892
1909
|
foe.active.types,
|
|
1893
1910
|
battle.active(player.foe()),
|
|
1894
|
-
);
|
|
1911
|
+
});
|
|
1895
1912
|
}
|
|
1896
1913
|
|
|
1897
1914
|
fn disable(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
|
|
@@ -1900,13 +1917,10 @@ pub const Effects = struct {
|
|
|
1900
1917
|
const foe_ident = battle.active(player.foe());
|
|
1901
1918
|
|
|
1902
1919
|
// Pokémon Showdown handles hit/miss earlier in doMove
|
|
1903
|
-
if (!showdown)
|
|
1904
|
-
if (!try checkHit(battle, player, move, options)) return null;
|
|
1905
|
-
try options.chance.commit(player, .hit);
|
|
1906
|
-
}
|
|
1920
|
+
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
1907
1921
|
|
|
1908
|
-
if (volatiles.
|
|
1909
|
-
try options.log.fail(foe_ident, .None);
|
|
1922
|
+
if (volatiles.disable_move != 0) {
|
|
1923
|
+
try options.log.fail(.{ foe_ident, .None });
|
|
1910
1924
|
return null;
|
|
1911
1925
|
}
|
|
1912
1926
|
|
|
@@ -1921,27 +1935,27 @@ pub const Effects = struct {
|
|
|
1921
1935
|
|
|
1922
1936
|
// Technically this is still considered simply a "miss" on the cartridge,
|
|
1923
1937
|
// but diverging from Pokémon Showdown here would mostly just be pedantic
|
|
1924
|
-
if (n == 0) {
|
|
1925
|
-
try options.log.fail(foe_ident, .None);
|
|
1938
|
+
if (n == 0 or (pkmn.options.mod and err)) {
|
|
1939
|
+
try options.log.fail(.{ foe_ident, .None });
|
|
1926
1940
|
return null;
|
|
1927
1941
|
} else if (err) {
|
|
1928
1942
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
1929
|
-
assert(!showdown);
|
|
1930
1943
|
return Result.Error;
|
|
1931
1944
|
}
|
|
1932
1945
|
|
|
1933
|
-
volatiles.
|
|
1946
|
+
volatiles.disable_move =
|
|
1934
1947
|
@intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
|
|
1935
|
-
volatiles.
|
|
1948
|
+
volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
|
|
1949
|
+
options.chance.observe(.disable, player.foe(), .started);
|
|
1936
1950
|
|
|
1937
|
-
const id = foe.active.move(volatiles.
|
|
1938
|
-
try options.log.
|
|
1951
|
+
const id = foe.active.move(volatiles.disable_move).id;
|
|
1952
|
+
try options.log.start(.{ foe_ident, .Disable, id });
|
|
1939
1953
|
return null;
|
|
1940
1954
|
}
|
|
1941
1955
|
|
|
1942
1956
|
fn drainHP(battle: anytype, player: Player, options: anytype) !void {
|
|
1943
|
-
var
|
|
1944
|
-
|
|
1957
|
+
var stored = battle.side(player).stored();
|
|
1958
|
+
const ident = battle.active(player);
|
|
1945
1959
|
|
|
1946
1960
|
if (battle.last_damage == 0) {
|
|
1947
1961
|
assert(showdown);
|
|
@@ -1954,7 +1968,7 @@ pub const Effects = struct {
|
|
|
1954
1968
|
if (stored.hp == stored.stats.hp) return;
|
|
1955
1969
|
stored.hp = @min(stored.stats.hp, stored.hp + drain);
|
|
1956
1970
|
|
|
1957
|
-
try options.log.
|
|
1971
|
+
try options.log.heal(.{ ident, stored, .Drain, battle.active(player.foe()) });
|
|
1958
1972
|
}
|
|
1959
1973
|
|
|
1960
1974
|
fn explode(battle: anytype, player: Player) !void {
|
|
@@ -1986,7 +2000,7 @@ pub const Effects = struct {
|
|
|
1986
2000
|
if (side.active.volatiles.FocusEnergy) return;
|
|
1987
2001
|
side.active.volatiles.FocusEnergy = true;
|
|
1988
2002
|
|
|
1989
|
-
try options.log.start(battle.active(player), .FocusEnergy);
|
|
2003
|
+
try options.log.start(.{ battle.active(player), .FocusEnergy });
|
|
1990
2004
|
}
|
|
1991
2005
|
|
|
1992
2006
|
fn freezeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2003,12 +2017,12 @@ pub const Effects = struct {
|
|
|
2003
2017
|
if (foe.active.types.includes(move.type)) return;
|
|
2004
2018
|
if (!try Rolls.secondaryChance(battle, player, true, options)) return;
|
|
2005
2019
|
// Freeze Clause Mod
|
|
2006
|
-
if (
|
|
2020
|
+
if (pkmn.options.mod) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
2007
2021
|
|
|
2008
2022
|
foe_stored.status = Status.init(.FRZ);
|
|
2009
2023
|
// GLITCH: Hyper Beam recharging status is not cleared
|
|
2010
2024
|
|
|
2011
|
-
try options.log.status(foe_ident, foe_stored.status, .None);
|
|
2025
|
+
try options.log.status(.{ foe_ident, foe_stored.status, .None });
|
|
2012
2026
|
}
|
|
2013
2027
|
|
|
2014
2028
|
fn haze(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -2016,7 +2030,7 @@ pub const Effects = struct {
|
|
|
2016
2030
|
var side = battle.side(player);
|
|
2017
2031
|
var foe = battle.foe(player);
|
|
2018
2032
|
|
|
2019
|
-
|
|
2033
|
+
const side_stored = side.stored();
|
|
2020
2034
|
var foe_stored = foe.stored();
|
|
2021
2035
|
|
|
2022
2036
|
const player_ident = battle.active(player);
|
|
@@ -2028,8 +2042,8 @@ pub const Effects = struct {
|
|
|
2028
2042
|
side.active.stats = side_stored.stats;
|
|
2029
2043
|
foe.active.stats = foe_stored.stats;
|
|
2030
2044
|
|
|
2031
|
-
try log.activate(player_ident, .Haze);
|
|
2032
|
-
try log.clearallboost();
|
|
2045
|
+
try log.activate(.{ player_ident, .Haze });
|
|
2046
|
+
try log.clearallboost(.{});
|
|
2033
2047
|
|
|
2034
2048
|
// Pokémon Showdown clears P1 then P2 instead of status -> side -> foe
|
|
2035
2049
|
if (showdown) {
|
|
@@ -2037,24 +2051,26 @@ pub const Effects = struct {
|
|
|
2037
2051
|
const p: Player = @enumFromInt(i);
|
|
2038
2052
|
// Pokémon Showdown incorrectly does not prevent sleep/freeze from moving
|
|
2039
2053
|
if (p != player and Status.any(s.stored().status)) {
|
|
2040
|
-
try log.curestatus(foe_ident, foe_stored.status, .Silent);
|
|
2054
|
+
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
2041
2055
|
s.stored().status = 0;
|
|
2056
|
+
options.chance.observe(.sleep, p, .None);
|
|
2042
2057
|
} else if (showdown and s.stored().status == Status.TOX) {
|
|
2043
2058
|
s.stored().status = Status.init(.PSN);
|
|
2044
|
-
try log.status(battle.active(p), s.stored().status, .None);
|
|
2059
|
+
try log.status(.{ battle.active(p), s.stored().status, .None });
|
|
2045
2060
|
}
|
|
2046
|
-
try clearVolatiles(battle, p,
|
|
2061
|
+
try clearVolatiles(battle, p, options);
|
|
2047
2062
|
}
|
|
2048
2063
|
} else {
|
|
2049
2064
|
if (Status.any(foe_stored.status)) {
|
|
2050
2065
|
if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
|
|
2051
2066
|
foe.last_selected_move = .SKIP_TURN;
|
|
2067
|
+
options.chance.observe(.sleep, player.foe(), .None);
|
|
2052
2068
|
}
|
|
2053
|
-
try log.curestatus(foe_ident, foe_stored.status, .Silent);
|
|
2069
|
+
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
2054
2070
|
foe_stored.status = 0;
|
|
2055
2071
|
}
|
|
2056
|
-
try clearVolatiles(battle, player,
|
|
2057
|
-
try clearVolatiles(battle, player.foe(),
|
|
2072
|
+
try clearVolatiles(battle, player, options);
|
|
2073
|
+
try clearVolatiles(battle, player.foe(), options);
|
|
2058
2074
|
}
|
|
2059
2075
|
}
|
|
2060
2076
|
|
|
@@ -2066,7 +2082,7 @@ pub const Effects = struct {
|
|
|
2066
2082
|
// GLITCH: HP recovery move failure glitches
|
|
2067
2083
|
const delta = stored.stats.hp - stored.hp;
|
|
2068
2084
|
if (delta == 0 or (delta & 255 == 255 and stored.hp % 256 != 0)) {
|
|
2069
|
-
return
|
|
2085
|
+
return options.log.fail(.{ ident, .None });
|
|
2070
2086
|
}
|
|
2071
2087
|
|
|
2072
2088
|
const rest = side.last_selected_move == .Rest;
|
|
@@ -2074,17 +2090,17 @@ pub const Effects = struct {
|
|
|
2074
2090
|
// Adding the sleep status runs the sleep condition handler to roll duration
|
|
2075
2091
|
if (showdown) battle.rng.advance(1);
|
|
2076
2092
|
stored.status = Status.slf(2);
|
|
2077
|
-
try options.log.
|
|
2093
|
+
try options.log.status(.{ ident, stored.status, .From, Move.Rest });
|
|
2078
2094
|
stored.hp = stored.stats.hp;
|
|
2079
2095
|
} else {
|
|
2080
2096
|
stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
|
|
2081
2097
|
}
|
|
2082
|
-
try options.log.heal(ident, stored, if (rest) Heal.Silent else Heal.None);
|
|
2098
|
+
try options.log.heal(.{ ident, stored, if (rest) Heal.Silent else Heal.None });
|
|
2083
2099
|
}
|
|
2084
2100
|
|
|
2085
2101
|
fn hyperBeam(battle: anytype, player: Player, options: anytype) !void {
|
|
2086
2102
|
battle.side(player).active.volatiles.Recharging = true;
|
|
2087
|
-
try options.log.mustrecharge(battle.active(player));
|
|
2103
|
+
try options.log.mustrecharge(.{battle.active(player)});
|
|
2088
2104
|
}
|
|
2089
2105
|
|
|
2090
2106
|
fn leechSeed(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2093,33 +2109,31 @@ pub const Effects = struct {
|
|
|
2093
2109
|
if (showdown) {
|
|
2094
2110
|
// Invulnerability trumps type immunity on Pokémon Showdown
|
|
2095
2111
|
if (!foe.active.volatiles.Invulnerable and foe.active.types.includes(.Grass)) {
|
|
2096
|
-
return options.log.immune(battle.active(player.foe()), .None);
|
|
2112
|
+
return options.log.immune(.{ battle.active(player.foe()), .None });
|
|
2097
2113
|
}
|
|
2098
2114
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2099
2115
|
if (foe.active.volatiles.LeechSeed) return;
|
|
2100
2116
|
} else {
|
|
2101
2117
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2102
2118
|
if (foe.active.types.includes(.Grass) or foe.active.volatiles.LeechSeed) {
|
|
2103
|
-
try options.log.lastmiss();
|
|
2104
|
-
return options.log.miss(battle.active(player));
|
|
2119
|
+
try options.log.lastmiss(.{});
|
|
2120
|
+
return options.log.miss(.{battle.active(player)});
|
|
2105
2121
|
}
|
|
2106
2122
|
}
|
|
2107
2123
|
|
|
2108
|
-
try options.chance.commit(player, .hit);
|
|
2109
2124
|
foe.active.volatiles.LeechSeed = true;
|
|
2110
2125
|
|
|
2111
|
-
try options.log.start(battle.active(player.foe()), .LeechSeed);
|
|
2126
|
+
try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
|
|
2112
2127
|
}
|
|
2113
2128
|
|
|
2114
2129
|
fn lightScreen(battle: anytype, player: Player, options: anytype) !void {
|
|
2115
2130
|
var side = battle.side(player);
|
|
2131
|
+
const ident = battle.active(player);
|
|
2116
2132
|
|
|
2117
|
-
if (side.active.volatiles.LightScreen) {
|
|
2118
|
-
return options.log.fail(battle.active(player), .None);
|
|
2119
|
-
}
|
|
2133
|
+
if (side.active.volatiles.LightScreen) return options.log.fail(.{ ident, .None });
|
|
2120
2134
|
side.active.volatiles.LightScreen = true;
|
|
2121
2135
|
|
|
2122
|
-
try options.log.start(
|
|
2136
|
+
try options.log.start(.{ ident, .LightScreen });
|
|
2123
2137
|
}
|
|
2124
2138
|
|
|
2125
2139
|
fn mimic(battle: anytype, player: Player, move: Move.Data, mslot: u8, options: anytype) !void {
|
|
@@ -2147,17 +2161,15 @@ pub const Effects = struct {
|
|
|
2147
2161
|
if (!has_mimic) {
|
|
2148
2162
|
// Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
|
|
2149
2163
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2150
|
-
|
|
2151
|
-
return try options.log.fail(battle.active(player.foe()), .None);
|
|
2164
|
+
return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
2152
2165
|
}
|
|
2153
2166
|
}
|
|
2154
2167
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2155
|
-
try options.chance.commit(player, .hit);
|
|
2156
2168
|
|
|
2157
2169
|
const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
|
|
2158
2170
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
2159
2171
|
|
|
2160
|
-
try options.log.
|
|
2172
|
+
try options.log.start(.{ battle.active(player), .Mimic, side.active.move(oslot).id });
|
|
2161
2173
|
}
|
|
2162
2174
|
|
|
2163
2175
|
fn mist(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -2166,7 +2178,7 @@ pub const Effects = struct {
|
|
|
2166
2178
|
if (side.active.volatiles.Mist) return;
|
|
2167
2179
|
side.active.volatiles.Mist = true;
|
|
2168
2180
|
|
|
2169
|
-
try options.log.start(battle.active(player), .Mist);
|
|
2181
|
+
try options.log.start(.{ battle.active(player), .Mist });
|
|
2170
2182
|
}
|
|
2171
2183
|
|
|
2172
2184
|
fn multiHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2192,25 +2204,25 @@ pub const Effects = struct {
|
|
|
2192
2204
|
|
|
2193
2205
|
if (showdown) {
|
|
2194
2206
|
// Invulnerability trumps type immunity on Pokémon Showdown
|
|
2195
|
-
if (immune and !foe.active.volatiles.Invulnerable)
|
|
2196
|
-
|
|
2207
|
+
if (immune and !foe.active.volatiles.Invulnerable) {
|
|
2208
|
+
return log.immune(.{ foe_ident, .None });
|
|
2209
|
+
} else if (!try checkHit(battle, player, move, options)) return;
|
|
2197
2210
|
}
|
|
2198
2211
|
if (Status.any(foe_stored.status)) {
|
|
2199
|
-
return log.fail(
|
|
2212
|
+
return log.fail(.{
|
|
2200
2213
|
foe_ident,
|
|
2201
|
-
if (Status.is(foe_stored.status, .PAR)) .Paralysis else .None,
|
|
2202
|
-
);
|
|
2214
|
+
if (Status.is(foe_stored.status, .PAR)) Fail.Paralysis else Fail.None,
|
|
2215
|
+
});
|
|
2203
2216
|
}
|
|
2204
2217
|
if (!showdown) {
|
|
2205
|
-
if (immune) return log.immune(foe_ident, .None);
|
|
2218
|
+
if (immune) return log.immune(.{ foe_ident, .None });
|
|
2206
2219
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2207
2220
|
}
|
|
2208
|
-
try options.chance.commit(player, .hit);
|
|
2209
2221
|
|
|
2210
2222
|
foe_stored.status = Status.init(.PAR);
|
|
2211
2223
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
2212
2224
|
|
|
2213
|
-
try log.status(foe_ident, foe_stored.status, .None);
|
|
2225
|
+
try log.status(.{ foe_ident, foe_stored.status, .None });
|
|
2214
2226
|
}
|
|
2215
2227
|
|
|
2216
2228
|
fn paralyzeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2232,12 +2244,12 @@ pub const Effects = struct {
|
|
|
2232
2244
|
foe_stored.status = Status.init(.PAR);
|
|
2233
2245
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
2234
2246
|
|
|
2235
|
-
try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
|
|
2247
|
+
try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
|
|
2236
2248
|
}
|
|
2237
2249
|
|
|
2238
2250
|
fn payDay(battle: anytype, player: Player, options: anytype) !void {
|
|
2239
2251
|
if (!showdown or !battle.foe(player).active.volatiles.Substitute) {
|
|
2240
|
-
try options.log.fieldactivate();
|
|
2252
|
+
try options.log.fieldactivate(.{});
|
|
2241
2253
|
}
|
|
2242
2254
|
}
|
|
2243
2255
|
|
|
@@ -2252,28 +2264,27 @@ pub const Effects = struct {
|
|
|
2252
2264
|
return;
|
|
2253
2265
|
} else if (foe.active.volatiles.Substitute) {
|
|
2254
2266
|
if (move.effect != .Poison) return;
|
|
2255
|
-
return log.fail(foe_ident, .None);
|
|
2267
|
+
return log.fail(.{ foe_ident, .None });
|
|
2256
2268
|
} else if (Status.any(foe_stored.status)) {
|
|
2257
2269
|
if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
|
|
2258
2270
|
// Pokémon Showdown considers Toxic to be a status even in Generation I and so
|
|
2259
2271
|
// will not include a fail reason for Toxic vs. Poison or vice-versa...
|
|
2260
|
-
return log.fail(foe_ident, if (Status.is(foe_stored.status, .PSN))
|
|
2272
|
+
return log.fail(.{ foe_ident, if (Status.is(foe_stored.status, .PSN))
|
|
2261
2273
|
if (!showdown)
|
|
2262
|
-
.Poison
|
|
2274
|
+
Fail.Poison
|
|
2263
2275
|
else if (toxic == (foe_stored.status == Status.TOX))
|
|
2264
|
-
if (toxic) .Toxic else .Poison
|
|
2276
|
+
if (toxic) Fail.Toxic else Fail.Poison
|
|
2265
2277
|
else
|
|
2266
|
-
.None
|
|
2278
|
+
Fail.None
|
|
2267
2279
|
else
|
|
2268
|
-
.None);
|
|
2280
|
+
Fail.None });
|
|
2269
2281
|
} else if (foe.active.types.includes(.Poison)) {
|
|
2270
2282
|
if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
|
|
2271
|
-
return log.immune(foe_ident, .None);
|
|
2283
|
+
return log.immune(.{ foe_ident, .None });
|
|
2272
2284
|
}
|
|
2273
2285
|
|
|
2274
2286
|
if (move.effect == .Poison) {
|
|
2275
2287
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2276
|
-
try options.chance.commit(player, .hit);
|
|
2277
2288
|
} else {
|
|
2278
2289
|
if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
|
|
2279
2290
|
return;
|
|
@@ -2287,7 +2298,7 @@ pub const Effects = struct {
|
|
|
2287
2298
|
foe.active.volatiles.toxic = 0;
|
|
2288
2299
|
}
|
|
2289
2300
|
|
|
2290
|
-
try log.status(foe_ident, foe_stored.status, .None);
|
|
2301
|
+
try log.status(.{ foe_ident, foe_stored.status, .None });
|
|
2291
2302
|
}
|
|
2292
2303
|
|
|
2293
2304
|
fn rage(battle: anytype, player: Player) !void {
|
|
@@ -2307,22 +2318,23 @@ pub const Effects = struct {
|
|
|
2307
2318
|
@as(u8, if (side.last_selected_move == .Struggle) 2 else 4), 1));
|
|
2308
2319
|
stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
|
|
2309
2320
|
|
|
2310
|
-
try options.log.
|
|
2321
|
+
try options.log.damage(.{
|
|
2311
2322
|
battle.active(player),
|
|
2312
2323
|
stored,
|
|
2313
2324
|
.RecoilOf,
|
|
2314
2325
|
battle.active(player.foe()),
|
|
2315
|
-
);
|
|
2326
|
+
});
|
|
2316
2327
|
if (showdown and stored.hp == 0) residual.* = false;
|
|
2317
2328
|
}
|
|
2318
2329
|
|
|
2319
2330
|
fn reflect(battle: anytype, player: Player, options: anytype) !void {
|
|
2320
2331
|
var side = battle.side(player);
|
|
2332
|
+
const ident = battle.active(player);
|
|
2321
2333
|
|
|
2322
|
-
if (side.active.volatiles.Reflect) return options.log.fail(
|
|
2334
|
+
if (side.active.volatiles.Reflect) return options.log.fail(.{ ident, .None });
|
|
2323
2335
|
side.active.volatiles.Reflect = true;
|
|
2324
2336
|
|
|
2325
|
-
try options.log.start(
|
|
2337
|
+
try options.log.start(.{ ident, .Reflect });
|
|
2326
2338
|
}
|
|
2327
2339
|
|
|
2328
2340
|
fn sleep(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2336,18 +2348,17 @@ pub const Effects = struct {
|
|
|
2336
2348
|
// the Sleep Clause Mod might activate, causing us to not actually bypass
|
|
2337
2349
|
} else {
|
|
2338
2350
|
if (Status.any(foe_stored.status)) {
|
|
2339
|
-
return options.log.fail(
|
|
2351
|
+
return options.log.fail(.{
|
|
2340
2352
|
foe_ident,
|
|
2341
|
-
if (Status.is(foe_stored.status, .SLP)) .Sleep else .None,
|
|
2342
|
-
);
|
|
2353
|
+
if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
|
|
2354
|
+
});
|
|
2343
2355
|
}
|
|
2344
|
-
// If
|
|
2356
|
+
// If moveHit in doMove didn't return true Pokémon Showdown wouldn't be in here
|
|
2345
2357
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2346
|
-
try options.chance.commit(player, .hit);
|
|
2347
2358
|
}
|
|
2348
2359
|
|
|
2349
2360
|
// Sleep Clause Mod
|
|
2350
|
-
if (
|
|
2361
|
+
if (pkmn.options.mod) {
|
|
2351
2362
|
for (foe.pokemon) |p| {
|
|
2352
2363
|
if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
|
|
2353
2364
|
}
|
|
@@ -2355,19 +2366,20 @@ pub const Effects = struct {
|
|
|
2355
2366
|
foe.active.volatiles.Recharging = false;
|
|
2356
2367
|
|
|
2357
2368
|
foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
|
|
2369
|
+
options.chance.observe(.sleep, player.foe(), .started);
|
|
2370
|
+
|
|
2358
2371
|
const last = battle.side(player).last_selected_move;
|
|
2359
|
-
try options.log.
|
|
2372
|
+
try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
|
|
2360
2373
|
}
|
|
2361
2374
|
|
|
2362
2375
|
fn splash(battle: anytype, player: Player, options: anytype) !void {
|
|
2363
|
-
try options.log.activate(battle.active(player), .Splash);
|
|
2376
|
+
try options.log.activate(.{ battle.active(player), .Splash });
|
|
2364
2377
|
}
|
|
2365
2378
|
|
|
2366
2379
|
fn substitute(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
|
|
2367
2380
|
var side = battle.side(player);
|
|
2368
2381
|
if (side.active.volatiles.Substitute) {
|
|
2369
|
-
|
|
2370
|
-
return;
|
|
2382
|
+
return options.log.fail(.{ battle.active(player), .Substitute });
|
|
2371
2383
|
}
|
|
2372
2384
|
|
|
2373
2385
|
assert(side.stored().stats.hp <= 1023);
|
|
@@ -2379,28 +2391,30 @@ pub const Effects = struct {
|
|
|
2379
2391
|
// avoid having to convert to floating point)
|
|
2380
2392
|
const required_hp = if (showdown) @divFloor(side.stored().stats.hp - 1, 4) + 1 else hp;
|
|
2381
2393
|
if (side.stored().hp < required_hp) {
|
|
2382
|
-
|
|
2383
|
-
return;
|
|
2394
|
+
return options.log.fail(.{ battle.active(player), .Weak });
|
|
2384
2395
|
}
|
|
2385
2396
|
|
|
2386
2397
|
// GLITCH: can leave the user with 0 HP (faints later) because didn't check '<=' above
|
|
2387
2398
|
side.stored().hp -= hp;
|
|
2388
2399
|
side.active.volatiles.substitute = hp + 1;
|
|
2389
2400
|
side.active.volatiles.Substitute = true;
|
|
2390
|
-
try options.log.start(battle.active(player), .Substitute);
|
|
2401
|
+
try options.log.start(.{ battle.active(player), .Substitute });
|
|
2391
2402
|
if (hp > 0) {
|
|
2392
|
-
try options.log.damage(battle.active(player), side.stored(), .None);
|
|
2403
|
+
try options.log.damage(.{ battle.active(player), side.stored(), .None });
|
|
2393
2404
|
if (showdown and side.stored().hp == 0) residual.* = false;
|
|
2394
2405
|
}
|
|
2395
2406
|
}
|
|
2396
2407
|
|
|
2397
2408
|
fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
2398
|
-
if (!showdown or battle.side(player).last_selected_move == .Teleport) return;
|
|
2399
|
-
|
|
2400
2409
|
// Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
|
|
2401
|
-
if (
|
|
2402
|
-
|
|
2403
|
-
|
|
2410
|
+
if (showdown) {
|
|
2411
|
+
if (battle.side(player).last_selected_move == .Teleport) return;
|
|
2412
|
+
_ = try checkHit(battle, player, move, options);
|
|
2413
|
+
battle.last_damage = 0;
|
|
2414
|
+
} else {
|
|
2415
|
+
try options.log.fail(.{ battle.active(player), .None });
|
|
2416
|
+
try options.log.laststill(.{});
|
|
2417
|
+
}
|
|
2404
2418
|
}
|
|
2405
2419
|
|
|
2406
2420
|
fn thrashing(battle: anytype, player: Player, options: anytype) void {
|
|
@@ -2410,6 +2424,8 @@ pub const Effects = struct {
|
|
|
2410
2424
|
|
|
2411
2425
|
volatiles.Thrashing = true;
|
|
2412
2426
|
volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
2427
|
+
|
|
2428
|
+
options.chance.observe(.attacking, player, .started);
|
|
2413
2429
|
}
|
|
2414
2430
|
|
|
2415
2431
|
fn transform(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -2438,7 +2454,7 @@ pub const Effects = struct {
|
|
|
2438
2454
|
side.active.moves[i].pp = if (m.id != .None) 5 else 0;
|
|
2439
2455
|
}
|
|
2440
2456
|
|
|
2441
|
-
try options.log.transform(battle.active(player), foe_ident);
|
|
2457
|
+
try options.log.transform(.{ battle.active(player), foe_ident });
|
|
2442
2458
|
}
|
|
2443
2459
|
|
|
2444
2460
|
fn binding(battle: anytype, player: Player, rewrap: bool, options: anytype) !void {
|
|
@@ -2448,13 +2464,14 @@ pub const Effects = struct {
|
|
|
2448
2464
|
if (side.active.volatiles.Binding) return;
|
|
2449
2465
|
side.active.volatiles.Binding = true;
|
|
2450
2466
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2451
|
-
// (Pokémon Showdown
|
|
2467
|
+
// (Pokémon Showdown clears Recharging in onTryMove instead)
|
|
2452
2468
|
if (!showdown) {
|
|
2453
2469
|
foe.active.volatiles.Recharging = false;
|
|
2454
2470
|
} else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
|
|
2455
2471
|
|
|
2456
2472
|
side.active.volatiles.attacks =
|
|
2457
2473
|
try Rolls.distribution(battle, .Binding, player, options) - 1;
|
|
2474
|
+
options.chance.observe(.binding, player, .started);
|
|
2458
2475
|
}
|
|
2459
2476
|
|
|
2460
2477
|
fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2468,87 +2485,87 @@ pub const Effects = struct {
|
|
|
2468
2485
|
switch (move.effect) {
|
|
2469
2486
|
.AttackUp1, .AttackUp2, .Rage => {
|
|
2470
2487
|
assert(boosts.atk >= -6 and boosts.atk <= 6);
|
|
2471
|
-
if (boosts.atk == 6) return
|
|
2488
|
+
if (boosts.atk == 6) return log.fail(.{ ident, .None });
|
|
2472
2489
|
const n: u2 = if (move.effect == .AttackUp2) 2 else 1;
|
|
2473
2490
|
boosts.atk = @as(i4, @intCast(@min(6, @as(i8, boosts.atk) + n)));
|
|
2474
|
-
const reason = if (move.effect == .Rage)
|
|
2491
|
+
const reason: Boost = if (move.effect == .Rage) .Rage else .Attack;
|
|
2475
2492
|
if (stats.atk == MAX_STAT_VALUE) {
|
|
2476
2493
|
boosts.atk -= 1;
|
|
2477
2494
|
if (showdown) {
|
|
2478
|
-
try log.boost(ident, reason, n);
|
|
2479
|
-
try log.boost(ident,
|
|
2495
|
+
try log.boost(.{ ident, reason, n });
|
|
2496
|
+
try log.boost(.{ ident, .Attack, -1 });
|
|
2480
2497
|
if (move.effect == .Rage) return;
|
|
2481
2498
|
}
|
|
2482
|
-
return
|
|
2499
|
+
return log.fail(.{ ident, .None });
|
|
2483
2500
|
}
|
|
2484
|
-
|
|
2501
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
|
|
2485
2502
|
const stat = unmodifiedStats(battle, side).atk;
|
|
2486
2503
|
stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2487
|
-
try log.boost(ident, reason, n);
|
|
2504
|
+
try log.boost(.{ ident, reason, n });
|
|
2488
2505
|
// Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
|
|
2489
2506
|
if (showdown and move.effect == .Rage) return;
|
|
2490
2507
|
},
|
|
2491
2508
|
.DefenseUp1, .DefenseUp2 => {
|
|
2492
2509
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
2493
|
-
if (boosts.def == 6) return
|
|
2510
|
+
if (boosts.def == 6) return log.fail(.{ ident, .None });
|
|
2494
2511
|
const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
|
|
2495
2512
|
boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
|
|
2496
2513
|
if (stats.def == MAX_STAT_VALUE) {
|
|
2497
2514
|
boosts.def -= 1;
|
|
2498
2515
|
if (showdown) {
|
|
2499
|
-
try log.boost(ident, .Defense, n);
|
|
2500
|
-
try log.boost(ident, .Defense, -1);
|
|
2516
|
+
try log.boost(.{ ident, .Defense, n });
|
|
2517
|
+
try log.boost(.{ ident, .Defense, -1 });
|
|
2501
2518
|
}
|
|
2502
|
-
return
|
|
2519
|
+
return log.fail(.{ ident, .None });
|
|
2503
2520
|
}
|
|
2504
|
-
|
|
2521
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
|
|
2505
2522
|
const stat = unmodifiedStats(battle, side).def;
|
|
2506
2523
|
stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2507
|
-
try log.boost(ident, .Defense, n);
|
|
2524
|
+
try log.boost(.{ ident, .Defense, n });
|
|
2508
2525
|
},
|
|
2509
2526
|
.SpeedUp2 => {
|
|
2510
2527
|
assert(boosts.spe >= -6 and boosts.spe <= 6);
|
|
2511
|
-
if (boosts.spe == 6) return
|
|
2528
|
+
if (boosts.spe == 6) return log.fail(.{ ident, .None });
|
|
2512
2529
|
boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
|
|
2513
2530
|
if (stats.spe == MAX_STAT_VALUE) {
|
|
2514
2531
|
boosts.spe -= 1;
|
|
2515
2532
|
if (showdown) {
|
|
2516
|
-
try log.boost(ident, .Speed, 2);
|
|
2517
|
-
try log.boost(ident, .Speed, -1);
|
|
2533
|
+
try log.boost(.{ ident, .Speed, 2 });
|
|
2534
|
+
try log.boost(.{ ident, .Speed, -1 });
|
|
2518
2535
|
}
|
|
2519
|
-
return
|
|
2536
|
+
return log.fail(.{ ident, .None });
|
|
2520
2537
|
}
|
|
2521
|
-
|
|
2538
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
|
|
2522
2539
|
const stat = unmodifiedStats(battle, side).spe;
|
|
2523
2540
|
stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2524
|
-
try log.boost(ident, .Speed, 2);
|
|
2541
|
+
try log.boost(.{ ident, .Speed, 2 });
|
|
2525
2542
|
},
|
|
2526
2543
|
.SpecialUp1, .SpecialUp2 => {
|
|
2527
2544
|
assert(boosts.spc >= -6 and boosts.spc <= 6);
|
|
2528
|
-
if (boosts.spc == 6) return
|
|
2545
|
+
if (boosts.spc == 6) return log.fail(.{ ident, .None });
|
|
2529
2546
|
const n: u2 = if (move.effect == .SpecialUp2) 2 else 1;
|
|
2530
2547
|
boosts.spc = @intCast(@min(6, @as(i8, boosts.spc) + n));
|
|
2531
2548
|
if (stats.spc == MAX_STAT_VALUE) {
|
|
2532
2549
|
boosts.spc -= 1;
|
|
2533
2550
|
if (showdown) {
|
|
2534
|
-
try log.boost(ident, .SpecialAttack, n);
|
|
2535
|
-
try log.boost(ident, .SpecialAttack, -1);
|
|
2536
|
-
try log.boost(ident, .SpecialDefense, n);
|
|
2537
|
-
try log.boost(ident, .SpecialDefense, -1);
|
|
2551
|
+
try log.boost(.{ ident, .SpecialAttack, n });
|
|
2552
|
+
try log.boost(.{ ident, .SpecialAttack, -1 });
|
|
2553
|
+
try log.boost(.{ ident, .SpecialDefense, n });
|
|
2554
|
+
try log.boost(.{ ident, .SpecialDefense, -1 });
|
|
2538
2555
|
}
|
|
2539
|
-
return
|
|
2556
|
+
return log.fail(.{ ident, .None });
|
|
2540
2557
|
}
|
|
2541
|
-
|
|
2558
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
|
|
2542
2559
|
const stat = unmodifiedStats(battle, side).spc;
|
|
2543
2560
|
stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2544
|
-
try log.boost(ident, .SpecialAttack, n);
|
|
2545
|
-
try log.boost(ident, .SpecialDefense, n);
|
|
2561
|
+
try log.boost(.{ ident, .SpecialAttack, n });
|
|
2562
|
+
try log.boost(.{ ident, .SpecialDefense, n });
|
|
2546
2563
|
},
|
|
2547
2564
|
.EvasionUp1 => {
|
|
2548
2565
|
assert(boosts.evasion >= -6 and boosts.evasion <= 6);
|
|
2549
|
-
if (boosts.evasion == 6) return
|
|
2566
|
+
if (boosts.evasion == 6) return log.fail(.{ ident, .None });
|
|
2550
2567
|
boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
|
|
2551
|
-
try log.boost(ident, .Evasion, 1);
|
|
2568
|
+
try log.boost(.{ ident, .Evasion, 1 });
|
|
2552
2569
|
},
|
|
2553
2570
|
else => unreachable,
|
|
2554
2571
|
}
|
|
@@ -2563,7 +2580,7 @@ pub const Effects = struct {
|
|
|
2563
2580
|
const foe_ident = battle.active(player.foe());
|
|
2564
2581
|
const secondary = move.effect.isStatDownChance();
|
|
2565
2582
|
|
|
2566
|
-
if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(foe_ident, .None);
|
|
2583
|
+
if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(.{ foe_ident, .None });
|
|
2567
2584
|
|
|
2568
2585
|
if (secondary) {
|
|
2569
2586
|
const proc = try Rolls.unboost(battle, player, options);
|
|
@@ -2571,7 +2588,6 @@ pub const Effects = struct {
|
|
|
2571
2588
|
} else {
|
|
2572
2589
|
// checkHit already checks for Invulnerable
|
|
2573
2590
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2574
|
-
try options.chance.commit(player, .hit);
|
|
2575
2591
|
}
|
|
2576
2592
|
|
|
2577
2593
|
var stats = &foe.active.stats;
|
|
@@ -2581,82 +2597,82 @@ pub const Effects = struct {
|
|
|
2581
2597
|
switch (move.effect) {
|
|
2582
2598
|
.AttackDown1, .AttackDownChance => {
|
|
2583
2599
|
assert(boosts.atk >= -6 and boosts.atk <= 6);
|
|
2584
|
-
if (boosts.atk == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2600
|
+
if (boosts.atk == -6) return if (fail) try log.fail(.{ foe_ident, .None });
|
|
2585
2601
|
boosts.atk = @intCast(@max(-6, @as(i8, boosts.atk) - 1));
|
|
2586
2602
|
if (stats.atk == 1) {
|
|
2587
2603
|
boosts.atk += 1;
|
|
2588
2604
|
if (showdown) {
|
|
2589
|
-
try log.boost(foe_ident, .Attack, -1);
|
|
2590
|
-
try log.boost(foe_ident, .Attack, 1);
|
|
2605
|
+
try log.boost(.{ foe_ident, .Attack, -1 });
|
|
2606
|
+
try log.boost(.{ foe_ident, .Attack, 1 });
|
|
2591
2607
|
}
|
|
2592
|
-
return
|
|
2608
|
+
return log.fail(.{ foe_ident, .None });
|
|
2593
2609
|
}
|
|
2594
|
-
|
|
2610
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
|
|
2595
2611
|
const stat = unmodifiedStats(battle, foe).atk;
|
|
2596
2612
|
stats.atk = @max(1, stat * mod[0] / mod[1]);
|
|
2597
|
-
try log.boost(foe_ident, .Attack, -1);
|
|
2613
|
+
try log.boost(.{ foe_ident, .Attack, -1 });
|
|
2598
2614
|
},
|
|
2599
2615
|
.DefenseDown1, .DefenseDown2, .DefenseDownChance => {
|
|
2600
2616
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
2601
|
-
if (boosts.def == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2617
|
+
if (boosts.def == -6) return if (fail) try log.fail(.{ foe_ident, .None });
|
|
2602
2618
|
const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
|
|
2603
2619
|
boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
|
|
2604
2620
|
if (stats.def == 1) {
|
|
2605
2621
|
boosts.def += 1;
|
|
2606
2622
|
if (showdown) {
|
|
2607
|
-
try log.boost(foe_ident, .Defense, -@as(i8, n));
|
|
2608
|
-
try log.boost(foe_ident, .Defense, 1);
|
|
2623
|
+
try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
|
|
2624
|
+
try log.boost(.{ foe_ident, .Defense, 1 });
|
|
2609
2625
|
}
|
|
2610
|
-
return
|
|
2626
|
+
return log.fail(.{ foe_ident, .None });
|
|
2611
2627
|
}
|
|
2612
|
-
|
|
2628
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
|
|
2613
2629
|
const stat = unmodifiedStats(battle, foe).def;
|
|
2614
2630
|
stats.def = @max(1, stat * mod[0] / mod[1]);
|
|
2615
|
-
try log.boost(foe_ident, .Defense, -@as(i8, n));
|
|
2631
|
+
try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
|
|
2616
2632
|
},
|
|
2617
2633
|
.SpeedDown1, .SpeedDownChance => {
|
|
2618
2634
|
assert(boosts.spe >= -6 and boosts.spe <= 6);
|
|
2619
|
-
if (boosts.spe == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2635
|
+
if (boosts.spe == -6) return if (fail) try log.fail(.{ foe_ident, .None });
|
|
2620
2636
|
boosts.spe = @intCast(@max(-6, @as(i8, boosts.spe) - 1));
|
|
2621
2637
|
if (stats.spe == 1) {
|
|
2622
2638
|
boosts.spe += 1;
|
|
2623
2639
|
if (showdown) {
|
|
2624
|
-
try log.boost(foe_ident, .Speed, -1);
|
|
2625
|
-
try log.boost(foe_ident, .Speed, 1);
|
|
2640
|
+
try log.boost(.{ foe_ident, .Speed, -1 });
|
|
2641
|
+
try log.boost(.{ foe_ident, .Speed, 1 });
|
|
2626
2642
|
}
|
|
2627
|
-
return
|
|
2643
|
+
return log.fail(.{ foe_ident, .None });
|
|
2628
2644
|
}
|
|
2629
|
-
|
|
2645
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
|
|
2630
2646
|
const stat = unmodifiedStats(battle, foe).spe;
|
|
2631
2647
|
stats.spe = @max(1, stat * mod[0] / mod[1]);
|
|
2632
|
-
try log.boost(foe_ident, .Speed, -1);
|
|
2648
|
+
try log.boost(.{ foe_ident, .Speed, -1 });
|
|
2633
2649
|
assert(boosts.spe >= -6);
|
|
2634
2650
|
},
|
|
2635
2651
|
.SpecialDownChance => {
|
|
2636
2652
|
assert(boosts.spc >= -6 and boosts.spc <= 6);
|
|
2637
|
-
if (boosts.spc == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2653
|
+
if (boosts.spc == -6) return if (fail) try log.fail(.{ foe_ident, .None });
|
|
2638
2654
|
boosts.spc = @intCast(@max(-6, @as(i8, boosts.spc) - 1));
|
|
2639
2655
|
if (stats.spc == 1) {
|
|
2640
2656
|
boosts.spc += 1;
|
|
2641
2657
|
if (showdown) {
|
|
2642
|
-
try log.boost(foe_ident, .SpecialAttack, -1);
|
|
2643
|
-
try log.boost(foe_ident, .SpecialAttack, 1);
|
|
2644
|
-
try log.boost(foe_ident, .SpecialDefense, -1);
|
|
2645
|
-
try log.boost(foe_ident, .SpecialDefense, 1);
|
|
2658
|
+
try log.boost(.{ foe_ident, .SpecialAttack, -1 });
|
|
2659
|
+
try log.boost(.{ foe_ident, .SpecialAttack, 1 });
|
|
2660
|
+
try log.boost(.{ foe_ident, .SpecialDefense, -1 });
|
|
2661
|
+
try log.boost(.{ foe_ident, .SpecialDefense, 1 });
|
|
2646
2662
|
}
|
|
2647
|
-
return
|
|
2663
|
+
return log.fail(.{ foe_ident, .None });
|
|
2648
2664
|
}
|
|
2649
|
-
|
|
2665
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
|
|
2650
2666
|
const stat = unmodifiedStats(battle, foe).spc;
|
|
2651
2667
|
stats.spc = @max(1, stat * mod[0] / mod[1]);
|
|
2652
|
-
try log.boost(foe_ident, .SpecialAttack, -1);
|
|
2653
|
-
try log.boost(foe_ident, .SpecialDefense, -1);
|
|
2668
|
+
try log.boost(.{ foe_ident, .SpecialAttack, -1 });
|
|
2669
|
+
try log.boost(.{ foe_ident, .SpecialDefense, -1 });
|
|
2654
2670
|
},
|
|
2655
2671
|
.AccuracyDown1 => {
|
|
2656
2672
|
assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
|
|
2657
|
-
if (boosts.accuracy == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2673
|
+
if (boosts.accuracy == -6) return if (fail) try log.fail(.{ foe_ident, .None });
|
|
2658
2674
|
boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
|
|
2659
|
-
try log.boost(foe_ident, .Accuracy, -1);
|
|
2675
|
+
try log.boost(.{ foe_ident, .Accuracy, -1 });
|
|
2660
2676
|
},
|
|
2661
2677
|
else => unreachable,
|
|
2662
2678
|
}
|
|
@@ -2680,40 +2696,40 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
|
|
|
2680
2696
|
}
|
|
2681
2697
|
}
|
|
2682
2698
|
|
|
2683
|
-
|
|
2699
|
+
fn isForced(active: anytype) bool {
|
|
2684
2700
|
return active.volatiles.Recharging or active.volatiles.Rage or
|
|
2685
2701
|
active.volatiles.Thrashing or active.volatiles.Charging;
|
|
2686
2702
|
}
|
|
2687
2703
|
|
|
2688
|
-
fn clearVolatiles(battle: anytype, who: Player,
|
|
2704
|
+
fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
|
|
2689
2705
|
var log = options.log;
|
|
2690
2706
|
var side = battle.side(who);
|
|
2691
2707
|
var volatiles = &side.active.volatiles;
|
|
2692
2708
|
const ident = battle.active(who);
|
|
2693
2709
|
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
try log.end(ident, .DisableSilent);
|
|
2710
|
+
if (volatiles.disable_move != 0) {
|
|
2711
|
+
volatiles.disable_move = 0;
|
|
2712
|
+
volatiles.disable_duration = 0;
|
|
2713
|
+
options.chance.observe(.disable, who, .None);
|
|
2714
|
+
try log.end(.{ ident, .DisableSilent });
|
|
2700
2715
|
}
|
|
2701
2716
|
if (volatiles.Confusion) {
|
|
2702
2717
|
// volatiles.confusion is left unchanged
|
|
2703
2718
|
volatiles.Confusion = false;
|
|
2704
|
-
|
|
2719
|
+
options.chance.observe(.confusion, who, .None);
|
|
2720
|
+
try log.end(.{ ident, .ConfusionSilent });
|
|
2705
2721
|
}
|
|
2706
2722
|
if (volatiles.Mist) {
|
|
2707
2723
|
volatiles.Mist = false;
|
|
2708
|
-
try log.end(ident, .
|
|
2724
|
+
try log.end(.{ ident, .MistSilent });
|
|
2709
2725
|
}
|
|
2710
2726
|
if (volatiles.FocusEnergy) {
|
|
2711
2727
|
volatiles.FocusEnergy = false;
|
|
2712
|
-
try log.end(ident, .
|
|
2728
|
+
try log.end(.{ ident, .FocusEnergySilent });
|
|
2713
2729
|
}
|
|
2714
2730
|
if (volatiles.LeechSeed) {
|
|
2715
2731
|
volatiles.LeechSeed = false;
|
|
2716
|
-
try log.end(ident, .
|
|
2732
|
+
try log.end(.{ ident, .LeechSeedSilent });
|
|
2717
2733
|
}
|
|
2718
2734
|
if (!showdown and volatiles.Toxic) {
|
|
2719
2735
|
volatiles.Toxic = false;
|
|
@@ -2721,24 +2737,39 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
|
|
|
2721
2737
|
}
|
|
2722
2738
|
if (volatiles.LightScreen) {
|
|
2723
2739
|
volatiles.LightScreen = false;
|
|
2724
|
-
try log.end(ident, .
|
|
2740
|
+
try log.end(.{ ident, .LightScreenSilent });
|
|
2725
2741
|
}
|
|
2726
2742
|
if (volatiles.Reflect) {
|
|
2727
2743
|
volatiles.Reflect = false;
|
|
2728
|
-
try log.end(ident, .
|
|
2744
|
+
try log.end(.{ ident, .ReflectSilent });
|
|
2729
2745
|
}
|
|
2730
2746
|
if (showdown and volatiles.Toxic) {
|
|
2731
2747
|
volatiles.Toxic = false;
|
|
2732
2748
|
// Pokémon Showdown erroneously clears the toxic counter
|
|
2733
2749
|
volatiles.toxic = 0;
|
|
2734
|
-
try log.end(ident, .
|
|
2750
|
+
try log.end(.{ ident, .ToxicSilent });
|
|
2735
2751
|
}
|
|
2736
2752
|
}
|
|
2737
2753
|
|
|
2754
|
+
fn decrement(
|
|
2755
|
+
comptime field: chance.Action.Field,
|
|
2756
|
+
player: Player,
|
|
2757
|
+
options: anytype,
|
|
2758
|
+
n: anytype,
|
|
2759
|
+
) @TypeOf(n) {
|
|
2760
|
+
return if (options.calc.overridden(player, field)) |obs| switch (obs) {
|
|
2761
|
+
.started, .ended => 0,
|
|
2762
|
+
.continuing => if (n > 1) n - 1 else n,
|
|
2763
|
+
else => unreachable,
|
|
2764
|
+
} else n - 1;
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2738
2767
|
pub const Rolls = struct {
|
|
2739
|
-
|
|
2768
|
+
fn speedTie(battle: anytype, options: anytype) !bool {
|
|
2740
2769
|
const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
|
|
2741
2770
|
player == .P1
|
|
2771
|
+
else if (@hasDecl(@TypeOf(battle.rng), "speedTie"))
|
|
2772
|
+
battle.rng.speedTie()
|
|
2742
2773
|
else if (showdown)
|
|
2743
2774
|
battle.rng.range(u8, 0, 2) == 0
|
|
2744
2775
|
else
|
|
@@ -2748,21 +2779,31 @@ pub const Rolls = struct {
|
|
|
2748
2779
|
return p1;
|
|
2749
2780
|
}
|
|
2750
2781
|
|
|
2751
|
-
|
|
2782
|
+
fn criticalHit(
|
|
2783
|
+
battle: anytype,
|
|
2784
|
+
player: Player,
|
|
2785
|
+
rate: u8,
|
|
2786
|
+
spurious: bool,
|
|
2787
|
+
options: anytype,
|
|
2788
|
+
) !bool {
|
|
2752
2789
|
const crit = if (options.calc.overridden(player, .critical_hit)) |val|
|
|
2753
2790
|
val == .true
|
|
2791
|
+
else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
|
|
2792
|
+
battle.rng.criticalHit(player, rate)
|
|
2754
2793
|
else if (showdown)
|
|
2755
2794
|
battle.rng.chance(u8, rate, 256)
|
|
2756
2795
|
else
|
|
2757
2796
|
std.math.rotl(u8, battle.rng.next(), 3) < rate;
|
|
2758
2797
|
|
|
2759
|
-
try options.chance.criticalHit(player, crit, rate);
|
|
2798
|
+
if (!spurious) try options.chance.criticalHit(player, crit, rate);
|
|
2760
2799
|
return crit;
|
|
2761
2800
|
}
|
|
2762
2801
|
|
|
2763
|
-
|
|
2802
|
+
fn damage(battle: anytype, player: Player, options: anytype) !u8 {
|
|
2764
2803
|
const roll = if (options.calc.overridden(player, .damage)) |val|
|
|
2765
2804
|
val
|
|
2805
|
+
else if (@hasDecl(@TypeOf(battle.rng), "damage"))
|
|
2806
|
+
battle.rng.damage(player)
|
|
2766
2807
|
else roll: {
|
|
2767
2808
|
if (showdown) break :roll battle.rng.range(u8, 217, 256);
|
|
2768
2809
|
while (true) {
|
|
@@ -2776,21 +2817,27 @@ pub const Rolls = struct {
|
|
|
2776
2817
|
return roll;
|
|
2777
2818
|
}
|
|
2778
2819
|
|
|
2779
|
-
|
|
2820
|
+
const uN = if (pkmn.options.miss) u8 else u9;
|
|
2821
|
+
fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) !bool {
|
|
2822
|
+
const acc: uN = if (pkmn.options.miss) accuracy else @as(uN, accuracy) + 1;
|
|
2780
2823
|
const ok = if (options.calc.overridden(player, .hit)) |val|
|
|
2781
2824
|
val == .true
|
|
2825
|
+
else if (@hasDecl(@TypeOf(battle.rng), "hit"))
|
|
2826
|
+
battle.rng.hit(player, acc)
|
|
2782
2827
|
else if (showdown)
|
|
2783
|
-
battle.rng.chance(
|
|
2828
|
+
battle.rng.chance(uN, acc, 256)
|
|
2784
2829
|
else
|
|
2785
|
-
battle.rng.next() <
|
|
2830
|
+
battle.rng.next() < acc;
|
|
2786
2831
|
|
|
2787
|
-
options.chance.hit(ok,
|
|
2832
|
+
try options.chance.hit(player, ok, acc);
|
|
2788
2833
|
return ok;
|
|
2789
2834
|
}
|
|
2790
2835
|
|
|
2791
|
-
|
|
2836
|
+
fn confused(battle: anytype, player: Player, options: anytype) !bool {
|
|
2792
2837
|
const cfz = if (options.calc.overridden(player, .confused)) |val|
|
|
2793
2838
|
val == .true
|
|
2839
|
+
else if (@hasDecl(@TypeOf(battle.rng), "confused"))
|
|
2840
|
+
battle.rng.confused(player)
|
|
2794
2841
|
else if (showdown)
|
|
2795
2842
|
!battle.rng.chance(u8, 128, 256)
|
|
2796
2843
|
else
|
|
@@ -2800,9 +2847,11 @@ pub const Rolls = struct {
|
|
|
2800
2847
|
return cfz;
|
|
2801
2848
|
}
|
|
2802
2849
|
|
|
2803
|
-
|
|
2850
|
+
fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
|
|
2804
2851
|
const par = if (options.calc.overridden(player, .paralyzed)) |val|
|
|
2805
2852
|
val == .true
|
|
2853
|
+
else if (@hasDecl(@TypeOf(battle.rng), "paralyzed"))
|
|
2854
|
+
battle.rng.paralyzed(player)
|
|
2806
2855
|
else if (showdown)
|
|
2807
2856
|
battle.rng.chance(u8, 63, 256)
|
|
2808
2857
|
else
|
|
@@ -2812,9 +2861,11 @@ pub const Rolls = struct {
|
|
|
2812
2861
|
return par;
|
|
2813
2862
|
}
|
|
2814
2863
|
|
|
2815
|
-
|
|
2864
|
+
fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
|
|
2816
2865
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2817
2866
|
val == .true
|
|
2867
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2868
|
+
battle.rng.secondaryChance(player, 25)
|
|
2818
2869
|
else if (showdown)
|
|
2819
2870
|
battle.rng.chance(u8, 25, 256)
|
|
2820
2871
|
else
|
|
@@ -2824,11 +2875,13 @@ pub const Rolls = struct {
|
|
|
2824
2875
|
return proc;
|
|
2825
2876
|
}
|
|
2826
2877
|
|
|
2827
|
-
|
|
2878
|
+
fn secondaryChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
|
|
2828
2879
|
const rate: u8 = if (low) 26 else 77;
|
|
2829
2880
|
|
|
2830
2881
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2831
2882
|
val == .true
|
|
2883
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2884
|
+
battle.rng.secondaryChance(player, rate)
|
|
2832
2885
|
else if (showdown)
|
|
2833
2886
|
battle.rng.chance(u8, rate, 256)
|
|
2834
2887
|
else
|
|
@@ -2838,11 +2891,13 @@ pub const Rolls = struct {
|
|
|
2838
2891
|
return proc;
|
|
2839
2892
|
}
|
|
2840
2893
|
|
|
2841
|
-
|
|
2894
|
+
fn poisonChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
|
|
2842
2895
|
const rate: u8 = if (low) 52 else 103;
|
|
2843
2896
|
|
|
2844
2897
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2845
2898
|
val == .true
|
|
2899
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2900
|
+
battle.rng.secondaryChance(player, rate)
|
|
2846
2901
|
else if (showdown)
|
|
2847
2902
|
battle.rng.chance(u8, rate, 256)
|
|
2848
2903
|
else
|
|
@@ -2852,9 +2907,11 @@ pub const Rolls = struct {
|
|
|
2852
2907
|
return proc;
|
|
2853
2908
|
}
|
|
2854
2909
|
|
|
2855
|
-
|
|
2910
|
+
fn unboost(battle: anytype, player: Player, options: anytype) !bool {
|
|
2856
2911
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2857
2912
|
val == .true
|
|
2913
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2914
|
+
battle.rng.secondaryChance(player, 85)
|
|
2858
2915
|
else if (showdown)
|
|
2859
2916
|
battle.rng.chance(u8, 85, 256)
|
|
2860
2917
|
else
|
|
@@ -2864,14 +2921,14 @@ pub const Rolls = struct {
|
|
|
2864
2921
|
return proc;
|
|
2865
2922
|
}
|
|
2866
2923
|
|
|
2867
|
-
|
|
2924
|
+
fn metronome(battle: anytype, player: Player, options: anytype) !Move {
|
|
2868
2925
|
const move: Move = if (options.calc.overridden(player, .metronome)) |val|
|
|
2869
2926
|
val
|
|
2870
|
-
else if (
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2927
|
+
else if (@hasDecl(@TypeOf(battle.rng), "metronome"))
|
|
2928
|
+
battle.rng.metronome(player)
|
|
2929
|
+
else if (showdown)
|
|
2930
|
+
Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
|
|
2931
|
+
else move: {
|
|
2875
2932
|
while (true) {
|
|
2876
2933
|
const r = battle.rng.next();
|
|
2877
2934
|
if (r == 0 or r == @intFromEnum(Move.Metronome)) continue;
|
|
@@ -2884,9 +2941,11 @@ pub const Rolls = struct {
|
|
|
2884
2941
|
return move;
|
|
2885
2942
|
}
|
|
2886
2943
|
|
|
2887
|
-
|
|
2944
|
+
fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
|
|
2888
2945
|
const power = if (options.calc.overridden(player, .psywave)) |val|
|
|
2889
2946
|
val - 1
|
|
2947
|
+
else if (@hasDecl(@TypeOf(battle.rng), "psywave"))
|
|
2948
|
+
battle.rng.psywave(player)
|
|
2890
2949
|
else if (showdown)
|
|
2891
2950
|
battle.rng.range(u8, 0, max)
|
|
2892
2951
|
else power: {
|
|
@@ -2901,11 +2960,14 @@ pub const Rolls = struct {
|
|
|
2901
2960
|
return power;
|
|
2902
2961
|
}
|
|
2903
2962
|
|
|
2904
|
-
|
|
2905
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2906
|
-
@intCast(val)
|
|
2963
|
+
fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2964
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2965
|
+
if (val >= 1 and val <= 7) @as(u3, @intCast(val)) else null
|
|
2966
|
+
else
|
|
2967
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
|
|
2968
|
+
battle.rng.sleepDuration(player)
|
|
2907
2969
|
else if (showdown)
|
|
2908
|
-
|
|
2970
|
+
battle.rng.range(u3, 1, 8)
|
|
2909
2971
|
else duration: {
|
|
2910
2972
|
while (true) {
|
|
2911
2973
|
const r = battle.rng.next() & 7;
|
|
@@ -2914,60 +2976,74 @@ pub const Rolls = struct {
|
|
|
2914
2976
|
};
|
|
2915
2977
|
|
|
2916
2978
|
assert(duration >= 1 and duration <= 7);
|
|
2917
|
-
options.chance.duration(
|
|
2979
|
+
options.chance.duration(player, duration);
|
|
2918
2980
|
return duration;
|
|
2919
2981
|
}
|
|
2920
2982
|
|
|
2921
|
-
|
|
2922
|
-
const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
|
|
2923
|
-
val
|
|
2983
|
+
fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
|
|
2984
|
+
const duration: u4 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2985
|
+
if (val >= 1 and val <= 8) val else null
|
|
2986
|
+
else
|
|
2987
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
|
|
2988
|
+
battle.rng.disableDuration(player)
|
|
2924
2989
|
else if (showdown)
|
|
2925
|
-
|
|
2990
|
+
battle.rng.range(u4, 1, 9)
|
|
2926
2991
|
else
|
|
2927
2992
|
@intCast((battle.rng.next() & 7) + 1);
|
|
2928
2993
|
|
|
2929
2994
|
assert(duration >= 1 and duration <= 8);
|
|
2930
|
-
options.chance.duration(
|
|
2995
|
+
options.chance.duration(player, duration);
|
|
2931
2996
|
return duration;
|
|
2932
2997
|
}
|
|
2933
2998
|
|
|
2934
|
-
|
|
2935
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2936
|
-
@intCast(val)
|
|
2999
|
+
fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
3000
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
3001
|
+
if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
|
|
3002
|
+
else
|
|
3003
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
|
|
3004
|
+
battle.rng.confusionDuration(player)
|
|
2937
3005
|
else if (showdown)
|
|
2938
|
-
|
|
3006
|
+
battle.rng.range(u3, 2, 6)
|
|
2939
3007
|
else
|
|
2940
3008
|
@intCast((battle.rng.next() & 3) + 2);
|
|
2941
3009
|
|
|
2942
3010
|
assert(duration >= 2 and duration <= 5);
|
|
2943
|
-
options.chance.duration(
|
|
3011
|
+
options.chance.duration(player, duration);
|
|
2944
3012
|
return duration;
|
|
2945
3013
|
}
|
|
2946
3014
|
|
|
2947
|
-
|
|
2948
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2949
|
-
@intCast(val)
|
|
3015
|
+
fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
3016
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
3017
|
+
if (val >= 2 and val <= 3) @as(u3, @intCast(val)) else null
|
|
3018
|
+
else
|
|
3019
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
|
|
3020
|
+
battle.rng.attackingDuration(player)
|
|
2950
3021
|
else if (showdown)
|
|
2951
|
-
|
|
3022
|
+
battle.rng.range(u3, 2, 4)
|
|
2952
3023
|
else
|
|
2953
3024
|
@intCast((battle.rng.next() & 1) + 2);
|
|
2954
3025
|
|
|
2955
3026
|
assert(duration >= 2 and duration <= 3);
|
|
2956
|
-
options.chance.duration(
|
|
3027
|
+
options.chance.duration(player, duration);
|
|
2957
3028
|
return duration;
|
|
2958
3029
|
}
|
|
2959
3030
|
|
|
2960
|
-
|
|
3031
|
+
const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
|
|
3032
|
+
|
|
3033
|
+
fn distribution(
|
|
2961
3034
|
battle: anytype,
|
|
2962
3035
|
comptime effect: Move.Effect,
|
|
2963
3036
|
player: Player,
|
|
2964
3037
|
options: anytype,
|
|
2965
3038
|
) !u3 {
|
|
2966
3039
|
const roll = if (effect == .Binding) .duration else .multi_hit;
|
|
2967
|
-
const n: u3 = if (options.calc.overridden(player, roll)) |val|
|
|
2968
|
-
@intCast(val)
|
|
3040
|
+
const n: u3 = (if (options.calc.overridden(player, roll)) |val|
|
|
3041
|
+
if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
|
|
3042
|
+
else
|
|
3043
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "distribution"))
|
|
3044
|
+
battle.rng.distribution(player)
|
|
2969
3045
|
else if (showdown)
|
|
2970
|
-
DISTRIBUTION[battle.rng.range(
|
|
3046
|
+
DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
|
|
2971
3047
|
else n: {
|
|
2972
3048
|
const r = (battle.rng.next() & 3);
|
|
2973
3049
|
break :n @intCast((if (r < 2) r else battle.rng.next() & 3) + 2);
|
|
@@ -2975,7 +3051,7 @@ pub const Rolls = struct {
|
|
|
2975
3051
|
|
|
2976
3052
|
assert(n >= 2 and n <= 5);
|
|
2977
3053
|
if (effect == .Binding) {
|
|
2978
|
-
options.chance.duration(
|
|
3054
|
+
options.chance.duration(player, n);
|
|
2979
3055
|
} else {
|
|
2980
3056
|
try options.chance.multiHit(player, n);
|
|
2981
3057
|
}
|
|
@@ -2990,15 +3066,15 @@ pub const Rolls = struct {
|
|
|
2990
3066
|
options: anytype,
|
|
2991
3067
|
) !u4 {
|
|
2992
3068
|
// TODO: consider throwing error instead of rerolling?
|
|
2993
|
-
const
|
|
3069
|
+
const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
|
|
2994
3070
|
if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
|
|
2995
3071
|
val
|
|
2996
3072
|
else
|
|
2997
3073
|
null
|
|
2998
3074
|
else
|
|
2999
|
-
null
|
|
3000
|
-
|
|
3001
|
-
|
|
3075
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
|
|
3076
|
+
battle.rng.moveSlot(player, moves, check_pp)
|
|
3077
|
+
else slot: {
|
|
3002
3078
|
if (showdown) {
|
|
3003
3079
|
if (check_pp == 0) {
|
|
3004
3080
|
var i: usize = moves.len;
|
|
@@ -3014,10 +3090,9 @@ pub const Rolls = struct {
|
|
|
3014
3090
|
while (i < moves.len and r > 0) : (i += 1) {
|
|
3015
3091
|
if (moves[i].pp > 0) {
|
|
3016
3092
|
r -= 1;
|
|
3017
|
-
if (r == 0) break;
|
|
3093
|
+
if (r == 0) break :slot @intCast(i + 1);
|
|
3018
3094
|
}
|
|
3019
3095
|
}
|
|
3020
|
-
break :slot @intCast(i + 1);
|
|
3021
3096
|
}
|
|
3022
3097
|
}
|
|
3023
3098
|
|
|
@@ -3039,11 +3114,11 @@ test "RNG agreement" {
|
|
|
3039
3114
|
expected[i] = @intCast(i * 0x1000000);
|
|
3040
3115
|
}
|
|
3041
3116
|
|
|
3042
|
-
var spe
|
|
3043
|
-
var cfz
|
|
3044
|
-
var par
|
|
3045
|
-
var brn
|
|
3046
|
-
var eff
|
|
3117
|
+
var spe: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
|
|
3118
|
+
var cfz: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
|
|
3119
|
+
var par: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
|
|
3120
|
+
var brn: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
|
|
3121
|
+
var eff: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
|
|
3047
3122
|
|
|
3048
3123
|
for (0..expected.len) |i| {
|
|
3049
3124
|
try expectEqual(spe.range(u8, 0, 2) == 0, i < Gen12.percent(50) + 1);
|
|
@@ -3079,11 +3154,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3079
3154
|
const side = battle.side(player);
|
|
3080
3155
|
const foe = battle.foe(player);
|
|
3081
3156
|
|
|
3082
|
-
|
|
3157
|
+
const active = &side.active;
|
|
3083
3158
|
const stored = side.stored();
|
|
3084
3159
|
|
|
3085
3160
|
// While players are not given any input options on the cartridge in these cases,
|
|
3086
|
-
// Pokémon Showdown instead produces a list with a single move that must be chosen
|
|
3161
|
+
// Pokémon Showdown instead produces a list with a single move that must be chosen
|
|
3087
3162
|
//
|
|
3088
3163
|
// Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
|
|
3089
3164
|
// here when not in compatibility mode, but the engine needs to be able to differentiate
|
|
@@ -3091,7 +3166,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3091
3166
|
// passing due to being forced into using a move. Instead of introducing another option
|
|
3092
3167
|
// we simply repurpose Move with no move slot, even though pedantically this is not
|
|
3093
3168
|
// strictly correct as the player would not have been presented the option to move or
|
|
3094
|
-
// switch at all
|
|
3169
|
+
// switch at all
|
|
3095
3170
|
if (isForced(active)) {
|
|
3096
3171
|
out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
|
|
3097
3172
|
n += 1;
|
|
@@ -3110,7 +3185,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3110
3185
|
// On the cartridge, all of these happen after "FIGHT" (indicating you are not
|
|
3111
3186
|
// switching) but before you are allowed to select a move. Pokémon Showdown instead
|
|
3112
3187
|
// either disables all other moves in the case of limited or requires you to select a
|
|
3113
|
-
// move normally if sleeping/frozen/bound
|
|
3188
|
+
// move normally if sleeping/frozen/bound
|
|
3114
3189
|
if (!showdown and (limited or foe.active.volatiles.Binding or
|
|
3115
3190
|
Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
|
|
3116
3191
|
{
|
|
@@ -3123,7 +3198,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3123
3198
|
// Pokémon Showdown handles Bide and Binding moves by checking if the move in question
|
|
3124
3199
|
// is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
|
|
3125
3200
|
// Move will not result in forcing the subsequent use unless the user also had the
|
|
3126
|
-
// proc-ed move in their moveset) and disabling all other moves
|
|
3201
|
+
// proc-ed move in their moveset) and disabling all other moves
|
|
3127
3202
|
if (limited) {
|
|
3128
3203
|
assert(showdown);
|
|
3129
3204
|
assert(side.last_selected_move != .None);
|
|
@@ -3133,7 +3208,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3133
3208
|
if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
|
|
3134
3209
|
// Pokémon Showdown displays Struggle if limited to Bide but unable to pick
|
|
3135
3210
|
const struggle =
|
|
3136
|
-
m.id == .Bide and (m.pp == 0 or active.volatiles.
|
|
3211
|
+
m.id == .Bide and (m.pp == 0 or active.volatiles.disable_move == slot);
|
|
3137
3212
|
const s = if (struggle) 0 else slot;
|
|
3138
3213
|
out[n] = .{ .type = .Move, .data = s };
|
|
3139
3214
|
n += 1;
|
|
@@ -3148,14 +3223,14 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3148
3223
|
const m = active.moves[slot - 1];
|
|
3149
3224
|
if (m.id == .None) break;
|
|
3150
3225
|
if (m.pp == 0) continue;
|
|
3151
|
-
if (active.volatiles.
|
|
3226
|
+
if (active.volatiles.disable_move == slot) continue;
|
|
3152
3227
|
out[n] = .{ .type = .Move, .data = slot };
|
|
3153
3228
|
n += 1;
|
|
3154
3229
|
}
|
|
3155
3230
|
// Struggle (Pokémon Showdown would use 'move 1' here)
|
|
3156
3231
|
if (n == before) {
|
|
3157
3232
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
3158
|
-
if (!
|
|
3233
|
+
if (!pkmn.options.mod) {
|
|
3159
3234
|
while (slot <= 4) : (slot += 1) {
|
|
3160
3235
|
if (active.moves[slot - 1].pp != 0) return n;
|
|
3161
3236
|
}
|