@pkmn/engine 0.1.0-dev.f3aaee39 → 0.1.0-dev.f3b63b86
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 +77 -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 +26 -4
- 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 +69 -74
- 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 +273 -120
- package/package.json +64 -123
- package/src/bin/install-pkmn-engine +47 -21
- package/src/bin/pkmn-debug +1 -1
- package/src/lib/bindings/c.zig +200 -0
- package/src/lib/{binding → bindings}/node.zig +15 -32
- package/src/lib/bindings/wasm.zig +50 -0
- package/src/lib/common/array.zig +70 -0
- package/src/lib/common/data.zig +12 -24
- package/src/lib/common/debug.zig +32 -11
- package/src/lib/common/js.zig +3 -7
- package/src/lib/common/optional.zig +19 -18
- package/src/lib/common/options.zig +6 -0
- package/src/lib/common/protocol.zig +919 -430
- package/src/lib/common/rational.zig +341 -254
- package/src/lib/common/rng.zig +22 -15
- package/src/lib/common/util.zig +33 -1
- package/src/lib/gen1/calc.zig +706 -177
- package/src/lib/gen1/chance.zig +428 -302
- 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 +9 -10
- package/src/lib/gen1/data.zig +60 -75
- package/src/lib/gen1/helpers.zig +90 -563
- package/src/lib/gen1/mechanics.zig +682 -600
- package/src/lib/gen2/calc.zig +150 -0
- package/src/lib/gen2/chance.zig +896 -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 +139 -34
- package/src/lib/gen2/data.zig +121 -96
- package/src/lib/gen2/generated.zig +801 -0
- package/src/lib/gen2/helpers.zig +53 -38
- package/src/lib/gen2/mechanics.zig +3580 -29
- package/src/lib/napi.h +2 -0
- package/src/lib/node.zig +14 -0
- package/src/lib/pkmn.zig +125 -29
- package/src/lib/wasm.zig +13 -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 -892
- package/src/lib/gen2/README.md +0 -308
- package/src/test/display/pkmn.html.tmpl +0 -368
|
@@ -1,38 +1,29 @@
|
|
|
1
|
-
const
|
|
2
|
-
|
|
3
|
-
const pkmn = @import("../pkmn.zig");
|
|
1
|
+
const DEBUG = @import("../common/debug.zig").print;
|
|
4
2
|
|
|
3
|
+
const builtin = @import("builtin");
|
|
4
|
+
const chance = @import("chance.zig");
|
|
5
5
|
const common = @import("../common/data.zig");
|
|
6
|
-
const
|
|
6
|
+
const data = @import("data.zig");
|
|
7
|
+
const pkmn = @import("../pkmn.zig");
|
|
7
8
|
const protocol = @import("../common/protocol.zig");
|
|
8
9
|
const rng = @import("../common/rng.zig");
|
|
9
|
-
|
|
10
|
-
const Action = @import("chance.zig").Action;
|
|
11
|
-
|
|
12
|
-
const data = @import("data.zig");
|
|
10
|
+
const std = @import("std");
|
|
13
11
|
|
|
14
12
|
const assert = std.debug.assert;
|
|
15
|
-
|
|
16
|
-
const expectEqual = std.testing.expectEqual;
|
|
17
|
-
|
|
18
|
-
const Choice = common.Choice;
|
|
19
|
-
const ID = common.ID;
|
|
20
|
-
const Player = common.Player;
|
|
21
|
-
const Result = common.Result;
|
|
22
|
-
|
|
23
|
-
const showdown = pkmn.options.showdown;
|
|
24
|
-
|
|
25
13
|
const Boost = protocol.Boost;
|
|
14
|
+
const Choice = common.Choice;
|
|
26
15
|
const Damage = protocol.Damage;
|
|
27
|
-
const Heal = protocol.Heal;
|
|
28
|
-
|
|
29
|
-
const Gen12 = rng.Gen12;
|
|
30
|
-
|
|
31
|
-
const ActivePokemon = data.ActivePokemon;
|
|
32
16
|
const Effectiveness = data.Effectiveness;
|
|
17
|
+
const expectEqual = std.testing.expectEqual;
|
|
18
|
+
const Fail = protocol.Fail;
|
|
19
|
+
const Gen12 = rng.Gen12;
|
|
20
|
+
const Heal = protocol.Heal;
|
|
21
|
+
const ID = common.ID;
|
|
33
22
|
const Move = data.Move;
|
|
34
23
|
const MoveSlot = data.MoveSlot;
|
|
35
|
-
const
|
|
24
|
+
const Player = common.Player;
|
|
25
|
+
const Result = common.Result;
|
|
26
|
+
const showdown = pkmn.options.showdown;
|
|
36
27
|
const Side = data.Side;
|
|
37
28
|
const Species = data.Species;
|
|
38
29
|
const Stats = data.Stats;
|
|
@@ -59,34 +50,26 @@ const BOOSTS = &[_][2]u8{
|
|
|
59
50
|
|
|
60
51
|
const MAX_STAT_VALUE = 999;
|
|
61
52
|
|
|
62
|
-
const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
|
|
63
|
-
|
|
64
53
|
pub fn update(battle: anytype, c1: Choice, c2: Choice, options: anytype) !Result {
|
|
65
|
-
// FIXME
|
|
66
|
-
// std.debug.print("\n", .{});
|
|
67
|
-
// DEBUG(battle.turn);
|
|
68
|
-
// std.debug.print("// ", .{});
|
|
69
54
|
assert(c1.type != .Pass or c2.type != .Pass or battle.turn == 0);
|
|
70
55
|
if (battle.turn == 0) return start(battle, options);
|
|
71
56
|
|
|
72
57
|
var s1 = false;
|
|
73
58
|
var s2 = false;
|
|
74
|
-
var f1: ?Move = null;
|
|
75
|
-
var f2: ?Move = null;
|
|
76
59
|
|
|
77
|
-
if (selectMove(battle, .P1, c1, c2, &s1
|
|
78
|
-
if (selectMove(battle, .P2, c2, c1, &s2
|
|
60
|
+
if (selectMove(battle, .P1, c1, c2, &s1)) |r| return r;
|
|
61
|
+
if (selectMove(battle, .P2, c2, c1, &s2)) |r| return r;
|
|
79
62
|
|
|
80
63
|
var p1 = battle.side(.P1);
|
|
81
64
|
var p2 = battle.side(.P2);
|
|
82
65
|
|
|
83
|
-
|
|
84
|
-
|
|
66
|
+
const r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
|
|
67
|
+
const r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
|
|
85
68
|
|
|
86
69
|
if (try turnOrder(battle, c1, c2, options) == .P1) {
|
|
87
|
-
if (try doTurn(battle, .P1, c1, r1, s1,
|
|
70
|
+
if (try doTurn(battle, .P1, c1, r1, s1, .P2, c2, r2, s2, options)) |r| return r;
|
|
88
71
|
} else {
|
|
89
|
-
if (try doTurn(battle, .P2, c2, r2, s2,
|
|
72
|
+
if (try doTurn(battle, .P2, c2, r2, s2, .P1, c1, r1, s1, options)) |r| return r;
|
|
90
73
|
}
|
|
91
74
|
|
|
92
75
|
if (p1.active.volatiles.attacks == 0) p1.active.volatiles.Binding = false;
|
|
@@ -99,11 +82,11 @@ fn start(battle: anytype, options: anytype) !Result {
|
|
|
99
82
|
const p1 = battle.side(.P1);
|
|
100
83
|
const p2 = battle.side(.P2);
|
|
101
84
|
|
|
102
|
-
|
|
85
|
+
const p1_slot = findFirstAlive(p1);
|
|
103
86
|
assert(!showdown or p1_slot == 1);
|
|
104
87
|
if (p1_slot == 0) return if (findFirstAlive(p2) == 0) Result.Tie else Result.Lose;
|
|
105
88
|
|
|
106
|
-
|
|
89
|
+
const p2_slot = findFirstAlive(p2);
|
|
107
90
|
assert(!showdown or p2_slot == 1);
|
|
108
91
|
if (p2_slot == 0) return Result.Win;
|
|
109
92
|
|
|
@@ -124,7 +107,6 @@ fn selectMove(
|
|
|
124
107
|
choice: Choice,
|
|
125
108
|
foe_choice: Choice,
|
|
126
109
|
skip_turn: *bool,
|
|
127
|
-
from: *?Move,
|
|
128
110
|
) ?Result {
|
|
129
111
|
if (choice.type == .Pass) return null;
|
|
130
112
|
|
|
@@ -141,7 +123,6 @@ fn selectMove(
|
|
|
141
123
|
return null;
|
|
142
124
|
}
|
|
143
125
|
if (volatiles.Rage) {
|
|
144
|
-
from.* = side.last_used_move;
|
|
145
126
|
if (showdown) {
|
|
146
127
|
if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
|
|
147
128
|
saveMove(battle, player, null);
|
|
@@ -151,7 +132,6 @@ fn selectMove(
|
|
|
151
132
|
// Pokémon Showdown removes Flinch at the end-of-turn in its residual handler
|
|
152
133
|
if (!showdown) volatiles.Flinch = false;
|
|
153
134
|
if (volatiles.Thrashing or volatiles.Charging) {
|
|
154
|
-
from.* = side.last_selected_move;
|
|
155
135
|
if (showdown) {
|
|
156
136
|
if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
|
|
157
137
|
saveMove(battle, player, null);
|
|
@@ -172,15 +152,13 @@ fn selectMove(
|
|
|
172
152
|
return null;
|
|
173
153
|
}
|
|
174
154
|
if (volatiles.Binding) {
|
|
175
|
-
from.* = side.last_used_move;
|
|
176
155
|
if (showdown) {
|
|
177
|
-
if (foe_choice.type == .Switch) from.* = null;
|
|
178
156
|
// Pokémon Showdown overwrites Mirror Move with whatever was selected - really this
|
|
179
157
|
// should set side.last_selected_move = last.id to reuse Mirror Move and fail in order
|
|
180
158
|
// to satisfy the conditions of the Desync Clause Mod. However, because Binding is still
|
|
181
159
|
// set the selected move will not actually be used, it will just be reported as having
|
|
182
160
|
// been used (this differs from how Pokémon Showdown works, but its impossible to
|
|
183
|
-
// replicate the incorrect behavior with the correct mechanisms)
|
|
161
|
+
// replicate the incorrect behavior with the correct mechanisms)
|
|
184
162
|
saveMove(battle, player, choice);
|
|
185
163
|
} else {
|
|
186
164
|
assert(choice.data == 0);
|
|
@@ -188,7 +166,10 @@ fn selectMove(
|
|
|
188
166
|
if (foe_choice.type == .Switch) {
|
|
189
167
|
const last = side.active.move(battle.lastMove(player).index);
|
|
190
168
|
if (last.id == .Metronome) side.last_selected_move = last.id;
|
|
191
|
-
if (last.id == .MirrorMove)
|
|
169
|
+
if (last.id == .MirrorMove) {
|
|
170
|
+
if (!pkmn.options.mod) return Result.Error;
|
|
171
|
+
side.last_selected_move = last.id;
|
|
172
|
+
}
|
|
192
173
|
}
|
|
193
174
|
}
|
|
194
175
|
return null;
|
|
@@ -210,16 +191,13 @@ fn selectMove(
|
|
|
210
191
|
if (choice.data == 0) {
|
|
211
192
|
const struggle = ok: {
|
|
212
193
|
for (side.active.moves, 0..) |move, i| {
|
|
213
|
-
if (move.pp > 0 and volatiles.
|
|
194
|
+
if (move.pp > 0 and volatiles.disable_move != i + 1) break :ok false;
|
|
214
195
|
}
|
|
215
196
|
break :ok true;
|
|
216
197
|
};
|
|
217
|
-
|
|
218
198
|
assert(struggle);
|
|
219
|
-
saveMove(battle, player, choice);
|
|
220
|
-
} else {
|
|
221
|
-
saveMove(battle, player, choice);
|
|
222
199
|
}
|
|
200
|
+
saveMove(battle, player, choice);
|
|
223
201
|
|
|
224
202
|
return null;
|
|
225
203
|
}
|
|
@@ -232,7 +210,7 @@ fn saveMove(battle: anytype, player: Player, choice: ?Choice) void {
|
|
|
232
210
|
if (c.data == 0) {
|
|
233
211
|
side.last_selected_move = .Struggle;
|
|
234
212
|
} else {
|
|
235
|
-
assert(showdown or side.active.volatiles.
|
|
213
|
+
assert(showdown or side.active.volatiles.disable_move != c.data);
|
|
236
214
|
const move = side.active.move(c.data);
|
|
237
215
|
// You cannot *select* a move with 0 PP (except on Pokémon Showdown where that is
|
|
238
216
|
// sometimes required...), but a 0 PP move can be used automatically
|
|
@@ -270,17 +248,17 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: a
|
|
|
270
248
|
active.moves = incoming.moves;
|
|
271
249
|
|
|
272
250
|
statusModify(incoming.status, &active.stats);
|
|
251
|
+
options.chance.switched(player, slot);
|
|
273
252
|
|
|
274
253
|
foe.active.volatiles.Binding = false;
|
|
275
254
|
|
|
276
|
-
try options.log.switched(battle.active(player), incoming);
|
|
277
|
-
options.chance.switched(player, side.order[0], out);
|
|
255
|
+
try options.log.switched(.{ battle.active(player), incoming });
|
|
278
256
|
|
|
279
257
|
if (showdown and incoming.status == Status.TOX) {
|
|
280
258
|
incoming.status = Status.init(.PSN);
|
|
281
259
|
// Technically, Pokémon Showdown adds these after *both* Pokémon have switched, but we'd
|
|
282
260
|
// rather not clutter up turnOrder just for this (incorrect) log message
|
|
283
|
-
try options.log.status(battle.active(player), incoming.status, .Silent);
|
|
261
|
+
try options.log.status(.{ battle.active(player), incoming.status, .Silent });
|
|
284
262
|
}
|
|
285
263
|
}
|
|
286
264
|
|
|
@@ -343,12 +321,10 @@ fn doTurn(
|
|
|
343
321
|
player_choice: Choice,
|
|
344
322
|
player_rewrap: bool,
|
|
345
323
|
player_skip: bool,
|
|
346
|
-
player_from: ?Move,
|
|
347
324
|
foe_player: Player,
|
|
348
325
|
foe_choice: Choice,
|
|
349
326
|
foe_rewrap: bool,
|
|
350
327
|
foe_skip: bool,
|
|
351
|
-
foe_from: ?Move,
|
|
352
328
|
options: anytype,
|
|
353
329
|
) !?Result {
|
|
354
330
|
assert(player_choice.type != .Pass);
|
|
@@ -361,19 +337,18 @@ fn doTurn(
|
|
|
361
337
|
player_choice,
|
|
362
338
|
player_rewrap,
|
|
363
339
|
player_skip,
|
|
364
|
-
player_from,
|
|
365
340
|
&residual,
|
|
366
341
|
options,
|
|
367
342
|
)) |r| return r;
|
|
368
343
|
if (!replace) {
|
|
369
344
|
if (player_choice.type != .Switch) {
|
|
370
|
-
if (try checkFaint(battle, foe_player, options)) |r| return r;
|
|
345
|
+
if (try checkFaint(battle, foe_player, true, options)) |r| return r;
|
|
371
346
|
}
|
|
372
347
|
if (residual) try handleResidual(battle, player, options);
|
|
373
|
-
if (try checkFaint(battle, player, options)) |r| return r;
|
|
348
|
+
if (try checkFaint(battle, player, false, options)) |r| return r;
|
|
374
349
|
} else if (foe_choice.type == .Pass) return null;
|
|
375
350
|
|
|
376
|
-
options.chance.clearPending();
|
|
351
|
+
if (!showdown) options.chance.clearPending();
|
|
377
352
|
|
|
378
353
|
residual = true;
|
|
379
354
|
replace = battle.side(foe_player).stored().hp == 0;
|
|
@@ -384,16 +359,15 @@ fn doTurn(
|
|
|
384
359
|
foe_choice,
|
|
385
360
|
foe_rewrap,
|
|
386
361
|
foe_skip,
|
|
387
|
-
foe_from,
|
|
388
362
|
&residual,
|
|
389
363
|
options,
|
|
390
364
|
)) |r| return r;
|
|
391
365
|
if (!replace) {
|
|
392
366
|
if (!calc and foe_choice.type != .Switch) {
|
|
393
|
-
if (try checkFaint(battle, player, options)) |r| return r;
|
|
367
|
+
if (try checkFaint(battle, player, true, options)) |r| return r;
|
|
394
368
|
}
|
|
395
369
|
if (residual) try handleResidual(battle, foe_player, options);
|
|
396
|
-
if (try checkFaint(battle, foe_player, options)) |r| return r;
|
|
370
|
+
if (try checkFaint(battle, foe_player, false, options)) |r| return r;
|
|
397
371
|
}
|
|
398
372
|
|
|
399
373
|
// Flinch is bugged on Pokémon Showdown because it gets implemented with a duration which causes
|
|
@@ -412,7 +386,6 @@ fn executeMove(
|
|
|
412
386
|
choice: Choice,
|
|
413
387
|
rewrap: bool,
|
|
414
388
|
skip: bool,
|
|
415
|
-
from: ?Move,
|
|
416
389
|
residual: *bool,
|
|
417
390
|
options: anytype,
|
|
418
391
|
) !?Result {
|
|
@@ -429,7 +402,7 @@ fn executeMove(
|
|
|
429
402
|
if (side.last_selected_move == .SKIP_TURN) {
|
|
430
403
|
assert(!showdown);
|
|
431
404
|
if (battle.foe(player).active.volatiles.Binding) {
|
|
432
|
-
try options.log.cant(battle.active(player), .Bound);
|
|
405
|
+
try options.log.cant(.{ battle.active(player), .Bound });
|
|
433
406
|
}
|
|
434
407
|
return null;
|
|
435
408
|
}
|
|
@@ -447,11 +420,13 @@ fn executeMove(
|
|
|
447
420
|
mslot = @intCast(battle.lastMove(player).index);
|
|
448
421
|
const stored = side.stored();
|
|
449
422
|
// GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
|
|
450
|
-
auto =
|
|
451
|
-
|
|
423
|
+
auto = pkmn.options.mod or isForced(&side.active) or
|
|
424
|
+
side.active.volatiles.Binding or side.active.volatiles.Bide or
|
|
425
|
+
side.last_selected_move == .HyperBeam or
|
|
426
|
+
Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
|
|
452
427
|
// If it wasn't Hyper Beam or the continuation of a move effect then we must have just
|
|
453
428
|
// thawed, in which case we will desync unless the last_selected_move happened to be at
|
|
454
|
-
// index 1 and the current Pokémon has the same move in its first slot
|
|
429
|
+
// index 1 and the current Pokémon has the same move in its first slot
|
|
455
430
|
if (!auto) {
|
|
456
431
|
// side.active.moves(slot) is safe to check even though the slot in question might not
|
|
457
432
|
// technically be from this Pokémon because it must be exactly 1 to not desync and
|
|
@@ -470,7 +445,7 @@ fn executeMove(
|
|
|
470
445
|
|
|
471
446
|
var skip_can = false;
|
|
472
447
|
var skip_pp = false;
|
|
473
|
-
switch (try beforeMove(battle, player, skip,
|
|
448
|
+
switch (try beforeMove(battle, player, skip, residual, options)) {
|
|
474
449
|
.done => return null,
|
|
475
450
|
.skip_can => skip_can = true,
|
|
476
451
|
.skip_pp => skip_pp = true,
|
|
@@ -478,10 +453,29 @@ fn executeMove(
|
|
|
478
453
|
.err => return @as(?Result, Result.Error),
|
|
479
454
|
}
|
|
480
455
|
|
|
481
|
-
|
|
482
|
-
|
|
456
|
+
// Pokémon Showdown incorrectly implements PP deduction when handling the Hyper Beam automatic
|
|
457
|
+
// selection glitch - if the move was proced due to Metronome / Mirror Move it either subtracts
|
|
458
|
+
// from the Hyper Beam slot if present or skips PP deduction entirely...
|
|
459
|
+
if (showdown and !skip_pp and side.last_selected_move == .HyperBeam and
|
|
460
|
+
side.last_selected_move != side.active.move(mslot).id)
|
|
461
|
+
{
|
|
462
|
+
assert(mslot == 1);
|
|
463
|
+
const has_beam = has_beam: {
|
|
464
|
+
for (side.active.moves, 0..) |m, i| {
|
|
465
|
+
if (m.id == .HyperBeam) {
|
|
466
|
+
mslot = @intCast(i + 1);
|
|
467
|
+
break :has_beam true;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
break :has_beam false;
|
|
471
|
+
};
|
|
472
|
+
if (!has_beam) skip_pp = true;
|
|
483
473
|
}
|
|
484
474
|
|
|
475
|
+
const can = skip_can or
|
|
476
|
+
try canMove(battle, player, mslot, auto, skip_pp, .None, residual, options);
|
|
477
|
+
if (!can) return null;
|
|
478
|
+
|
|
485
479
|
return doMove(battle, player, mslot, rewrap, auto, residual, options);
|
|
486
480
|
}
|
|
487
481
|
|
|
@@ -491,7 +485,6 @@ fn beforeMove(
|
|
|
491
485
|
battle: anytype,
|
|
492
486
|
player: Player,
|
|
493
487
|
skip: bool,
|
|
494
|
-
from: ?Move,
|
|
495
488
|
residual: *bool,
|
|
496
489
|
options: anytype,
|
|
497
490
|
) !BeforeMove {
|
|
@@ -505,36 +498,41 @@ fn beforeMove(
|
|
|
505
498
|
|
|
506
499
|
if (Status.is(stored.status, .SLP)) {
|
|
507
500
|
const before = stored.status;
|
|
501
|
+
const slf = Status.is(stored.status, .EXT);
|
|
508
502
|
// Even if the EXT bit is set this will still correctly modify the sleep duration
|
|
509
|
-
if (options.calc.
|
|
510
|
-
|
|
503
|
+
if (options.calc.overridden(player, .sleep)) |obs| switch (obs) {
|
|
504
|
+
.started, .ended => stored.status = 0,
|
|
505
|
+
.continuing => if (Status.duration(stored.status) > 1) {
|
|
506
|
+
stored.status -= 1;
|
|
507
|
+
},
|
|
508
|
+
else => unreachable,
|
|
511
509
|
} else {
|
|
512
510
|
stored.status -= 1;
|
|
513
511
|
}
|
|
514
512
|
|
|
515
513
|
const duration = Status.duration(stored.status);
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
514
|
+
try options.chance.sleep(player, if (slf)
|
|
515
|
+
.None
|
|
516
|
+
else if (duration == 0) .ended else .continuing);
|
|
519
517
|
|
|
520
518
|
if (duration == 0) {
|
|
521
|
-
try log.curestatus(ident, before, .Message);
|
|
519
|
+
try log.curestatus(.{ ident, before, .Message });
|
|
522
520
|
stored.status = 0; // clears EXT if present
|
|
523
521
|
} else {
|
|
524
|
-
try log.cant(ident, .Sleep);
|
|
522
|
+
try log.cant(.{ ident, .Sleep });
|
|
525
523
|
}
|
|
526
524
|
side.last_used_move = .None;
|
|
527
525
|
return .done;
|
|
528
526
|
}
|
|
529
527
|
|
|
530
528
|
if (Status.is(stored.status, .FRZ)) {
|
|
531
|
-
try log.cant(ident, .Freeze);
|
|
529
|
+
try log.cant(.{ ident, .Freeze });
|
|
532
530
|
side.last_used_move = .None;
|
|
533
531
|
return .done;
|
|
534
532
|
}
|
|
535
533
|
|
|
536
534
|
if (skip or foe.active.volatiles.Binding) {
|
|
537
|
-
try log.cant(ident, .Bound);
|
|
535
|
+
try log.cant(.{ ident, .Bound });
|
|
538
536
|
return .done;
|
|
539
537
|
}
|
|
540
538
|
|
|
@@ -542,27 +540,27 @@ fn beforeMove(
|
|
|
542
540
|
// Pokémon Showdown doesn't clear Flinch until its imaginary "residual" phase, meaning
|
|
543
541
|
// Pokémon can sometimes flinch multiple times from the same original hit
|
|
544
542
|
if (!showdown) volatiles.Flinch = false;
|
|
545
|
-
try log.cant(ident, .Flinch);
|
|
543
|
+
try log.cant(.{ ident, .Flinch });
|
|
546
544
|
return .done;
|
|
547
545
|
}
|
|
548
546
|
|
|
549
547
|
if (volatiles.Recharging) {
|
|
550
548
|
volatiles.Recharging = false;
|
|
551
|
-
try log.cant(ident, .Recharge);
|
|
549
|
+
try log.cant(.{ ident, .Recharge });
|
|
552
550
|
return .done;
|
|
553
551
|
}
|
|
554
552
|
|
|
555
|
-
if (volatiles.
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
553
|
+
if (volatiles.disable_duration > 0) {
|
|
554
|
+
volatiles.disable_duration =
|
|
555
|
+
decrement(.disable, player, options, volatiles.disable_duration);
|
|
556
|
+
try options.chance.disable(
|
|
557
|
+
player,
|
|
558
|
+
if (volatiles.disable_duration == 0) .ended else .continuing,
|
|
559
|
+
);
|
|
562
560
|
|
|
563
|
-
if (volatiles.
|
|
564
|
-
volatiles.
|
|
565
|
-
try log.end(ident, .Disable);
|
|
561
|
+
if (volatiles.disable_duration == 0) {
|
|
562
|
+
volatiles.disable_move = 0;
|
|
563
|
+
try log.end(.{ ident, .Disable });
|
|
566
564
|
}
|
|
567
565
|
}
|
|
568
566
|
// Pokémon Showdown's disable condition has a single onBeforeMove handler
|
|
@@ -571,25 +569,30 @@ fn beforeMove(
|
|
|
571
569
|
// This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
|
|
572
570
|
// before the side had a selected a move - we simply need to assume this leads to a desync
|
|
573
571
|
if (side.last_selected_move == .None) {
|
|
574
|
-
assert(!
|
|
572
|
+
assert(!pkmn.options.mod);
|
|
575
573
|
return .err;
|
|
576
574
|
}
|
|
577
575
|
|
|
578
576
|
if (volatiles.Confusion) {
|
|
579
577
|
assert(volatiles.confusion > 0);
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
if (
|
|
578
|
+
if (options.calc.overridden(player, .confusion)) |obs| switch (obs) {
|
|
579
|
+
.started, .ended => volatiles.confusion = 0,
|
|
580
|
+
.continuing => if (volatiles.confusion > 1) {
|
|
581
|
+
volatiles.confusion -= 1;
|
|
582
|
+
},
|
|
583
|
+
.overwritten => {},
|
|
584
|
+
else => unreachable,
|
|
583
585
|
} else {
|
|
584
586
|
volatiles.confusion -= 1;
|
|
585
587
|
}
|
|
586
|
-
|
|
588
|
+
|
|
589
|
+
try options.chance.confusion(player, if (volatiles.confusion == 0) .ended else .continuing);
|
|
587
590
|
|
|
588
591
|
if (volatiles.confusion == 0) {
|
|
589
592
|
volatiles.Confusion = false;
|
|
590
|
-
try log.end(ident, .Confusion);
|
|
593
|
+
try log.end(.{ ident, .Confusion });
|
|
591
594
|
} else {
|
|
592
|
-
try log.activate(ident, .Confusion);
|
|
595
|
+
try log.activate(.{ ident, .Confusion });
|
|
593
596
|
|
|
594
597
|
if (try Rolls.confused(battle, player, options)) {
|
|
595
598
|
assert(!volatiles.MultiHit);
|
|
@@ -601,7 +604,9 @@ fn beforeMove(
|
|
|
601
604
|
volatiles.Charging = false;
|
|
602
605
|
volatiles.Binding = false;
|
|
603
606
|
volatiles.Invulnerable = false;
|
|
604
|
-
|
|
607
|
+
|
|
608
|
+
options.chance.observe(.attacking, player, .None);
|
|
609
|
+
options.chance.observe(.binding, player, .None);
|
|
605
610
|
{
|
|
606
611
|
// This feels (and is) disgusting but the cartridge literally just overwrites
|
|
607
612
|
// the opponent's defense with the user's defense and resets it after. As a
|
|
@@ -609,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
|
|
@@ -888,20 +878,20 @@ fn doMove(
|
|
|
888
878
|
var miss = showdown and miss: {
|
|
889
879
|
immune = move.target != .Self and !status and !counter and
|
|
890
880
|
(@intFromEnum(move.type.effectiveness(foe.active.types.type1)) == 0 or
|
|
891
|
-
|
|
881
|
+
@intFromEnum(move.type.effectiveness(foe.active.types.type2)) == 0);
|
|
892
882
|
if (immune and move.effect != .Binding) break :miss true;
|
|
893
883
|
if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) {
|
|
894
884
|
battle.last_damage = 0;
|
|
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;
|
|
@@ -1008,18 +1019,18 @@ fn doMove(
|
|
|
1008
1019
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1009
1020
|
|
|
1010
1021
|
var nullified = false;
|
|
1011
|
-
var hit:
|
|
1022
|
+
var hit: usize = 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()), @as(u4, @intCast(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,40 +1267,43 @@ 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
|
|
|
1253
1274
|
// Pretend Splash was used as a stand-in when no move has been used to fail below with 0 BP
|
|
1254
1275
|
const foe_last_selected_move =
|
|
1255
1276
|
Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1277
|
+
.Splash
|
|
1278
|
+
else
|
|
1279
|
+
foe.last_selected_move);
|
|
1259
1280
|
|
|
1260
1281
|
const used = battle.lastMove(player.foe()).counterable != 0;
|
|
1261
1282
|
const selected = foe_last_selected_move.bp > 0 and
|
|
1262
1283
|
foe.last_selected_move != .Counter and
|
|
1263
1284
|
(foe_last_selected_move.type == .Normal or
|
|
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
|
|
|
@@ -1339,10 +1363,10 @@ fn mirrorMove(
|
|
|
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)) {
|
|
@@ -1363,7 +1387,7 @@ fn metronome(
|
|
|
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
|
|
|
@@ -1419,7 +1446,16 @@ fn moveHit(
|
|
|
1419
1446
|
break :miss true;
|
|
1420
1447
|
}
|
|
1421
1448
|
if (!showdown) {
|
|
1422
|
-
if (move.effect == .Swift)
|
|
1449
|
+
if (move.effect == .Swift) {
|
|
1450
|
+
// If Swift could cause a division-by-zero freeze (which doesn't exist on Pokémon
|
|
1451
|
+
// Showdown, hence why the block above doesn't include this code), we need the
|
|
1452
|
+
// damage roll to be committed. However, the commit code will not save this roll if
|
|
1453
|
+
// the glitch occurs and the move was not marked as having hit. To work around this
|
|
1454
|
+
// we flag that the move hit (as Swift trivially does), but pass the probability of
|
|
1455
|
+
// the hit as zero to ensure it doesn't actually get saved.
|
|
1456
|
+
try options.chance.hit(player, true, 0);
|
|
1457
|
+
return true;
|
|
1458
|
+
}
|
|
1423
1459
|
if (foe.active.volatiles.Invulnerable) break :miss true;
|
|
1424
1460
|
}
|
|
1425
1461
|
|
|
@@ -1438,7 +1474,7 @@ fn moveHit(
|
|
|
1438
1474
|
const overwritten = overwrite and state > 0;
|
|
1439
1475
|
assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
|
|
1440
1476
|
|
|
1441
|
-
var accuracy = if (overwritten) state else
|
|
1477
|
+
var accuracy: u16 = if (overwritten) state else move.accuracy;
|
|
1442
1478
|
var boost = BOOSTS[@as(u4, @intCast(@as(i8, side.active.boosts.accuracy) + 6))];
|
|
1443
1479
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1444
1480
|
boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
|
|
@@ -1450,7 +1486,7 @@ fn moveHit(
|
|
|
1450
1486
|
if (overwrite) side.active.volatiles.state = accuracy;
|
|
1451
1487
|
|
|
1452
1488
|
// GLITCH: max accuracy is 255 so 1/256 chance of miss
|
|
1453
|
-
break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
|
|
1489
|
+
break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
|
|
1454
1490
|
};
|
|
1455
1491
|
|
|
1456
1492
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1461,13 +1497,17 @@ fn moveHit(
|
|
|
1461
1497
|
|
|
1462
1498
|
if (!miss) return true;
|
|
1463
1499
|
if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
|
|
1500
|
+
|
|
1464
1501
|
side.active.volatiles.Binding = false;
|
|
1502
|
+
options.chance.observe(.binding, player, .None);
|
|
1503
|
+
|
|
1465
1504
|
return false;
|
|
1466
1505
|
}
|
|
1467
1506
|
|
|
1468
1507
|
fn checkFaint(
|
|
1469
1508
|
battle: anytype,
|
|
1470
1509
|
player: Player,
|
|
1510
|
+
cap: bool,
|
|
1471
1511
|
options: anytype,
|
|
1472
1512
|
) @TypeOf(options.log).Error!?Result {
|
|
1473
1513
|
const side = battle.side(player);
|
|
@@ -1477,33 +1517,32 @@ fn checkFaint(
|
|
|
1477
1517
|
const foe_fainted = foe.stored().hp == 0;
|
|
1478
1518
|
|
|
1479
1519
|
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;
|
|
1520
|
+
const foe_out = foe_fainted and findFirstAlive(foe) == 0;
|
|
1521
|
+
const more = player_out or foe_out;
|
|
1483
1522
|
|
|
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;
|
|
1523
|
+
if (try faint(battle, player, !(more or foe_fainted), cap, options)) |r| return r;
|
|
1524
|
+
if (foe_fainted) if (try faint(battle, player.foe(), !more, false, options)) |r| return r;
|
|
1486
1525
|
|
|
1487
1526
|
assert(!side.active.volatiles.MultiHit);
|
|
1488
1527
|
assert(!foe.active.volatiles.MultiHit);
|
|
1489
1528
|
|
|
1490
|
-
if (
|
|
1491
|
-
try options.log.tie();
|
|
1529
|
+
if (player_out and foe_out) {
|
|
1530
|
+
try options.log.tie(.{});
|
|
1492
1531
|
return Result.Tie;
|
|
1493
1532
|
} else if (player_out) {
|
|
1494
|
-
try options.log.win(player.foe());
|
|
1533
|
+
try options.log.win(.{player.foe()});
|
|
1495
1534
|
return if (player == .P1) Result.Lose else Result.Win;
|
|
1496
1535
|
} else if (foe_out) {
|
|
1497
|
-
try options.log.win(player);
|
|
1536
|
+
try options.log.win(.{player});
|
|
1498
1537
|
return if (player == .P1) Result.Win else Result.Lose;
|
|
1499
1538
|
}
|
|
1500
1539
|
|
|
1501
1540
|
const foe_choice: Choice.Type = if (foe_fainted) .Switch else .Pass;
|
|
1502
|
-
if (player == .P1) return
|
|
1503
|
-
return
|
|
1541
|
+
if (player == .P1) return .{ .p1 = .Switch, .p2 = foe_choice };
|
|
1542
|
+
return .{ .p1 = foe_choice, .p2 = .Switch };
|
|
1504
1543
|
}
|
|
1505
1544
|
|
|
1506
|
-
fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
|
|
1545
|
+
fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytype) !?Result {
|
|
1507
1546
|
var side = battle.side(player);
|
|
1508
1547
|
var foe = battle.foe(player);
|
|
1509
1548
|
assert(side.stored().hp == 0);
|
|
@@ -1512,7 +1551,7 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
|
|
|
1512
1551
|
assert(!foe_volatiles.MultiHit);
|
|
1513
1552
|
if (foe_volatiles.Bide) {
|
|
1514
1553
|
assert(!foe_volatiles.Thrashing and !foe_volatiles.Rage);
|
|
1515
|
-
foe_volatiles.state = if (
|
|
1554
|
+
foe_volatiles.state = if (pkmn.options.mod) 0 else foe_volatiles.state & 255;
|
|
1516
1555
|
if (foe_volatiles.state != 0) return Result.Error;
|
|
1517
1556
|
}
|
|
1518
1557
|
|
|
@@ -1536,8 +1575,10 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
|
|
|
1536
1575
|
side.stored().stats.spe;
|
|
1537
1576
|
}
|
|
1538
1577
|
|
|
1539
|
-
try options.log.faint(battle.active(player), done);
|
|
1540
|
-
|
|
1578
|
+
try options.log.faint(.{ battle.active(player), done });
|
|
1579
|
+
// We can only safely set capped if the faint occured as the direct result of an opponent's
|
|
1580
|
+
// move (as opposed to due to residual damage, recoil, Explosion, etc)
|
|
1581
|
+
if (cap) options.calc.capped(player);
|
|
1541
1582
|
return null;
|
|
1542
1583
|
}
|
|
1543
1584
|
|
|
@@ -1548,21 +1589,19 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
|
|
|
1548
1589
|
var volatiles = &side.active.volatiles;
|
|
1549
1590
|
|
|
1550
1591
|
const brn = Status.is(stored.status, .BRN);
|
|
1551
|
-
if (brn or Status.is(stored.status, .PSN))
|
|
1592
|
+
if (brn or Status.is(stored.status, .PSN)) {
|
|
1552
1593
|
var damage = @max(stored.stats.hp / 16, 1);
|
|
1553
|
-
|
|
1554
1594
|
if (volatiles.Toxic) {
|
|
1555
1595
|
volatiles.toxic += 1;
|
|
1556
1596
|
damage *= volatiles.toxic;
|
|
1557
1597
|
}
|
|
1598
|
+
damage = @min(damage, stored.hp);
|
|
1558
1599
|
|
|
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);
|
|
1600
|
+
if (!showdown or damage > 0) {
|
|
1601
|
+
stored.hp -= damage;
|
|
1602
|
+
// Pokémon Showdown uses damage [of] here but its not relevant in Generation I
|
|
1603
|
+
try options.log.damage(.{ ident, stored, if (brn) Damage.Burn else Damage.Poison });
|
|
1604
|
+
}
|
|
1566
1605
|
}
|
|
1567
1606
|
|
|
1568
1607
|
if (volatiles.LeechSeed) {
|
|
@@ -1586,14 +1625,14 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
|
|
|
1586
1625
|
const amount = @min(damage, stored.hp);
|
|
1587
1626
|
stored.hp -= amount;
|
|
1588
1627
|
|
|
1589
|
-
// As above, Pokémon Showdown uses
|
|
1590
|
-
if (amount > 0) try options.log.damage(ident, stored, .LeechSeed);
|
|
1628
|
+
// As above, Pokémon Showdown uses damage [of] but its not relevant
|
|
1629
|
+
if (amount > 0) try options.log.damage(.{ ident, stored, .LeechSeed });
|
|
1591
1630
|
|
|
1592
1631
|
const before = foe_stored.hp;
|
|
1593
1632
|
// Uncapped damage is added back to the foe
|
|
1594
1633
|
foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
|
|
1595
1634
|
// 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);
|
|
1635
|
+
if (foe_stored.hp > before) try options.log.heal(.{ foe_ident, foe_stored, .Silent });
|
|
1597
1636
|
}
|
|
1598
1637
|
}
|
|
1599
1638
|
|
|
@@ -1603,19 +1642,19 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
|
|
|
1603
1642
|
|
|
1604
1643
|
battle.turn += 1;
|
|
1605
1644
|
|
|
1606
|
-
if (
|
|
1607
|
-
try options.log.tie();
|
|
1645
|
+
if (pkmn.options.mod and pkmn.options.ebc and checkEBC(battle)) {
|
|
1646
|
+
try options.log.tie(.{});
|
|
1608
1647
|
return Result.Tie;
|
|
1609
1648
|
}
|
|
1610
1649
|
|
|
1611
|
-
if (
|
|
1612
|
-
try options.log.tie();
|
|
1650
|
+
if (pkmn.options.mod and battle.turn >= 1000) {
|
|
1651
|
+
try options.log.tie(.{});
|
|
1613
1652
|
return Result.Tie;
|
|
1614
1653
|
} else if (battle.turn >= 65535) {
|
|
1615
1654
|
return Result.Error;
|
|
1616
1655
|
}
|
|
1617
1656
|
|
|
1618
|
-
try options.log.turn(battle.turn);
|
|
1657
|
+
try options.log.turn(.{battle.turn});
|
|
1619
1658
|
|
|
1620
1659
|
return Result.Default;
|
|
1621
1660
|
}
|
|
@@ -1675,9 +1714,9 @@ fn checkEBC(battle: anytype) bool {
|
|
|
1675
1714
|
}
|
|
1676
1715
|
|
|
1677
1716
|
fn disabled(side: *Side, ident: ID, options: anytype) !bool {
|
|
1678
|
-
if (side.active.volatiles.
|
|
1717
|
+
if (side.active.volatiles.disable_move != 0) {
|
|
1679
1718
|
// A Pokémon that transforms after being disabled may end up with less move slots
|
|
1680
|
-
const m = side.active.moves[side.active.volatiles.
|
|
1719
|
+
const m = side.active.moves[side.active.volatiles.disable_move - 1].id;
|
|
1681
1720
|
// side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
|
|
1682
1721
|
const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
|
|
1683
1722
|
m
|
|
@@ -1685,32 +1724,21 @@ fn disabled(side: *Side, ident: ID, options: anytype) !bool {
|
|
|
1685
1724
|
side.last_selected_move;
|
|
1686
1725
|
if (m != .None and m == last) {
|
|
1687
1726
|
side.active.volatiles.Charging = false;
|
|
1688
|
-
try options.log.
|
|
1727
|
+
try options.log.cant(.{ ident, .Disable, last });
|
|
1689
1728
|
return true;
|
|
1690
1729
|
}
|
|
1691
1730
|
}
|
|
1692
1731
|
return false;
|
|
1693
1732
|
}
|
|
1694
1733
|
|
|
1695
|
-
|
|
1734
|
+
fn buildRage(battle: anytype, who: Player, options: anytype) !void {
|
|
1696
1735
|
const side = battle.side(who);
|
|
1697
1736
|
if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
|
|
1698
1737
|
try Effects.boost(battle, who, Move.get(.Rage), options);
|
|
1699
1738
|
}
|
|
1700
1739
|
}
|
|
1701
1740
|
|
|
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(
|
|
1741
|
+
fn onBegin(
|
|
1714
1742
|
battle: anytype,
|
|
1715
1743
|
player: Player,
|
|
1716
1744
|
move: Move.Data,
|
|
@@ -1718,7 +1746,6 @@ inline fn onBegin(
|
|
|
1718
1746
|
residual: *bool,
|
|
1719
1747
|
options: anytype,
|
|
1720
1748
|
) !void {
|
|
1721
|
-
assert(move.effect.onBegin());
|
|
1722
1749
|
return switch (move.effect) {
|
|
1723
1750
|
.Confusion => Effects.confusion(battle, player, move, options),
|
|
1724
1751
|
.Conversion => Effects.conversion(battle, player, options),
|
|
@@ -1740,8 +1767,7 @@ inline fn onBegin(
|
|
|
1740
1767
|
};
|
|
1741
1768
|
}
|
|
1742
1769
|
|
|
1743
|
-
|
|
1744
|
-
assert(move.effect.onEnd());
|
|
1770
|
+
fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1745
1771
|
return switch (move.effect) {
|
|
1746
1772
|
.Bide => Effects.bide(battle, player, options),
|
|
1747
1773
|
// zig fmt: off
|
|
@@ -1756,14 +1782,13 @@ inline fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anyty
|
|
|
1756
1782
|
};
|
|
1757
1783
|
}
|
|
1758
1784
|
|
|
1759
|
-
|
|
1785
|
+
fn alwaysHappens(
|
|
1760
1786
|
battle: anytype,
|
|
1761
1787
|
player: Player,
|
|
1762
1788
|
move: Move.Data,
|
|
1763
1789
|
residual: *bool,
|
|
1764
1790
|
options: anytype,
|
|
1765
1791
|
) !void {
|
|
1766
|
-
assert(move.effect.alwaysHappens());
|
|
1767
1792
|
return switch (move.effect) {
|
|
1768
1793
|
.DrainHP, .DreamEater => Effects.drainHP(battle, player, options),
|
|
1769
1794
|
.Explode => Effects.explode(battle, player),
|
|
@@ -1775,7 +1800,7 @@ inline fn alwaysHappens(
|
|
|
1775
1800
|
};
|
|
1776
1801
|
}
|
|
1777
1802
|
|
|
1778
|
-
|
|
1803
|
+
fn moveEffect(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1779
1804
|
return switch (move.effect) {
|
|
1780
1805
|
.BurnChance1, .BurnChance2 => Effects.burnChance(battle, player, move, options),
|
|
1781
1806
|
.ConfusionChance => Effects.confusion(battle, player, move, options),
|
|
@@ -1799,10 +1824,12 @@ pub const Effects = struct {
|
|
|
1799
1824
|
|
|
1800
1825
|
side.active.volatiles.Bide = true;
|
|
1801
1826
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1827
|
+
|
|
1802
1828
|
side.active.volatiles.state = 0;
|
|
1803
1829
|
side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
1830
|
+
options.chance.observe(.attacking, player, .started);
|
|
1804
1831
|
|
|
1805
|
-
try options.log.start(battle.active(player), .Bide);
|
|
1832
|
+
try options.log.start(.{ battle.active(player), .Bide });
|
|
1806
1833
|
}
|
|
1807
1834
|
|
|
1808
1835
|
fn burnChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -1816,11 +1843,11 @@ pub const Effects = struct {
|
|
|
1816
1843
|
// GLITCH: Freeze top move selection desync can occur if thawed Pokémon is slower
|
|
1817
1844
|
if (Status.is(foe_stored.status, .FRZ)) {
|
|
1818
1845
|
assert(move.type == .Fire);
|
|
1819
|
-
try options.log.curestatus(
|
|
1846
|
+
try options.log.curestatus(.{
|
|
1820
1847
|
battle.active(player.foe()),
|
|
1821
1848
|
foe_stored.status,
|
|
1822
1849
|
.Message,
|
|
1823
|
-
);
|
|
1850
|
+
});
|
|
1824
1851
|
foe_stored.status = 0;
|
|
1825
1852
|
}
|
|
1826
1853
|
return;
|
|
@@ -1834,7 +1861,7 @@ pub const Effects = struct {
|
|
|
1834
1861
|
foe_stored.status = Status.init(.BRN);
|
|
1835
1862
|
foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
|
|
1836
1863
|
|
|
1837
|
-
try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
|
|
1864
|
+
try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
|
|
1838
1865
|
}
|
|
1839
1866
|
|
|
1840
1867
|
fn charge(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -1844,8 +1871,8 @@ pub const Effects = struct {
|
|
|
1844
1871
|
volatiles.Charging = true;
|
|
1845
1872
|
const move = side.last_selected_move;
|
|
1846
1873
|
if (move == .Fly or move == .Dig) volatiles.Invulnerable = true;
|
|
1847
|
-
try options.log.laststill();
|
|
1848
|
-
try options.log.prepare(battle.active(player), move);
|
|
1874
|
+
try options.log.laststill(.{});
|
|
1875
|
+
try options.log.prepare(.{ battle.active(player), move });
|
|
1849
1876
|
}
|
|
1850
1877
|
|
|
1851
1878
|
fn confusion(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -1856,42 +1883,39 @@ pub const Effects = struct {
|
|
|
1856
1883
|
if (!try Rolls.confusionChance(battle, player, options)) return;
|
|
1857
1884
|
} else {
|
|
1858
1885
|
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
|
-
}
|
|
1886
|
+
if (!try checkHit(battle, player, move, options)) return;
|
|
1887
|
+
if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
1864
1888
|
} else {
|
|
1865
|
-
if (sub) {
|
|
1866
|
-
|
|
1867
|
-
} else if (!try checkHit(battle, player, move, options)) {
|
|
1868
|
-
return;
|
|
1869
|
-
}
|
|
1889
|
+
if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
1890
|
+
if (!try checkHit(battle, player, move, options)) return;
|
|
1870
1891
|
}
|
|
1871
|
-
try options.chance.commit(player, .hit);
|
|
1872
1892
|
}
|
|
1873
1893
|
|
|
1874
1894
|
if (foe.active.volatiles.Confusion) return;
|
|
1875
1895
|
foe.active.volatiles.Confusion = true;
|
|
1876
|
-
foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
|
|
1877
1896
|
|
|
1878
|
-
|
|
1897
|
+
foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
1898
|
+
options.chance.observe(.confusion, player.foe(), .started);
|
|
1899
|
+
options.calc.confusion(player.foe());
|
|
1900
|
+
|
|
1901
|
+
try options.log.start(.{ battle.active(player.foe()), .Confusion });
|
|
1879
1902
|
}
|
|
1880
1903
|
|
|
1881
1904
|
fn conversion(battle: anytype, player: Player, options: anytype) !void {
|
|
1882
1905
|
const foe = battle.foe(player);
|
|
1883
1906
|
|
|
1884
1907
|
if (foe.active.volatiles.Invulnerable) {
|
|
1885
|
-
try options.log.lastmiss();
|
|
1886
|
-
return options.log.miss(battle.active(player));
|
|
1908
|
+
try options.log.lastmiss(.{});
|
|
1909
|
+
return options.log.miss(.{battle.active(player)});
|
|
1887
1910
|
}
|
|
1888
1911
|
|
|
1889
1912
|
battle.side(player).active.types = foe.active.types;
|
|
1890
|
-
return options.log.
|
|
1913
|
+
return options.log.start(.{
|
|
1891
1914
|
battle.active(player),
|
|
1915
|
+
.TypeChange,
|
|
1892
1916
|
foe.active.types,
|
|
1893
1917
|
battle.active(player.foe()),
|
|
1894
|
-
);
|
|
1918
|
+
});
|
|
1895
1919
|
}
|
|
1896
1920
|
|
|
1897
1921
|
fn disable(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
|
|
@@ -1900,13 +1924,10 @@ pub const Effects = struct {
|
|
|
1900
1924
|
const foe_ident = battle.active(player.foe());
|
|
1901
1925
|
|
|
1902
1926
|
// 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
|
-
}
|
|
1927
|
+
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
1907
1928
|
|
|
1908
|
-
if (volatiles.
|
|
1909
|
-
try options.log.fail(foe_ident, .None);
|
|
1929
|
+
if (volatiles.disable_move != 0) {
|
|
1930
|
+
try options.log.fail(.{ foe_ident, .None });
|
|
1910
1931
|
return null;
|
|
1911
1932
|
}
|
|
1912
1933
|
|
|
@@ -1921,27 +1942,27 @@ pub const Effects = struct {
|
|
|
1921
1942
|
|
|
1922
1943
|
// Technically this is still considered simply a "miss" on the cartridge,
|
|
1923
1944
|
// but diverging from Pokémon Showdown here would mostly just be pedantic
|
|
1924
|
-
if (n == 0) {
|
|
1925
|
-
try options.log.fail(foe_ident, .None);
|
|
1945
|
+
if (n == 0 or (pkmn.options.mod and err)) {
|
|
1946
|
+
try options.log.fail(.{ foe_ident, .None });
|
|
1926
1947
|
return null;
|
|
1927
1948
|
} else if (err) {
|
|
1928
1949
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
1929
|
-
assert(!showdown);
|
|
1930
1950
|
return Result.Error;
|
|
1931
1951
|
}
|
|
1932
1952
|
|
|
1933
|
-
volatiles.
|
|
1953
|
+
volatiles.disable_move =
|
|
1934
1954
|
@intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
|
|
1935
|
-
volatiles.
|
|
1955
|
+
volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
|
|
1956
|
+
options.chance.observe(.disable, player.foe(), .started);
|
|
1936
1957
|
|
|
1937
|
-
const id = foe.active.move(volatiles.
|
|
1938
|
-
try options.log.
|
|
1958
|
+
const id = foe.active.move(volatiles.disable_move).id;
|
|
1959
|
+
try options.log.start(.{ foe_ident, .Disable, id });
|
|
1939
1960
|
return null;
|
|
1940
1961
|
}
|
|
1941
1962
|
|
|
1942
1963
|
fn drainHP(battle: anytype, player: Player, options: anytype) !void {
|
|
1943
|
-
var
|
|
1944
|
-
|
|
1964
|
+
var stored = battle.side(player).stored();
|
|
1965
|
+
const ident = battle.active(player);
|
|
1945
1966
|
|
|
1946
1967
|
if (battle.last_damage == 0) {
|
|
1947
1968
|
assert(showdown);
|
|
@@ -1954,7 +1975,7 @@ pub const Effects = struct {
|
|
|
1954
1975
|
if (stored.hp == stored.stats.hp) return;
|
|
1955
1976
|
stored.hp = @min(stored.stats.hp, stored.hp + drain);
|
|
1956
1977
|
|
|
1957
|
-
try options.log.
|
|
1978
|
+
try options.log.heal(.{ ident, stored, .Drain, battle.active(player.foe()) });
|
|
1958
1979
|
}
|
|
1959
1980
|
|
|
1960
1981
|
fn explode(battle: anytype, player: Player) !void {
|
|
@@ -1986,7 +2007,7 @@ pub const Effects = struct {
|
|
|
1986
2007
|
if (side.active.volatiles.FocusEnergy) return;
|
|
1987
2008
|
side.active.volatiles.FocusEnergy = true;
|
|
1988
2009
|
|
|
1989
|
-
try options.log.start(battle.active(player), .FocusEnergy);
|
|
2010
|
+
try options.log.start(.{ battle.active(player), .FocusEnergy });
|
|
1990
2011
|
}
|
|
1991
2012
|
|
|
1992
2013
|
fn freezeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2003,12 +2024,12 @@ pub const Effects = struct {
|
|
|
2003
2024
|
if (foe.active.types.includes(move.type)) return;
|
|
2004
2025
|
if (!try Rolls.secondaryChance(battle, player, true, options)) return;
|
|
2005
2026
|
// Freeze Clause Mod
|
|
2006
|
-
if (
|
|
2027
|
+
if (pkmn.options.mod) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
2007
2028
|
|
|
2008
2029
|
foe_stored.status = Status.init(.FRZ);
|
|
2009
2030
|
// GLITCH: Hyper Beam recharging status is not cleared
|
|
2010
2031
|
|
|
2011
|
-
try options.log.status(foe_ident, foe_stored.status, .None);
|
|
2032
|
+
try options.log.status(.{ foe_ident, foe_stored.status, .None });
|
|
2012
2033
|
}
|
|
2013
2034
|
|
|
2014
2035
|
fn haze(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -2016,7 +2037,7 @@ pub const Effects = struct {
|
|
|
2016
2037
|
var side = battle.side(player);
|
|
2017
2038
|
var foe = battle.foe(player);
|
|
2018
2039
|
|
|
2019
|
-
|
|
2040
|
+
const side_stored = side.stored();
|
|
2020
2041
|
var foe_stored = foe.stored();
|
|
2021
2042
|
|
|
2022
2043
|
const player_ident = battle.active(player);
|
|
@@ -2028,8 +2049,8 @@ pub const Effects = struct {
|
|
|
2028
2049
|
side.active.stats = side_stored.stats;
|
|
2029
2050
|
foe.active.stats = foe_stored.stats;
|
|
2030
2051
|
|
|
2031
|
-
try log.activate(player_ident, .Haze);
|
|
2032
|
-
try log.clearallboost();
|
|
2052
|
+
try log.activate(.{ player_ident, .Haze });
|
|
2053
|
+
try log.clearallboost(.{});
|
|
2033
2054
|
|
|
2034
2055
|
// Pokémon Showdown clears P1 then P2 instead of status -> side -> foe
|
|
2035
2056
|
if (showdown) {
|
|
@@ -2037,24 +2058,26 @@ pub const Effects = struct {
|
|
|
2037
2058
|
const p: Player = @enumFromInt(i);
|
|
2038
2059
|
// Pokémon Showdown incorrectly does not prevent sleep/freeze from moving
|
|
2039
2060
|
if (p != player and Status.any(s.stored().status)) {
|
|
2040
|
-
try log.curestatus(foe_ident, foe_stored.status, .Silent);
|
|
2061
|
+
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
2041
2062
|
s.stored().status = 0;
|
|
2063
|
+
options.chance.observe(.sleep, p, .None);
|
|
2042
2064
|
} else if (showdown and s.stored().status == Status.TOX) {
|
|
2043
2065
|
s.stored().status = Status.init(.PSN);
|
|
2044
|
-
try log.status(battle.active(p), s.stored().status, .None);
|
|
2066
|
+
try log.status(.{ battle.active(p), s.stored().status, .None });
|
|
2045
2067
|
}
|
|
2046
|
-
try clearVolatiles(battle, p,
|
|
2068
|
+
try clearVolatiles(battle, p, options);
|
|
2047
2069
|
}
|
|
2048
2070
|
} else {
|
|
2049
2071
|
if (Status.any(foe_stored.status)) {
|
|
2050
2072
|
if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
|
|
2051
2073
|
foe.last_selected_move = .SKIP_TURN;
|
|
2074
|
+
options.chance.observe(.sleep, player.foe(), .None);
|
|
2052
2075
|
}
|
|
2053
|
-
try log.curestatus(foe_ident, foe_stored.status, .Silent);
|
|
2076
|
+
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
2054
2077
|
foe_stored.status = 0;
|
|
2055
2078
|
}
|
|
2056
|
-
try clearVolatiles(battle, player,
|
|
2057
|
-
try clearVolatiles(battle, player.foe(),
|
|
2079
|
+
try clearVolatiles(battle, player, options);
|
|
2080
|
+
try clearVolatiles(battle, player.foe(), options);
|
|
2058
2081
|
}
|
|
2059
2082
|
}
|
|
2060
2083
|
|
|
@@ -2066,7 +2089,7 @@ pub const Effects = struct {
|
|
|
2066
2089
|
// GLITCH: HP recovery move failure glitches
|
|
2067
2090
|
const delta = stored.stats.hp - stored.hp;
|
|
2068
2091
|
if (delta == 0 or (delta & 255 == 255 and stored.hp % 256 != 0)) {
|
|
2069
|
-
return
|
|
2092
|
+
return options.log.fail(.{ ident, .None });
|
|
2070
2093
|
}
|
|
2071
2094
|
|
|
2072
2095
|
const rest = side.last_selected_move == .Rest;
|
|
@@ -2074,17 +2097,17 @@ pub const Effects = struct {
|
|
|
2074
2097
|
// Adding the sleep status runs the sleep condition handler to roll duration
|
|
2075
2098
|
if (showdown) battle.rng.advance(1);
|
|
2076
2099
|
stored.status = Status.slf(2);
|
|
2077
|
-
try options.log.
|
|
2100
|
+
try options.log.status(.{ ident, stored.status, .From, Move.Rest });
|
|
2078
2101
|
stored.hp = stored.stats.hp;
|
|
2079
2102
|
} else {
|
|
2080
2103
|
stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
|
|
2081
2104
|
}
|
|
2082
|
-
try options.log.heal(ident, stored, if (rest) Heal.Silent else Heal.None);
|
|
2105
|
+
try options.log.heal(.{ ident, stored, if (rest) Heal.Silent else Heal.None });
|
|
2083
2106
|
}
|
|
2084
2107
|
|
|
2085
2108
|
fn hyperBeam(battle: anytype, player: Player, options: anytype) !void {
|
|
2086
2109
|
battle.side(player).active.volatiles.Recharging = true;
|
|
2087
|
-
try options.log.mustrecharge(battle.active(player));
|
|
2110
|
+
try options.log.mustrecharge(.{battle.active(player)});
|
|
2088
2111
|
}
|
|
2089
2112
|
|
|
2090
2113
|
fn leechSeed(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2093,33 +2116,31 @@ pub const Effects = struct {
|
|
|
2093
2116
|
if (showdown) {
|
|
2094
2117
|
// Invulnerability trumps type immunity on Pokémon Showdown
|
|
2095
2118
|
if (!foe.active.volatiles.Invulnerable and foe.active.types.includes(.Grass)) {
|
|
2096
|
-
return options.log.immune(battle.active(player.foe()), .None);
|
|
2119
|
+
return options.log.immune(.{ battle.active(player.foe()), .None });
|
|
2097
2120
|
}
|
|
2098
2121
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2099
2122
|
if (foe.active.volatiles.LeechSeed) return;
|
|
2100
2123
|
} else {
|
|
2101
2124
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2102
2125
|
if (foe.active.types.includes(.Grass) or foe.active.volatiles.LeechSeed) {
|
|
2103
|
-
try options.log.lastmiss();
|
|
2104
|
-
return options.log.miss(battle.active(player));
|
|
2126
|
+
try options.log.lastmiss(.{});
|
|
2127
|
+
return options.log.miss(.{battle.active(player)});
|
|
2105
2128
|
}
|
|
2106
2129
|
}
|
|
2107
2130
|
|
|
2108
|
-
try options.chance.commit(player, .hit);
|
|
2109
2131
|
foe.active.volatiles.LeechSeed = true;
|
|
2110
2132
|
|
|
2111
|
-
try options.log.start(battle.active(player.foe()), .LeechSeed);
|
|
2133
|
+
try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
|
|
2112
2134
|
}
|
|
2113
2135
|
|
|
2114
2136
|
fn lightScreen(battle: anytype, player: Player, options: anytype) !void {
|
|
2115
2137
|
var side = battle.side(player);
|
|
2138
|
+
const ident = battle.active(player);
|
|
2116
2139
|
|
|
2117
|
-
if (side.active.volatiles.LightScreen) {
|
|
2118
|
-
return options.log.fail(battle.active(player), .None);
|
|
2119
|
-
}
|
|
2140
|
+
if (side.active.volatiles.LightScreen) return options.log.fail(.{ ident, .None });
|
|
2120
2141
|
side.active.volatiles.LightScreen = true;
|
|
2121
2142
|
|
|
2122
|
-
try options.log.start(
|
|
2143
|
+
try options.log.start(.{ ident, .LightScreen });
|
|
2123
2144
|
}
|
|
2124
2145
|
|
|
2125
2146
|
fn mimic(battle: anytype, player: Player, move: Move.Data, mslot: u8, options: anytype) !void {
|
|
@@ -2147,17 +2168,15 @@ pub const Effects = struct {
|
|
|
2147
2168
|
if (!has_mimic) {
|
|
2148
2169
|
// Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
|
|
2149
2170
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2150
|
-
|
|
2151
|
-
return try options.log.fail(battle.active(player.foe()), .None);
|
|
2171
|
+
return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
2152
2172
|
}
|
|
2153
2173
|
}
|
|
2154
2174
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2155
|
-
try options.chance.commit(player, .hit);
|
|
2156
2175
|
|
|
2157
2176
|
const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
|
|
2158
2177
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
2159
2178
|
|
|
2160
|
-
try options.log.
|
|
2179
|
+
try options.log.start(.{ battle.active(player), .Mimic, side.active.move(oslot).id });
|
|
2161
2180
|
}
|
|
2162
2181
|
|
|
2163
2182
|
fn mist(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -2166,7 +2185,7 @@ pub const Effects = struct {
|
|
|
2166
2185
|
if (side.active.volatiles.Mist) return;
|
|
2167
2186
|
side.active.volatiles.Mist = true;
|
|
2168
2187
|
|
|
2169
|
-
try options.log.start(battle.active(player), .Mist);
|
|
2188
|
+
try options.log.start(.{ battle.active(player), .Mist });
|
|
2170
2189
|
}
|
|
2171
2190
|
|
|
2172
2191
|
fn multiHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2192,25 +2211,25 @@ pub const Effects = struct {
|
|
|
2192
2211
|
|
|
2193
2212
|
if (showdown) {
|
|
2194
2213
|
// Invulnerability trumps type immunity on Pokémon Showdown
|
|
2195
|
-
if (immune and !foe.active.volatiles.Invulnerable)
|
|
2196
|
-
|
|
2214
|
+
if (immune and !foe.active.volatiles.Invulnerable) {
|
|
2215
|
+
return log.immune(.{ foe_ident, .None });
|
|
2216
|
+
} else if (!try checkHit(battle, player, move, options)) return;
|
|
2197
2217
|
}
|
|
2198
2218
|
if (Status.any(foe_stored.status)) {
|
|
2199
|
-
return log.fail(
|
|
2219
|
+
return log.fail(.{
|
|
2200
2220
|
foe_ident,
|
|
2201
|
-
if (Status.is(foe_stored.status, .PAR)) .Paralysis else .None,
|
|
2202
|
-
);
|
|
2221
|
+
if (Status.is(foe_stored.status, .PAR)) Fail.Paralysis else Fail.None,
|
|
2222
|
+
});
|
|
2203
2223
|
}
|
|
2204
2224
|
if (!showdown) {
|
|
2205
|
-
if (immune) return log.immune(foe_ident, .None);
|
|
2225
|
+
if (immune) return log.immune(.{ foe_ident, .None });
|
|
2206
2226
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2207
2227
|
}
|
|
2208
|
-
try options.chance.commit(player, .hit);
|
|
2209
2228
|
|
|
2210
2229
|
foe_stored.status = Status.init(.PAR);
|
|
2211
2230
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
2212
2231
|
|
|
2213
|
-
try log.status(foe_ident, foe_stored.status, .None);
|
|
2232
|
+
try log.status(.{ foe_ident, foe_stored.status, .None });
|
|
2214
2233
|
}
|
|
2215
2234
|
|
|
2216
2235
|
fn paralyzeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2232,12 +2251,12 @@ pub const Effects = struct {
|
|
|
2232
2251
|
foe_stored.status = Status.init(.PAR);
|
|
2233
2252
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
2234
2253
|
|
|
2235
|
-
try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
|
|
2254
|
+
try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
|
|
2236
2255
|
}
|
|
2237
2256
|
|
|
2238
2257
|
fn payDay(battle: anytype, player: Player, options: anytype) !void {
|
|
2239
2258
|
if (!showdown or !battle.foe(player).active.volatiles.Substitute) {
|
|
2240
|
-
try options.log.fieldactivate();
|
|
2259
|
+
try options.log.fieldactivate(.{});
|
|
2241
2260
|
}
|
|
2242
2261
|
}
|
|
2243
2262
|
|
|
@@ -2252,28 +2271,27 @@ pub const Effects = struct {
|
|
|
2252
2271
|
return;
|
|
2253
2272
|
} else if (foe.active.volatiles.Substitute) {
|
|
2254
2273
|
if (move.effect != .Poison) return;
|
|
2255
|
-
return log.fail(foe_ident, .None);
|
|
2274
|
+
return log.fail(.{ foe_ident, .None });
|
|
2256
2275
|
} else if (Status.any(foe_stored.status)) {
|
|
2257
2276
|
if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
|
|
2258
2277
|
// Pokémon Showdown considers Toxic to be a status even in Generation I and so
|
|
2259
2278
|
// 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))
|
|
2279
|
+
return log.fail(.{ foe_ident, if (Status.is(foe_stored.status, .PSN))
|
|
2261
2280
|
if (!showdown)
|
|
2262
|
-
.Poison
|
|
2281
|
+
Fail.Poison
|
|
2263
2282
|
else if (toxic == (foe_stored.status == Status.TOX))
|
|
2264
|
-
if (toxic) .Toxic else .Poison
|
|
2283
|
+
if (toxic) Fail.Toxic else Fail.Poison
|
|
2265
2284
|
else
|
|
2266
|
-
.None
|
|
2285
|
+
Fail.None
|
|
2267
2286
|
else
|
|
2268
|
-
.None);
|
|
2287
|
+
Fail.None });
|
|
2269
2288
|
} else if (foe.active.types.includes(.Poison)) {
|
|
2270
2289
|
if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
|
|
2271
|
-
return log.immune(foe_ident, .None);
|
|
2290
|
+
return log.immune(.{ foe_ident, .None });
|
|
2272
2291
|
}
|
|
2273
2292
|
|
|
2274
2293
|
if (move.effect == .Poison) {
|
|
2275
2294
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2276
|
-
try options.chance.commit(player, .hit);
|
|
2277
2295
|
} else {
|
|
2278
2296
|
if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
|
|
2279
2297
|
return;
|
|
@@ -2287,7 +2305,7 @@ pub const Effects = struct {
|
|
|
2287
2305
|
foe.active.volatiles.toxic = 0;
|
|
2288
2306
|
}
|
|
2289
2307
|
|
|
2290
|
-
try log.status(foe_ident, foe_stored.status, .None);
|
|
2308
|
+
try log.status(.{ foe_ident, foe_stored.status, .None });
|
|
2291
2309
|
}
|
|
2292
2310
|
|
|
2293
2311
|
fn rage(battle: anytype, player: Player) !void {
|
|
@@ -2307,22 +2325,23 @@ pub const Effects = struct {
|
|
|
2307
2325
|
@as(u8, if (side.last_selected_move == .Struggle) 2 else 4), 1));
|
|
2308
2326
|
stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
|
|
2309
2327
|
|
|
2310
|
-
try options.log.
|
|
2328
|
+
try options.log.damage(.{
|
|
2311
2329
|
battle.active(player),
|
|
2312
2330
|
stored,
|
|
2313
2331
|
.RecoilOf,
|
|
2314
2332
|
battle.active(player.foe()),
|
|
2315
|
-
);
|
|
2333
|
+
});
|
|
2316
2334
|
if (showdown and stored.hp == 0) residual.* = false;
|
|
2317
2335
|
}
|
|
2318
2336
|
|
|
2319
2337
|
fn reflect(battle: anytype, player: Player, options: anytype) !void {
|
|
2320
2338
|
var side = battle.side(player);
|
|
2339
|
+
const ident = battle.active(player);
|
|
2321
2340
|
|
|
2322
|
-
if (side.active.volatiles.Reflect) return options.log.fail(
|
|
2341
|
+
if (side.active.volatiles.Reflect) return options.log.fail(.{ ident, .None });
|
|
2323
2342
|
side.active.volatiles.Reflect = true;
|
|
2324
2343
|
|
|
2325
|
-
try options.log.start(
|
|
2344
|
+
try options.log.start(.{ ident, .Reflect });
|
|
2326
2345
|
}
|
|
2327
2346
|
|
|
2328
2347
|
fn sleep(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2336,18 +2355,17 @@ pub const Effects = struct {
|
|
|
2336
2355
|
// the Sleep Clause Mod might activate, causing us to not actually bypass
|
|
2337
2356
|
} else {
|
|
2338
2357
|
if (Status.any(foe_stored.status)) {
|
|
2339
|
-
return options.log.fail(
|
|
2358
|
+
return options.log.fail(.{
|
|
2340
2359
|
foe_ident,
|
|
2341
|
-
if (Status.is(foe_stored.status, .SLP)) .Sleep else .None,
|
|
2342
|
-
);
|
|
2360
|
+
if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
|
|
2361
|
+
});
|
|
2343
2362
|
}
|
|
2344
|
-
// If
|
|
2363
|
+
// If moveHit in doMove didn't return true Pokémon Showdown wouldn't be in here
|
|
2345
2364
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2346
|
-
try options.chance.commit(player, .hit);
|
|
2347
2365
|
}
|
|
2348
2366
|
|
|
2349
2367
|
// Sleep Clause Mod
|
|
2350
|
-
if (
|
|
2368
|
+
if (pkmn.options.mod) {
|
|
2351
2369
|
for (foe.pokemon) |p| {
|
|
2352
2370
|
if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
|
|
2353
2371
|
}
|
|
@@ -2355,19 +2373,20 @@ pub const Effects = struct {
|
|
|
2355
2373
|
foe.active.volatiles.Recharging = false;
|
|
2356
2374
|
|
|
2357
2375
|
foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
|
|
2376
|
+
options.chance.observe(.sleep, player.foe(), .started);
|
|
2377
|
+
|
|
2358
2378
|
const last = battle.side(player).last_selected_move;
|
|
2359
|
-
try options.log.
|
|
2379
|
+
try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
|
|
2360
2380
|
}
|
|
2361
2381
|
|
|
2362
2382
|
fn splash(battle: anytype, player: Player, options: anytype) !void {
|
|
2363
|
-
try options.log.activate(battle.active(player), .Splash);
|
|
2383
|
+
try options.log.activate(.{ battle.active(player), .Splash });
|
|
2364
2384
|
}
|
|
2365
2385
|
|
|
2366
2386
|
fn substitute(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
|
|
2367
2387
|
var side = battle.side(player);
|
|
2368
2388
|
if (side.active.volatiles.Substitute) {
|
|
2369
|
-
|
|
2370
|
-
return;
|
|
2389
|
+
return options.log.fail(.{ battle.active(player), .Substitute });
|
|
2371
2390
|
}
|
|
2372
2391
|
|
|
2373
2392
|
assert(side.stored().stats.hp <= 1023);
|
|
@@ -2379,28 +2398,30 @@ pub const Effects = struct {
|
|
|
2379
2398
|
// avoid having to convert to floating point)
|
|
2380
2399
|
const required_hp = if (showdown) @divFloor(side.stored().stats.hp - 1, 4) + 1 else hp;
|
|
2381
2400
|
if (side.stored().hp < required_hp) {
|
|
2382
|
-
|
|
2383
|
-
return;
|
|
2401
|
+
return options.log.fail(.{ battle.active(player), .Weak });
|
|
2384
2402
|
}
|
|
2385
2403
|
|
|
2386
2404
|
// GLITCH: can leave the user with 0 HP (faints later) because didn't check '<=' above
|
|
2387
2405
|
side.stored().hp -= hp;
|
|
2388
2406
|
side.active.volatiles.substitute = hp + 1;
|
|
2389
2407
|
side.active.volatiles.Substitute = true;
|
|
2390
|
-
try options.log.start(battle.active(player), .Substitute);
|
|
2408
|
+
try options.log.start(.{ battle.active(player), .Substitute });
|
|
2391
2409
|
if (hp > 0) {
|
|
2392
|
-
try options.log.damage(battle.active(player), side.stored(), .None);
|
|
2410
|
+
try options.log.damage(.{ battle.active(player), side.stored(), .None });
|
|
2393
2411
|
if (showdown and side.stored().hp == 0) residual.* = false;
|
|
2394
2412
|
}
|
|
2395
2413
|
}
|
|
2396
2414
|
|
|
2397
2415
|
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
2416
|
// Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
|
|
2401
|
-
if (
|
|
2402
|
-
|
|
2403
|
-
|
|
2417
|
+
if (showdown) {
|
|
2418
|
+
if (battle.side(player).last_selected_move == .Teleport) return;
|
|
2419
|
+
_ = try checkHit(battle, player, move, options);
|
|
2420
|
+
battle.last_damage = 0;
|
|
2421
|
+
} else {
|
|
2422
|
+
try options.log.fail(.{ battle.active(player), .None });
|
|
2423
|
+
try options.log.laststill(.{});
|
|
2424
|
+
}
|
|
2404
2425
|
}
|
|
2405
2426
|
|
|
2406
2427
|
fn thrashing(battle: anytype, player: Player, options: anytype) void {
|
|
@@ -2410,6 +2431,8 @@ pub const Effects = struct {
|
|
|
2410
2431
|
|
|
2411
2432
|
volatiles.Thrashing = true;
|
|
2412
2433
|
volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
2434
|
+
|
|
2435
|
+
options.chance.observe(.attacking, player, .started);
|
|
2413
2436
|
}
|
|
2414
2437
|
|
|
2415
2438
|
fn transform(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -2438,7 +2461,7 @@ pub const Effects = struct {
|
|
|
2438
2461
|
side.active.moves[i].pp = if (m.id != .None) 5 else 0;
|
|
2439
2462
|
}
|
|
2440
2463
|
|
|
2441
|
-
try options.log.transform(battle.active(player), foe_ident);
|
|
2464
|
+
try options.log.transform(.{ battle.active(player), foe_ident });
|
|
2442
2465
|
}
|
|
2443
2466
|
|
|
2444
2467
|
fn binding(battle: anytype, player: Player, rewrap: bool, options: anytype) !void {
|
|
@@ -2448,13 +2471,14 @@ pub const Effects = struct {
|
|
|
2448
2471
|
if (side.active.volatiles.Binding) return;
|
|
2449
2472
|
side.active.volatiles.Binding = true;
|
|
2450
2473
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2451
|
-
// (Pokémon Showdown
|
|
2474
|
+
// (Pokémon Showdown clears Recharging in onTryMove instead)
|
|
2452
2475
|
if (!showdown) {
|
|
2453
2476
|
foe.active.volatiles.Recharging = false;
|
|
2454
2477
|
} else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
|
|
2455
2478
|
|
|
2456
2479
|
side.active.volatiles.attacks =
|
|
2457
2480
|
try Rolls.distribution(battle, .Binding, player, options) - 1;
|
|
2481
|
+
options.chance.observe(.binding, player, .started);
|
|
2458
2482
|
}
|
|
2459
2483
|
|
|
2460
2484
|
fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2468,87 +2492,87 @@ pub const Effects = struct {
|
|
|
2468
2492
|
switch (move.effect) {
|
|
2469
2493
|
.AttackUp1, .AttackUp2, .Rage => {
|
|
2470
2494
|
assert(boosts.atk >= -6 and boosts.atk <= 6);
|
|
2471
|
-
if (boosts.atk == 6) return
|
|
2495
|
+
if (boosts.atk == 6) return log.fail(.{ ident, .None });
|
|
2472
2496
|
const n: u2 = if (move.effect == .AttackUp2) 2 else 1;
|
|
2473
2497
|
boosts.atk = @as(i4, @intCast(@min(6, @as(i8, boosts.atk) + n)));
|
|
2474
|
-
const reason = if (move.effect == .Rage)
|
|
2498
|
+
const reason: Boost = if (move.effect == .Rage) .Rage else .Attack;
|
|
2475
2499
|
if (stats.atk == MAX_STAT_VALUE) {
|
|
2476
2500
|
boosts.atk -= 1;
|
|
2477
2501
|
if (showdown) {
|
|
2478
|
-
try log.boost(ident, reason, n);
|
|
2479
|
-
try log.boost(ident,
|
|
2502
|
+
try log.boost(.{ ident, reason, n });
|
|
2503
|
+
try log.boost(.{ ident, .Attack, -1 });
|
|
2480
2504
|
if (move.effect == .Rage) return;
|
|
2481
2505
|
}
|
|
2482
|
-
return
|
|
2506
|
+
return log.fail(.{ ident, .None });
|
|
2483
2507
|
}
|
|
2484
|
-
|
|
2508
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
|
|
2485
2509
|
const stat = unmodifiedStats(battle, side).atk;
|
|
2486
2510
|
stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2487
|
-
try log.boost(ident, reason, n);
|
|
2511
|
+
try log.boost(.{ ident, reason, n });
|
|
2488
2512
|
// Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
|
|
2489
2513
|
if (showdown and move.effect == .Rage) return;
|
|
2490
2514
|
},
|
|
2491
2515
|
.DefenseUp1, .DefenseUp2 => {
|
|
2492
2516
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
2493
|
-
if (boosts.def == 6) return
|
|
2517
|
+
if (boosts.def == 6) return log.fail(.{ ident, .None });
|
|
2494
2518
|
const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
|
|
2495
2519
|
boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
|
|
2496
2520
|
if (stats.def == MAX_STAT_VALUE) {
|
|
2497
2521
|
boosts.def -= 1;
|
|
2498
2522
|
if (showdown) {
|
|
2499
|
-
try log.boost(ident, .Defense, n);
|
|
2500
|
-
try log.boost(ident, .Defense, -1);
|
|
2523
|
+
try log.boost(.{ ident, .Defense, n });
|
|
2524
|
+
try log.boost(.{ ident, .Defense, -1 });
|
|
2501
2525
|
}
|
|
2502
|
-
return
|
|
2526
|
+
return log.fail(.{ ident, .None });
|
|
2503
2527
|
}
|
|
2504
|
-
|
|
2528
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
|
|
2505
2529
|
const stat = unmodifiedStats(battle, side).def;
|
|
2506
2530
|
stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2507
|
-
try log.boost(ident, .Defense, n);
|
|
2531
|
+
try log.boost(.{ ident, .Defense, n });
|
|
2508
2532
|
},
|
|
2509
2533
|
.SpeedUp2 => {
|
|
2510
2534
|
assert(boosts.spe >= -6 and boosts.spe <= 6);
|
|
2511
|
-
if (boosts.spe == 6) return
|
|
2535
|
+
if (boosts.spe == 6) return log.fail(.{ ident, .None });
|
|
2512
2536
|
boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
|
|
2513
2537
|
if (stats.spe == MAX_STAT_VALUE) {
|
|
2514
2538
|
boosts.spe -= 1;
|
|
2515
2539
|
if (showdown) {
|
|
2516
|
-
try log.boost(ident, .Speed, 2);
|
|
2517
|
-
try log.boost(ident, .Speed, -1);
|
|
2540
|
+
try log.boost(.{ ident, .Speed, 2 });
|
|
2541
|
+
try log.boost(.{ ident, .Speed, -1 });
|
|
2518
2542
|
}
|
|
2519
|
-
return
|
|
2543
|
+
return log.fail(.{ ident, .None });
|
|
2520
2544
|
}
|
|
2521
|
-
|
|
2545
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
|
|
2522
2546
|
const stat = unmodifiedStats(battle, side).spe;
|
|
2523
2547
|
stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2524
|
-
try log.boost(ident, .Speed, 2);
|
|
2548
|
+
try log.boost(.{ ident, .Speed, 2 });
|
|
2525
2549
|
},
|
|
2526
2550
|
.SpecialUp1, .SpecialUp2 => {
|
|
2527
2551
|
assert(boosts.spc >= -6 and boosts.spc <= 6);
|
|
2528
|
-
if (boosts.spc == 6) return
|
|
2552
|
+
if (boosts.spc == 6) return log.fail(.{ ident, .None });
|
|
2529
2553
|
const n: u2 = if (move.effect == .SpecialUp2) 2 else 1;
|
|
2530
2554
|
boosts.spc = @intCast(@min(6, @as(i8, boosts.spc) + n));
|
|
2531
2555
|
if (stats.spc == MAX_STAT_VALUE) {
|
|
2532
2556
|
boosts.spc -= 1;
|
|
2533
2557
|
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);
|
|
2558
|
+
try log.boost(.{ ident, .SpecialAttack, n });
|
|
2559
|
+
try log.boost(.{ ident, .SpecialAttack, -1 });
|
|
2560
|
+
try log.boost(.{ ident, .SpecialDefense, n });
|
|
2561
|
+
try log.boost(.{ ident, .SpecialDefense, -1 });
|
|
2538
2562
|
}
|
|
2539
|
-
return
|
|
2563
|
+
return log.fail(.{ ident, .None });
|
|
2540
2564
|
}
|
|
2541
|
-
|
|
2565
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
|
|
2542
2566
|
const stat = unmodifiedStats(battle, side).spc;
|
|
2543
2567
|
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);
|
|
2568
|
+
try log.boost(.{ ident, .SpecialAttack, n });
|
|
2569
|
+
try log.boost(.{ ident, .SpecialDefense, n });
|
|
2546
2570
|
},
|
|
2547
2571
|
.EvasionUp1 => {
|
|
2548
2572
|
assert(boosts.evasion >= -6 and boosts.evasion <= 6);
|
|
2549
|
-
if (boosts.evasion == 6) return
|
|
2573
|
+
if (boosts.evasion == 6) return log.fail(.{ ident, .None });
|
|
2550
2574
|
boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
|
|
2551
|
-
try log.boost(ident, .Evasion, 1);
|
|
2575
|
+
try log.boost(.{ ident, .Evasion, 1 });
|
|
2552
2576
|
},
|
|
2553
2577
|
else => unreachable,
|
|
2554
2578
|
}
|
|
@@ -2563,7 +2587,7 @@ pub const Effects = struct {
|
|
|
2563
2587
|
const foe_ident = battle.active(player.foe());
|
|
2564
2588
|
const secondary = move.effect.isStatDownChance();
|
|
2565
2589
|
|
|
2566
|
-
if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(foe_ident, .None);
|
|
2590
|
+
if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(.{ foe_ident, .None });
|
|
2567
2591
|
|
|
2568
2592
|
if (secondary) {
|
|
2569
2593
|
const proc = try Rolls.unboost(battle, player, options);
|
|
@@ -2571,7 +2595,6 @@ pub const Effects = struct {
|
|
|
2571
2595
|
} else {
|
|
2572
2596
|
// checkHit already checks for Invulnerable
|
|
2573
2597
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2574
|
-
try options.chance.commit(player, .hit);
|
|
2575
2598
|
}
|
|
2576
2599
|
|
|
2577
2600
|
var stats = &foe.active.stats;
|
|
@@ -2581,82 +2604,82 @@ pub const Effects = struct {
|
|
|
2581
2604
|
switch (move.effect) {
|
|
2582
2605
|
.AttackDown1, .AttackDownChance => {
|
|
2583
2606
|
assert(boosts.atk >= -6 and boosts.atk <= 6);
|
|
2584
|
-
if (boosts.atk == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2607
|
+
if (boosts.atk == -6) return if (fail) try log.fail(.{ foe_ident, .None });
|
|
2585
2608
|
boosts.atk = @intCast(@max(-6, @as(i8, boosts.atk) - 1));
|
|
2586
2609
|
if (stats.atk == 1) {
|
|
2587
2610
|
boosts.atk += 1;
|
|
2588
2611
|
if (showdown) {
|
|
2589
|
-
try log.boost(foe_ident, .Attack, -1);
|
|
2590
|
-
try log.boost(foe_ident, .Attack, 1);
|
|
2612
|
+
try log.boost(.{ foe_ident, .Attack, -1 });
|
|
2613
|
+
try log.boost(.{ foe_ident, .Attack, 1 });
|
|
2591
2614
|
}
|
|
2592
|
-
return
|
|
2615
|
+
return log.fail(.{ foe_ident, .None });
|
|
2593
2616
|
}
|
|
2594
|
-
|
|
2617
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
|
|
2595
2618
|
const stat = unmodifiedStats(battle, foe).atk;
|
|
2596
2619
|
stats.atk = @max(1, stat * mod[0] / mod[1]);
|
|
2597
|
-
try log.boost(foe_ident, .Attack, -1);
|
|
2620
|
+
try log.boost(.{ foe_ident, .Attack, -1 });
|
|
2598
2621
|
},
|
|
2599
2622
|
.DefenseDown1, .DefenseDown2, .DefenseDownChance => {
|
|
2600
2623
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
2601
|
-
if (boosts.def == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2624
|
+
if (boosts.def == -6) return if (fail) try log.fail(.{ foe_ident, .None });
|
|
2602
2625
|
const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
|
|
2603
2626
|
boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
|
|
2604
2627
|
if (stats.def == 1) {
|
|
2605
2628
|
boosts.def += 1;
|
|
2606
2629
|
if (showdown) {
|
|
2607
|
-
try log.boost(foe_ident, .Defense, -@as(i8, n));
|
|
2608
|
-
try log.boost(foe_ident, .Defense, 1);
|
|
2630
|
+
try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
|
|
2631
|
+
try log.boost(.{ foe_ident, .Defense, 1 });
|
|
2609
2632
|
}
|
|
2610
|
-
return
|
|
2633
|
+
return log.fail(.{ foe_ident, .None });
|
|
2611
2634
|
}
|
|
2612
|
-
|
|
2635
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
|
|
2613
2636
|
const stat = unmodifiedStats(battle, foe).def;
|
|
2614
2637
|
stats.def = @max(1, stat * mod[0] / mod[1]);
|
|
2615
|
-
try log.boost(foe_ident, .Defense, -@as(i8, n));
|
|
2638
|
+
try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
|
|
2616
2639
|
},
|
|
2617
2640
|
.SpeedDown1, .SpeedDownChance => {
|
|
2618
2641
|
assert(boosts.spe >= -6 and boosts.spe <= 6);
|
|
2619
|
-
if (boosts.spe == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2642
|
+
if (boosts.spe == -6) return if (fail) try log.fail(.{ foe_ident, .None });
|
|
2620
2643
|
boosts.spe = @intCast(@max(-6, @as(i8, boosts.spe) - 1));
|
|
2621
2644
|
if (stats.spe == 1) {
|
|
2622
2645
|
boosts.spe += 1;
|
|
2623
2646
|
if (showdown) {
|
|
2624
|
-
try log.boost(foe_ident, .Speed, -1);
|
|
2625
|
-
try log.boost(foe_ident, .Speed, 1);
|
|
2647
|
+
try log.boost(.{ foe_ident, .Speed, -1 });
|
|
2648
|
+
try log.boost(.{ foe_ident, .Speed, 1 });
|
|
2626
2649
|
}
|
|
2627
|
-
return
|
|
2650
|
+
return log.fail(.{ foe_ident, .None });
|
|
2628
2651
|
}
|
|
2629
|
-
|
|
2652
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
|
|
2630
2653
|
const stat = unmodifiedStats(battle, foe).spe;
|
|
2631
2654
|
stats.spe = @max(1, stat * mod[0] / mod[1]);
|
|
2632
|
-
try log.boost(foe_ident, .Speed, -1);
|
|
2655
|
+
try log.boost(.{ foe_ident, .Speed, -1 });
|
|
2633
2656
|
assert(boosts.spe >= -6);
|
|
2634
2657
|
},
|
|
2635
2658
|
.SpecialDownChance => {
|
|
2636
2659
|
assert(boosts.spc >= -6 and boosts.spc <= 6);
|
|
2637
|
-
if (boosts.spc == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2660
|
+
if (boosts.spc == -6) return if (fail) try log.fail(.{ foe_ident, .None });
|
|
2638
2661
|
boosts.spc = @intCast(@max(-6, @as(i8, boosts.spc) - 1));
|
|
2639
2662
|
if (stats.spc == 1) {
|
|
2640
2663
|
boosts.spc += 1;
|
|
2641
2664
|
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);
|
|
2665
|
+
try log.boost(.{ foe_ident, .SpecialAttack, -1 });
|
|
2666
|
+
try log.boost(.{ foe_ident, .SpecialAttack, 1 });
|
|
2667
|
+
try log.boost(.{ foe_ident, .SpecialDefense, -1 });
|
|
2668
|
+
try log.boost(.{ foe_ident, .SpecialDefense, 1 });
|
|
2646
2669
|
}
|
|
2647
|
-
return
|
|
2670
|
+
return log.fail(.{ foe_ident, .None });
|
|
2648
2671
|
}
|
|
2649
|
-
|
|
2672
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
|
|
2650
2673
|
const stat = unmodifiedStats(battle, foe).spc;
|
|
2651
2674
|
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);
|
|
2675
|
+
try log.boost(.{ foe_ident, .SpecialAttack, -1 });
|
|
2676
|
+
try log.boost(.{ foe_ident, .SpecialDefense, -1 });
|
|
2654
2677
|
},
|
|
2655
2678
|
.AccuracyDown1 => {
|
|
2656
2679
|
assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
|
|
2657
|
-
if (boosts.accuracy == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2680
|
+
if (boosts.accuracy == -6) return if (fail) try log.fail(.{ foe_ident, .None });
|
|
2658
2681
|
boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
|
|
2659
|
-
try log.boost(foe_ident, .Accuracy, -1);
|
|
2682
|
+
try log.boost(.{ foe_ident, .Accuracy, -1 });
|
|
2660
2683
|
},
|
|
2661
2684
|
else => unreachable,
|
|
2662
2685
|
}
|
|
@@ -2680,40 +2703,40 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
|
|
|
2680
2703
|
}
|
|
2681
2704
|
}
|
|
2682
2705
|
|
|
2683
|
-
|
|
2706
|
+
fn isForced(active: anytype) bool {
|
|
2684
2707
|
return active.volatiles.Recharging or active.volatiles.Rage or
|
|
2685
2708
|
active.volatiles.Thrashing or active.volatiles.Charging;
|
|
2686
2709
|
}
|
|
2687
2710
|
|
|
2688
|
-
fn clearVolatiles(battle: anytype, who: Player,
|
|
2711
|
+
fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
|
|
2689
2712
|
var log = options.log;
|
|
2690
2713
|
var side = battle.side(who);
|
|
2691
2714
|
var volatiles = &side.active.volatiles;
|
|
2692
2715
|
const ident = battle.active(who);
|
|
2693
2716
|
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
try log.end(ident, .DisableSilent);
|
|
2717
|
+
if (volatiles.disable_move != 0) {
|
|
2718
|
+
volatiles.disable_move = 0;
|
|
2719
|
+
volatiles.disable_duration = 0;
|
|
2720
|
+
options.chance.observe(.disable, who, .None);
|
|
2721
|
+
try log.end(.{ ident, .DisableSilent });
|
|
2700
2722
|
}
|
|
2701
2723
|
if (volatiles.Confusion) {
|
|
2702
2724
|
// volatiles.confusion is left unchanged
|
|
2703
2725
|
volatiles.Confusion = false;
|
|
2704
|
-
|
|
2726
|
+
options.chance.observe(.confusion, who, .None);
|
|
2727
|
+
try log.end(.{ ident, .ConfusionSilent });
|
|
2705
2728
|
}
|
|
2706
2729
|
if (volatiles.Mist) {
|
|
2707
2730
|
volatiles.Mist = false;
|
|
2708
|
-
try log.end(ident, .
|
|
2731
|
+
try log.end(.{ ident, .MistSilent });
|
|
2709
2732
|
}
|
|
2710
2733
|
if (volatiles.FocusEnergy) {
|
|
2711
2734
|
volatiles.FocusEnergy = false;
|
|
2712
|
-
try log.end(ident, .
|
|
2735
|
+
try log.end(.{ ident, .FocusEnergySilent });
|
|
2713
2736
|
}
|
|
2714
2737
|
if (volatiles.LeechSeed) {
|
|
2715
2738
|
volatiles.LeechSeed = false;
|
|
2716
|
-
try log.end(ident, .
|
|
2739
|
+
try log.end(.{ ident, .LeechSeedSilent });
|
|
2717
2740
|
}
|
|
2718
2741
|
if (!showdown and volatiles.Toxic) {
|
|
2719
2742
|
volatiles.Toxic = false;
|
|
@@ -2721,24 +2744,39 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
|
|
|
2721
2744
|
}
|
|
2722
2745
|
if (volatiles.LightScreen) {
|
|
2723
2746
|
volatiles.LightScreen = false;
|
|
2724
|
-
try log.end(ident, .
|
|
2747
|
+
try log.end(.{ ident, .LightScreenSilent });
|
|
2725
2748
|
}
|
|
2726
2749
|
if (volatiles.Reflect) {
|
|
2727
2750
|
volatiles.Reflect = false;
|
|
2728
|
-
try log.end(ident, .
|
|
2751
|
+
try log.end(.{ ident, .ReflectSilent });
|
|
2729
2752
|
}
|
|
2730
2753
|
if (showdown and volatiles.Toxic) {
|
|
2731
2754
|
volatiles.Toxic = false;
|
|
2732
2755
|
// Pokémon Showdown erroneously clears the toxic counter
|
|
2733
2756
|
volatiles.toxic = 0;
|
|
2734
|
-
try log.end(ident, .
|
|
2757
|
+
try log.end(.{ ident, .ToxicSilent });
|
|
2735
2758
|
}
|
|
2736
2759
|
}
|
|
2737
2760
|
|
|
2761
|
+
fn decrement(
|
|
2762
|
+
comptime field: chance.Action.Field,
|
|
2763
|
+
player: Player,
|
|
2764
|
+
options: anytype,
|
|
2765
|
+
n: anytype,
|
|
2766
|
+
) @TypeOf(n) {
|
|
2767
|
+
return if (options.calc.overridden(player, field)) |obs| switch (obs) {
|
|
2768
|
+
.started, .ended => 0,
|
|
2769
|
+
.continuing => if (n > 1) n - 1 else n,
|
|
2770
|
+
else => unreachable,
|
|
2771
|
+
} else n - 1;
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2738
2774
|
pub const Rolls = struct {
|
|
2739
|
-
|
|
2775
|
+
fn speedTie(battle: anytype, options: anytype) !bool {
|
|
2740
2776
|
const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
|
|
2741
2777
|
player == .P1
|
|
2778
|
+
else if (@hasDecl(@TypeOf(battle.rng), "speedTie"))
|
|
2779
|
+
battle.rng.speedTie()
|
|
2742
2780
|
else if (showdown)
|
|
2743
2781
|
battle.rng.range(u8, 0, 2) == 0
|
|
2744
2782
|
else
|
|
@@ -2748,21 +2786,31 @@ pub const Rolls = struct {
|
|
|
2748
2786
|
return p1;
|
|
2749
2787
|
}
|
|
2750
2788
|
|
|
2751
|
-
|
|
2789
|
+
fn criticalHit(
|
|
2790
|
+
battle: anytype,
|
|
2791
|
+
player: Player,
|
|
2792
|
+
rate: u8,
|
|
2793
|
+
spurious: bool,
|
|
2794
|
+
options: anytype,
|
|
2795
|
+
) !bool {
|
|
2752
2796
|
const crit = if (options.calc.overridden(player, .critical_hit)) |val|
|
|
2753
2797
|
val == .true
|
|
2798
|
+
else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
|
|
2799
|
+
battle.rng.criticalHit(player, rate)
|
|
2754
2800
|
else if (showdown)
|
|
2755
2801
|
battle.rng.chance(u8, rate, 256)
|
|
2756
2802
|
else
|
|
2757
2803
|
std.math.rotl(u8, battle.rng.next(), 3) < rate;
|
|
2758
2804
|
|
|
2759
|
-
try options.chance.criticalHit(player, crit, rate);
|
|
2805
|
+
if (!spurious) try options.chance.criticalHit(player, crit, rate);
|
|
2760
2806
|
return crit;
|
|
2761
2807
|
}
|
|
2762
2808
|
|
|
2763
|
-
|
|
2809
|
+
fn damage(battle: anytype, player: Player, options: anytype) !u8 {
|
|
2764
2810
|
const roll = if (options.calc.overridden(player, .damage)) |val|
|
|
2765
2811
|
val
|
|
2812
|
+
else if (@hasDecl(@TypeOf(battle.rng), "damage"))
|
|
2813
|
+
battle.rng.damage(player)
|
|
2766
2814
|
else roll: {
|
|
2767
2815
|
if (showdown) break :roll battle.rng.range(u8, 217, 256);
|
|
2768
2816
|
while (true) {
|
|
@@ -2776,21 +2824,27 @@ pub const Rolls = struct {
|
|
|
2776
2824
|
return roll;
|
|
2777
2825
|
}
|
|
2778
2826
|
|
|
2779
|
-
|
|
2827
|
+
const uN = if (pkmn.options.miss) u8 else u9;
|
|
2828
|
+
fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) !bool {
|
|
2829
|
+
const acc: uN = if (pkmn.options.miss) accuracy else @as(uN, accuracy) + 1;
|
|
2780
2830
|
const ok = if (options.calc.overridden(player, .hit)) |val|
|
|
2781
2831
|
val == .true
|
|
2832
|
+
else if (@hasDecl(@TypeOf(battle.rng), "hit"))
|
|
2833
|
+
battle.rng.hit(player, acc)
|
|
2782
2834
|
else if (showdown)
|
|
2783
|
-
battle.rng.chance(
|
|
2835
|
+
battle.rng.chance(uN, acc, 256)
|
|
2784
2836
|
else
|
|
2785
|
-
battle.rng.next() <
|
|
2837
|
+
battle.rng.next() < acc;
|
|
2786
2838
|
|
|
2787
|
-
options.chance.hit(ok,
|
|
2839
|
+
try options.chance.hit(player, ok, acc);
|
|
2788
2840
|
return ok;
|
|
2789
2841
|
}
|
|
2790
2842
|
|
|
2791
|
-
|
|
2843
|
+
fn confused(battle: anytype, player: Player, options: anytype) !bool {
|
|
2792
2844
|
const cfz = if (options.calc.overridden(player, .confused)) |val|
|
|
2793
2845
|
val == .true
|
|
2846
|
+
else if (@hasDecl(@TypeOf(battle.rng), "confused"))
|
|
2847
|
+
battle.rng.confused(player)
|
|
2794
2848
|
else if (showdown)
|
|
2795
2849
|
!battle.rng.chance(u8, 128, 256)
|
|
2796
2850
|
else
|
|
@@ -2800,9 +2854,11 @@ pub const Rolls = struct {
|
|
|
2800
2854
|
return cfz;
|
|
2801
2855
|
}
|
|
2802
2856
|
|
|
2803
|
-
|
|
2857
|
+
fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
|
|
2804
2858
|
const par = if (options.calc.overridden(player, .paralyzed)) |val|
|
|
2805
2859
|
val == .true
|
|
2860
|
+
else if (@hasDecl(@TypeOf(battle.rng), "paralyzed"))
|
|
2861
|
+
battle.rng.paralyzed(player)
|
|
2806
2862
|
else if (showdown)
|
|
2807
2863
|
battle.rng.chance(u8, 63, 256)
|
|
2808
2864
|
else
|
|
@@ -2812,9 +2868,11 @@ pub const Rolls = struct {
|
|
|
2812
2868
|
return par;
|
|
2813
2869
|
}
|
|
2814
2870
|
|
|
2815
|
-
|
|
2871
|
+
fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
|
|
2816
2872
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2817
2873
|
val == .true
|
|
2874
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2875
|
+
battle.rng.secondaryChance(player, 25)
|
|
2818
2876
|
else if (showdown)
|
|
2819
2877
|
battle.rng.chance(u8, 25, 256)
|
|
2820
2878
|
else
|
|
@@ -2824,11 +2882,13 @@ pub const Rolls = struct {
|
|
|
2824
2882
|
return proc;
|
|
2825
2883
|
}
|
|
2826
2884
|
|
|
2827
|
-
|
|
2885
|
+
fn secondaryChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
|
|
2828
2886
|
const rate: u8 = if (low) 26 else 77;
|
|
2829
2887
|
|
|
2830
2888
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2831
2889
|
val == .true
|
|
2890
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2891
|
+
battle.rng.secondaryChance(player, rate)
|
|
2832
2892
|
else if (showdown)
|
|
2833
2893
|
battle.rng.chance(u8, rate, 256)
|
|
2834
2894
|
else
|
|
@@ -2838,11 +2898,13 @@ pub const Rolls = struct {
|
|
|
2838
2898
|
return proc;
|
|
2839
2899
|
}
|
|
2840
2900
|
|
|
2841
|
-
|
|
2901
|
+
fn poisonChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
|
|
2842
2902
|
const rate: u8 = if (low) 52 else 103;
|
|
2843
2903
|
|
|
2844
2904
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2845
2905
|
val == .true
|
|
2906
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2907
|
+
battle.rng.secondaryChance(player, rate)
|
|
2846
2908
|
else if (showdown)
|
|
2847
2909
|
battle.rng.chance(u8, rate, 256)
|
|
2848
2910
|
else
|
|
@@ -2852,9 +2914,11 @@ pub const Rolls = struct {
|
|
|
2852
2914
|
return proc;
|
|
2853
2915
|
}
|
|
2854
2916
|
|
|
2855
|
-
|
|
2917
|
+
fn unboost(battle: anytype, player: Player, options: anytype) !bool {
|
|
2856
2918
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2857
2919
|
val == .true
|
|
2920
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2921
|
+
battle.rng.secondaryChance(player, 85)
|
|
2858
2922
|
else if (showdown)
|
|
2859
2923
|
battle.rng.chance(u8, 85, 256)
|
|
2860
2924
|
else
|
|
@@ -2864,14 +2928,14 @@ pub const Rolls = struct {
|
|
|
2864
2928
|
return proc;
|
|
2865
2929
|
}
|
|
2866
2930
|
|
|
2867
|
-
|
|
2931
|
+
fn metronome(battle: anytype, player: Player, options: anytype) !Move {
|
|
2868
2932
|
const move: Move = if (options.calc.overridden(player, .metronome)) |val|
|
|
2869
2933
|
val
|
|
2870
|
-
else if (
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2934
|
+
else if (@hasDecl(@TypeOf(battle.rng), "metronome"))
|
|
2935
|
+
battle.rng.metronome(player)
|
|
2936
|
+
else if (showdown)
|
|
2937
|
+
Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
|
|
2938
|
+
else move: {
|
|
2875
2939
|
while (true) {
|
|
2876
2940
|
const r = battle.rng.next();
|
|
2877
2941
|
if (r == 0 or r == @intFromEnum(Move.Metronome)) continue;
|
|
@@ -2884,9 +2948,11 @@ pub const Rolls = struct {
|
|
|
2884
2948
|
return move;
|
|
2885
2949
|
}
|
|
2886
2950
|
|
|
2887
|
-
|
|
2951
|
+
fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
|
|
2888
2952
|
const power = if (options.calc.overridden(player, .psywave)) |val|
|
|
2889
2953
|
val - 1
|
|
2954
|
+
else if (@hasDecl(@TypeOf(battle.rng), "psywave"))
|
|
2955
|
+
battle.rng.psywave(player)
|
|
2890
2956
|
else if (showdown)
|
|
2891
2957
|
battle.rng.range(u8, 0, max)
|
|
2892
2958
|
else power: {
|
|
@@ -2901,11 +2967,14 @@ pub const Rolls = struct {
|
|
|
2901
2967
|
return power;
|
|
2902
2968
|
}
|
|
2903
2969
|
|
|
2904
|
-
|
|
2905
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2906
|
-
@intCast(val)
|
|
2970
|
+
fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2971
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2972
|
+
if (val >= 1 and val <= 7) @as(u3, @intCast(val)) else null
|
|
2973
|
+
else
|
|
2974
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
|
|
2975
|
+
battle.rng.sleepDuration(player)
|
|
2907
2976
|
else if (showdown)
|
|
2908
|
-
|
|
2977
|
+
battle.rng.range(u3, 1, 8)
|
|
2909
2978
|
else duration: {
|
|
2910
2979
|
while (true) {
|
|
2911
2980
|
const r = battle.rng.next() & 7;
|
|
@@ -2914,60 +2983,74 @@ pub const Rolls = struct {
|
|
|
2914
2983
|
};
|
|
2915
2984
|
|
|
2916
2985
|
assert(duration >= 1 and duration <= 7);
|
|
2917
|
-
options.chance.duration(
|
|
2986
|
+
options.chance.duration(player, duration);
|
|
2918
2987
|
return duration;
|
|
2919
2988
|
}
|
|
2920
2989
|
|
|
2921
|
-
|
|
2922
|
-
const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
|
|
2923
|
-
val
|
|
2990
|
+
fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
|
|
2991
|
+
const duration: u4 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2992
|
+
if (val >= 1 and val <= 8) val else null
|
|
2993
|
+
else
|
|
2994
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
|
|
2995
|
+
battle.rng.disableDuration(player)
|
|
2924
2996
|
else if (showdown)
|
|
2925
|
-
|
|
2997
|
+
battle.rng.range(u4, 1, 9)
|
|
2926
2998
|
else
|
|
2927
2999
|
@intCast((battle.rng.next() & 7) + 1);
|
|
2928
3000
|
|
|
2929
3001
|
assert(duration >= 1 and duration <= 8);
|
|
2930
|
-
options.chance.duration(
|
|
3002
|
+
options.chance.duration(player, duration);
|
|
2931
3003
|
return duration;
|
|
2932
3004
|
}
|
|
2933
3005
|
|
|
2934
|
-
|
|
2935
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2936
|
-
@intCast(val)
|
|
3006
|
+
fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
3007
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
3008
|
+
if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
|
|
3009
|
+
else
|
|
3010
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
|
|
3011
|
+
battle.rng.confusionDuration(player)
|
|
2937
3012
|
else if (showdown)
|
|
2938
|
-
|
|
3013
|
+
battle.rng.range(u3, 2, 6)
|
|
2939
3014
|
else
|
|
2940
3015
|
@intCast((battle.rng.next() & 3) + 2);
|
|
2941
3016
|
|
|
2942
3017
|
assert(duration >= 2 and duration <= 5);
|
|
2943
|
-
options.chance.duration(
|
|
3018
|
+
options.chance.duration(player, duration);
|
|
2944
3019
|
return duration;
|
|
2945
3020
|
}
|
|
2946
3021
|
|
|
2947
|
-
|
|
2948
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2949
|
-
@intCast(val)
|
|
3022
|
+
fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
3023
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
3024
|
+
if (val >= 2 and val <= 3) @as(u3, @intCast(val)) else null
|
|
3025
|
+
else
|
|
3026
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
|
|
3027
|
+
battle.rng.attackingDuration(player)
|
|
2950
3028
|
else if (showdown)
|
|
2951
|
-
|
|
3029
|
+
battle.rng.range(u3, 2, 4)
|
|
2952
3030
|
else
|
|
2953
3031
|
@intCast((battle.rng.next() & 1) + 2);
|
|
2954
3032
|
|
|
2955
3033
|
assert(duration >= 2 and duration <= 3);
|
|
2956
|
-
options.chance.duration(
|
|
3034
|
+
options.chance.duration(player, duration);
|
|
2957
3035
|
return duration;
|
|
2958
3036
|
}
|
|
2959
3037
|
|
|
2960
|
-
|
|
3038
|
+
const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
|
|
3039
|
+
|
|
3040
|
+
fn distribution(
|
|
2961
3041
|
battle: anytype,
|
|
2962
3042
|
comptime effect: Move.Effect,
|
|
2963
3043
|
player: Player,
|
|
2964
3044
|
options: anytype,
|
|
2965
3045
|
) !u3 {
|
|
2966
3046
|
const roll = if (effect == .Binding) .duration else .multi_hit;
|
|
2967
|
-
const n: u3 = if (options.calc.overridden(player, roll)) |val|
|
|
2968
|
-
@intCast(val)
|
|
3047
|
+
const n: u3 = (if (options.calc.overridden(player, roll)) |val|
|
|
3048
|
+
if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
|
|
3049
|
+
else
|
|
3050
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "distribution"))
|
|
3051
|
+
battle.rng.distribution(player)
|
|
2969
3052
|
else if (showdown)
|
|
2970
|
-
DISTRIBUTION[battle.rng.range(
|
|
3053
|
+
DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
|
|
2971
3054
|
else n: {
|
|
2972
3055
|
const r = (battle.rng.next() & 3);
|
|
2973
3056
|
break :n @intCast((if (r < 2) r else battle.rng.next() & 3) + 2);
|
|
@@ -2975,7 +3058,7 @@ pub const Rolls = struct {
|
|
|
2975
3058
|
|
|
2976
3059
|
assert(n >= 2 and n <= 5);
|
|
2977
3060
|
if (effect == .Binding) {
|
|
2978
|
-
options.chance.duration(
|
|
3061
|
+
options.chance.duration(player, n);
|
|
2979
3062
|
} else {
|
|
2980
3063
|
try options.chance.multiHit(player, n);
|
|
2981
3064
|
}
|
|
@@ -2990,15 +3073,15 @@ pub const Rolls = struct {
|
|
|
2990
3073
|
options: anytype,
|
|
2991
3074
|
) !u4 {
|
|
2992
3075
|
// TODO: consider throwing error instead of rerolling?
|
|
2993
|
-
const
|
|
3076
|
+
const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
|
|
2994
3077
|
if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
|
|
2995
3078
|
val
|
|
2996
3079
|
else
|
|
2997
3080
|
null
|
|
2998
3081
|
else
|
|
2999
|
-
null
|
|
3000
|
-
|
|
3001
|
-
|
|
3082
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
|
|
3083
|
+
battle.rng.moveSlot(player, moves, check_pp)
|
|
3084
|
+
else slot: {
|
|
3002
3085
|
if (showdown) {
|
|
3003
3086
|
if (check_pp == 0) {
|
|
3004
3087
|
var i: usize = moves.len;
|
|
@@ -3014,10 +3097,9 @@ pub const Rolls = struct {
|
|
|
3014
3097
|
while (i < moves.len and r > 0) : (i += 1) {
|
|
3015
3098
|
if (moves[i].pp > 0) {
|
|
3016
3099
|
r -= 1;
|
|
3017
|
-
if (r == 0) break;
|
|
3100
|
+
if (r == 0) break :slot @intCast(i + 1);
|
|
3018
3101
|
}
|
|
3019
3102
|
}
|
|
3020
|
-
break :slot @intCast(i + 1);
|
|
3021
3103
|
}
|
|
3022
3104
|
}
|
|
3023
3105
|
|
|
@@ -3039,11 +3121,11 @@ test "RNG agreement" {
|
|
|
3039
3121
|
expected[i] = @intCast(i * 0x1000000);
|
|
3040
3122
|
}
|
|
3041
3123
|
|
|
3042
|
-
var spe
|
|
3043
|
-
var cfz
|
|
3044
|
-
var par
|
|
3045
|
-
var brn
|
|
3046
|
-
var eff
|
|
3124
|
+
var spe: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
|
|
3125
|
+
var cfz: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
|
|
3126
|
+
var par: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
|
|
3127
|
+
var brn: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
|
|
3128
|
+
var eff: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
|
|
3047
3129
|
|
|
3048
3130
|
for (0..expected.len) |i| {
|
|
3049
3131
|
try expectEqual(spe.range(u8, 0, 2) == 0, i < Gen12.percent(50) + 1);
|
|
@@ -3063,11 +3145,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3063
3145
|
},
|
|
3064
3146
|
.Switch => {
|
|
3065
3147
|
const side = battle.side(player);
|
|
3066
|
-
var slot:
|
|
3148
|
+
var slot: usize = 2;
|
|
3067
3149
|
while (slot <= 6) : (slot += 1) {
|
|
3068
3150
|
const id = side.order[slot - 1];
|
|
3069
3151
|
if (id == 0 or side.pokemon[id - 1].hp == 0) continue;
|
|
3070
|
-
out[n] = .{ .type = .Switch, .data = slot };
|
|
3152
|
+
out[n] = .{ .type = .Switch, .data = @intCast(slot) };
|
|
3071
3153
|
n += 1;
|
|
3072
3154
|
}
|
|
3073
3155
|
if (n == 0) {
|
|
@@ -3079,11 +3161,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3079
3161
|
const side = battle.side(player);
|
|
3080
3162
|
const foe = battle.foe(player);
|
|
3081
3163
|
|
|
3082
|
-
|
|
3164
|
+
const active = &side.active;
|
|
3083
3165
|
const stored = side.stored();
|
|
3084
3166
|
|
|
3085
3167
|
// While players are not given any input options on the cartridge in these cases,
|
|
3086
|
-
// Pokémon Showdown instead produces a list with a single move that must be chosen
|
|
3168
|
+
// Pokémon Showdown instead produces a list with a single move that must be chosen
|
|
3087
3169
|
//
|
|
3088
3170
|
// Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
|
|
3089
3171
|
// here when not in compatibility mode, but the engine needs to be able to differentiate
|
|
@@ -3091,18 +3173,18 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3091
3173
|
// passing due to being forced into using a move. Instead of introducing another option
|
|
3092
3174
|
// we simply repurpose Move with no move slot, even though pedantically this is not
|
|
3093
3175
|
// strictly correct as the player would not have been presented the option to move or
|
|
3094
|
-
// switch at all
|
|
3176
|
+
// switch at all
|
|
3095
3177
|
if (isForced(active)) {
|
|
3096
3178
|
out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
|
|
3097
3179
|
n += 1;
|
|
3098
3180
|
return n;
|
|
3099
3181
|
}
|
|
3100
3182
|
|
|
3101
|
-
var slot:
|
|
3183
|
+
var slot: usize = 2;
|
|
3102
3184
|
while (slot <= 6) : (slot += 1) {
|
|
3103
3185
|
const id = side.order[slot - 1];
|
|
3104
3186
|
if (id == 0 or side.pokemon[id - 1].hp == 0) continue;
|
|
3105
|
-
out[n] = .{ .type = .Switch, .data = slot };
|
|
3187
|
+
out[n] = .{ .type = .Switch, .data = @intCast(slot) };
|
|
3106
3188
|
n += 1;
|
|
3107
3189
|
}
|
|
3108
3190
|
|
|
@@ -3110,7 +3192,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3110
3192
|
// On the cartridge, all of these happen after "FIGHT" (indicating you are not
|
|
3111
3193
|
// switching) but before you are allowed to select a move. Pokémon Showdown instead
|
|
3112
3194
|
// either disables all other moves in the case of limited or requires you to select a
|
|
3113
|
-
// move normally if sleeping/frozen/bound
|
|
3195
|
+
// move normally if sleeping/frozen/bound
|
|
3114
3196
|
if (!showdown and (limited or foe.active.volatiles.Binding or
|
|
3115
3197
|
Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
|
|
3116
3198
|
{
|
|
@@ -3123,7 +3205,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3123
3205
|
// Pokémon Showdown handles Bide and Binding moves by checking if the move in question
|
|
3124
3206
|
// is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
|
|
3125
3207
|
// Move will not result in forcing the subsequent use unless the user also had the
|
|
3126
|
-
// proc-ed move in their moveset) and disabling all other moves
|
|
3208
|
+
// proc-ed move in their moveset) and disabling all other moves
|
|
3127
3209
|
if (limited) {
|
|
3128
3210
|
assert(showdown);
|
|
3129
3211
|
assert(side.last_selected_move != .None);
|
|
@@ -3133,9 +3215,9 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3133
3215
|
if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
|
|
3134
3216
|
// Pokémon Showdown displays Struggle if limited to Bide but unable to pick
|
|
3135
3217
|
const struggle =
|
|
3136
|
-
m.id == .Bide and (m.pp == 0 or active.volatiles.
|
|
3218
|
+
m.id == .Bide and (m.pp == 0 or active.volatiles.disable_move == slot);
|
|
3137
3219
|
const s = if (struggle) 0 else slot;
|
|
3138
|
-
out[n] = .{ .type = .Move, .data = s };
|
|
3220
|
+
out[n] = .{ .type = .Move, .data = @intCast(s) };
|
|
3139
3221
|
n += 1;
|
|
3140
3222
|
return n;
|
|
3141
3223
|
}
|
|
@@ -3148,14 +3230,14 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3148
3230
|
const m = active.moves[slot - 1];
|
|
3149
3231
|
if (m.id == .None) break;
|
|
3150
3232
|
if (m.pp == 0) continue;
|
|
3151
|
-
if (active.volatiles.
|
|
3152
|
-
out[n] = .{ .type = .Move, .data = slot };
|
|
3233
|
+
if (active.volatiles.disable_move == slot) continue;
|
|
3234
|
+
out[n] = .{ .type = .Move, .data = @intCast(slot) };
|
|
3153
3235
|
n += 1;
|
|
3154
3236
|
}
|
|
3155
3237
|
// Struggle (Pokémon Showdown would use 'move 1' here)
|
|
3156
3238
|
if (n == before) {
|
|
3157
3239
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
3158
|
-
if (!
|
|
3240
|
+
if (!pkmn.options.mod) {
|
|
3159
3241
|
while (slot <= 4) : (slot += 1) {
|
|
3160
3242
|
if (active.moves[slot - 1].pp != 0) return n;
|
|
3161
3243
|
}
|