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