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