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