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