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