@pkmn/engine 0.1.0-dev.437f6a52 → 0.1.0-dev.4438e918
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -3
- package/README.md +104 -59
- package/build/pkg/addon/browser.d.ts +3 -0
- package/build/pkg/addon/browser.js +47 -0
- package/build/pkg/addon/browser.js.map +1 -0
- package/build/pkg/addon/node.d.ts +3 -0
- package/build/pkg/addon/node.js +110 -0
- package/build/pkg/addon/node.js.map +1 -0
- package/build/pkg/addon/wasm.d.ts +2 -0
- package/build/pkg/addon/wasm.js +53 -0
- package/build/pkg/addon/wasm.js.map +1 -0
- package/build/pkg/addon.d.ts +26 -4
- package/build/pkg/addon.js +39 -71
- package/build/pkg/addon.js.map +1 -1
- package/build/pkg/data/ids.json +12 -12
- package/build/pkg/data/layout.json +17 -4
- package/build/pkg/data/protocol.json +84 -13
- package/build/pkg/data.d.ts +27 -5
- package/build/pkg/data.js.map +1 -1
- package/build/pkg/gen1.d.ts +15 -18
- package/build/pkg/gen1.js +30 -21
- package/build/pkg/gen1.js.map +1 -1
- package/build/pkg/index.d.ts +42 -91
- package/build/pkg/index.js +23 -70
- package/build/pkg/index.js.map +1 -1
- package/build/pkg/protocol.d.ts +29 -1
- package/build/pkg/protocol.js +123 -102
- package/build/pkg/protocol.js.map +1 -1
- package/build/tools/debug.js +84 -73
- package/build/tools/debug.js.map +1 -1
- package/build/tools/display/debug.jsx +66 -0
- package/build/tools/display/debug.jsx.map +1 -0
- package/build/tools/display/index.js +152 -0
- package/build/tools/display/index.js.map +1 -0
- package/build/tools/display/ui.jsx +360 -0
- package/build/tools/display/ui.jsx.map +1 -0
- package/build/tools/display/util.js +157 -0
- package/build/tools/display/util.js.map +1 -0
- package/build.zig +304 -142
- package/package.json +71 -121
- package/src/bin/install-pkmn-engine +48 -22
- package/src/bin/pkmn-debug +1 -1
- package/src/lib/bindings/c.zig +200 -0
- package/src/lib/{binding → bindings}/node.zig +15 -32
- package/src/lib/bindings/wasm.zig +50 -0
- package/src/lib/common/array.zig +70 -0
- package/src/lib/common/battle.zig +17 -0
- package/src/lib/common/data.zig +12 -24
- package/src/lib/common/debug.zig +33 -28
- package/src/lib/common/js.zig +114 -0
- package/src/lib/common/optional.zig +61 -0
- package/src/lib/common/options.zig +6 -0
- package/src/lib/common/protocol.zig +964 -450
- package/src/lib/common/rational.zig +440 -0
- package/src/lib/common/rng.zig +22 -15
- package/src/lib/common/util.zig +44 -0
- package/src/lib/gen1/calc.zig +975 -0
- package/src/lib/gen1/chance.zig +1222 -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 +9 -10
- package/src/lib/gen1/data.zig +71 -81
- package/src/lib/gen1/helpers.zig +90 -410
- package/src/lib/gen1/mechanics.zig +712 -648
- package/src/lib/gen2/calc.zig +150 -0
- package/src/lib/gen2/chance.zig +896 -0
- package/src/lib/gen2/data/items.zig +13 -21
- package/src/lib/gen2/data/moves.zig +664 -572
- package/src/lib/gen2/data/species.zig +2 -4
- package/src/lib/gen2/data/types.zig +139 -34
- package/src/lib/gen2/data.zig +121 -96
- package/src/lib/gen2/generated.zig +801 -0
- package/src/lib/gen2/helpers.zig +53 -38
- package/src/lib/gen2/mechanics.zig +3580 -29
- package/src/lib/napi.h +2 -0
- package/src/lib/node.zig +14 -0
- package/src/lib/pkmn.zig +125 -29
- package/src/lib/wasm.zig +13 -0
- package/src/tools/display/dom.ts +108 -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,13 +82,13 @@ 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
|
-
if (p1_slot == 0) return if (findFirstAlive(p2) == 0)
|
|
87
|
+
if (p1_slot == 0) return if (findFirstAlive(p2) == 0) .Tie else .Lose;
|
|
105
88
|
|
|
106
|
-
|
|
89
|
+
const p2_slot = findFirstAlive(p2);
|
|
107
90
|
assert(!showdown or p2_slot == 1);
|
|
108
|
-
if (p2_slot == 0) return
|
|
91
|
+
if (p2_slot == 0) return .Win;
|
|
109
92
|
|
|
110
93
|
try switchIn(battle, .P1, p1_slot, true, options);
|
|
111
94
|
try switchIn(battle, .P2, p2_slot, true, options);
|
|
@@ -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 .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,23 +417,22 @@ 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
|
|
472
433
|
// every Pokémon must have at least one move
|
|
473
434
|
if (mslot != 1 or side.active.move(mslot).id != side.last_selected_move) {
|
|
474
|
-
return
|
|
435
|
+
return .Error;
|
|
475
436
|
} else {
|
|
476
437
|
auto = true;
|
|
477
438
|
}
|
|
@@ -479,26 +440,42 @@ 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,
|
|
494
452
|
.ok => {},
|
|
495
|
-
.err => return
|
|
453
|
+
.err => return .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,19 +614,14 @@ 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
|
-
// Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
|
|
632
|
-
// on the targeting behavior of the confused Pokémon's selected move which results
|
|
633
|
-
// in the wrong behavior with respect to the Substitute + Confusion glitch
|
|
634
|
-
const target = if (showdown and Move.get(side.last_selected_move).target == .Self)
|
|
635
|
-
player
|
|
636
|
-
else
|
|
637
|
-
player.foe();
|
|
638
621
|
|
|
639
622
|
const uncapped = battle.last_damage;
|
|
640
623
|
// Skipping adjustDamage / randomizeDamage / checkHit
|
|
641
|
-
_ = try applyDamage(battle, player,
|
|
624
|
+
_ = try applyDamage(battle, player, player.foe(), .Confusion, options);
|
|
642
625
|
// Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
|
|
643
626
|
if (showdown) battle.last_damage = uncapped;
|
|
644
627
|
|
|
@@ -655,9 +638,12 @@ fn beforeMove(
|
|
|
655
638
|
volatiles.Thrashing = false;
|
|
656
639
|
volatiles.Charging = false;
|
|
657
640
|
volatiles.Binding = false;
|
|
658
|
-
|
|
641
|
+
|
|
642
|
+
options.chance.observe(.attacking, player, .None);
|
|
643
|
+
options.chance.observe(.binding, player, .None);
|
|
644
|
+
|
|
659
645
|
// GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
|
|
660
|
-
try log.cant(ident, .Paralysis);
|
|
646
|
+
try log.cant(.{ ident, .Paralysis });
|
|
661
647
|
return .done;
|
|
662
648
|
}
|
|
663
649
|
|
|
@@ -673,28 +659,22 @@ fn beforeMove(
|
|
|
673
659
|
volatiles.state +%= battle.last_damage;
|
|
674
660
|
}
|
|
675
661
|
|
|
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);
|
|
662
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
663
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
684
664
|
|
|
685
665
|
if (volatiles.attacks != 0) {
|
|
686
|
-
try log.activate(ident, .Bide);
|
|
666
|
+
try log.activate(.{ ident, .Bide });
|
|
687
667
|
return .done;
|
|
688
668
|
}
|
|
689
669
|
|
|
690
670
|
volatiles.Bide = false;
|
|
691
|
-
try log.end(ident, .Bide);
|
|
671
|
+
try log.end(.{ ident, .Bide });
|
|
692
672
|
|
|
693
673
|
battle.last_damage = volatiles.state *% 2;
|
|
694
674
|
volatiles.state = 0;
|
|
695
675
|
|
|
696
676
|
if (battle.last_damage == 0) {
|
|
697
|
-
try log.fail(ident, .None);
|
|
677
|
+
try log.fail(.{ ident, .None });
|
|
698
678
|
return .done;
|
|
699
679
|
}
|
|
700
680
|
|
|
@@ -714,47 +694,42 @@ fn beforeMove(
|
|
|
714
694
|
}
|
|
715
695
|
|
|
716
696
|
if (volatiles.Thrashing) {
|
|
717
|
-
|
|
718
|
-
assert(from != null);
|
|
697
|
+
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
719
698
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
} else {
|
|
723
|
-
volatiles.attacks -= 1;
|
|
724
|
-
}
|
|
725
|
-
try options.chance.attacking(player, volatiles.attacks);
|
|
699
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
700
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
726
701
|
|
|
727
|
-
if (
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
702
|
+
if (volatiles.attacks == 0) {
|
|
703
|
+
const overwritten = volatiles.Confusion;
|
|
704
|
+
|
|
705
|
+
volatiles.Thrashing = false;
|
|
706
|
+
volatiles.Confusion = true;
|
|
707
|
+
volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
708
|
+
|
|
709
|
+
// On Pokémon Showdown this could leak information to the opponent that thrashing has
|
|
710
|
+
// ended (the engine's protocol is always from the omniscient perspective and clients
|
|
711
|
+
// can be expected to recognize this pattern and pass the information only to the
|
|
712
|
+
// correct player)
|
|
713
|
+
try log.start(.{ battle.active(player), .ConfusionSilent });
|
|
714
|
+
options.chance.observe(.confusion, player, if (overwritten)
|
|
715
|
+
if (pkmn.options.overwrite) .overwritten else .continuing
|
|
716
|
+
else
|
|
717
|
+
.started);
|
|
718
|
+
if (!overwritten) options.calc.confusion(player);
|
|
738
719
|
}
|
|
720
|
+
|
|
721
|
+
// This shouldn't actually set last_used_move, but Pokémon Showdown sets last
|
|
722
|
+
// used in useMove and doesn't have the notion of skipping canMove semantics
|
|
723
|
+
if (showdown) side.last_used_move = side.last_selected_move;
|
|
724
|
+
|
|
739
725
|
return .skip_can;
|
|
740
726
|
}
|
|
741
727
|
|
|
742
728
|
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);
|
|
729
|
+
volatiles.attacks = decrement(.binding, player, options, volatiles.attacks);
|
|
730
|
+
try options.chance.binding(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
751
731
|
|
|
752
|
-
try log.move(
|
|
753
|
-
battle.active(player),
|
|
754
|
-
side.last_selected_move,
|
|
755
|
-
battle.active(player.foe()),
|
|
756
|
-
side.last_selected_move,
|
|
757
|
-
);
|
|
732
|
+
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
758
733
|
if (showdown or battle.last_damage != 0) {
|
|
759
734
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
760
735
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
@@ -774,7 +749,7 @@ fn canMove(
|
|
|
774
749
|
mslot: u4,
|
|
775
750
|
auto: bool,
|
|
776
751
|
skip_pp: bool,
|
|
777
|
-
from:
|
|
752
|
+
from: Move,
|
|
778
753
|
residual: *bool,
|
|
779
754
|
options: anytype,
|
|
780
755
|
) !bool {
|
|
@@ -782,33 +757,21 @@ fn canMove(
|
|
|
782
757
|
const player_ident = battle.active(player);
|
|
783
758
|
const move = Move.get(side.last_selected_move);
|
|
784
759
|
|
|
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
760
|
if (side.active.volatiles.Charging) {
|
|
791
761
|
side.active.volatiles.Charging = false;
|
|
792
762
|
side.active.volatiles.Invulnerable = false;
|
|
793
763
|
} else if (move.effect == .Charge) {
|
|
794
|
-
try options.log.move(player_ident, side.last_selected_move,
|
|
764
|
+
try options.log.move(.{ player_ident, side.last_selected_move, ID{}, from });
|
|
795
765
|
setCounterable(battle, player, side, move);
|
|
796
766
|
try Effects.charge(battle, player, options);
|
|
797
767
|
return false;
|
|
798
768
|
}
|
|
799
769
|
|
|
800
|
-
|
|
801
|
-
if (!
|
|
770
|
+
side.last_used_move = side.last_selected_move;
|
|
771
|
+
if (!skip_pp) decrementPP(side, mslot, auto);
|
|
802
772
|
|
|
803
773
|
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);
|
|
774
|
+
try options.log.move(.{ player_ident, side.last_selected_move, battle.active(target), from });
|
|
812
775
|
setCounterable(battle, player, side, move);
|
|
813
776
|
|
|
814
777
|
if (move.effect.onBegin()) {
|
|
@@ -818,9 +781,14 @@ fn canMove(
|
|
|
818
781
|
|
|
819
782
|
if (move.effect == .Thrashing) {
|
|
820
783
|
Effects.thrashing(battle, player, options);
|
|
821
|
-
} else if (
|
|
822
|
-
// Pokémon Showdown handles this after hit/miss checks and damage calculation
|
|
823
|
-
|
|
784
|
+
} else if (move.effect == .Binding) {
|
|
785
|
+
// Pokémon Showdown handles this after hit/miss checks and damage calculation, though clears
|
|
786
|
+
// Recharging in onTryMove (required for the Hyper Beam automatic selection glitch)
|
|
787
|
+
if (showdown) {
|
|
788
|
+
battle.foe(player).active.volatiles.Recharging = false;
|
|
789
|
+
} else {
|
|
790
|
+
try Effects.binding(battle, player, false, options);
|
|
791
|
+
}
|
|
824
792
|
}
|
|
825
793
|
|
|
826
794
|
return true;
|
|
@@ -834,11 +802,7 @@ fn setCounterable(battle: anytype, player: Player, side: *Side, move: Move.Data)
|
|
|
834
802
|
// as the desync results from actually having two separate battle states that subtly disagree
|
|
835
803
|
const counterable = side.last_selected_move != .Counter and move.bp > 0 and
|
|
836
804
|
(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
|
-
}
|
|
805
|
+
battle.lastMove(player).counterable = @intFromBool(counterable);
|
|
842
806
|
}
|
|
843
807
|
|
|
844
808
|
fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
|
|
@@ -850,16 +814,18 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
|
|
|
850
814
|
assert(!volatiles.Rage and !volatiles.Thrashing and !volatiles.MultiHit);
|
|
851
815
|
if (volatiles.Bide) return;
|
|
852
816
|
|
|
853
|
-
|
|
854
|
-
|
|
817
|
+
var move_slot = active.move(mslot);
|
|
818
|
+
assert(move_slot.pp > 0 or auto);
|
|
819
|
+
move_slot.pp = @as(u6, @intCast(move_slot.pp)) -% 1;
|
|
855
820
|
if (volatiles.Transform) return;
|
|
856
821
|
|
|
857
|
-
|
|
858
|
-
|
|
822
|
+
move_slot = side.stored().move(mslot);
|
|
823
|
+
assert(move_slot.pp > 0 or auto);
|
|
824
|
+
move_slot.pp = @as(u6, @intCast(move_slot.pp)) -% 1;
|
|
859
825
|
assert(active.move(mslot).pp == side.stored().move(mslot).pp);
|
|
860
826
|
}
|
|
861
827
|
|
|
862
|
-
fn incrementPP(side: *Side, mslot: u4) void {
|
|
828
|
+
fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
|
|
863
829
|
var active = &side.active;
|
|
864
830
|
const volatiles = &active.volatiles;
|
|
865
831
|
|
|
@@ -867,8 +833,11 @@ fn incrementPP(side: *Side, mslot: u4) void {
|
|
|
867
833
|
// GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
|
|
868
834
|
if (showdown and volatiles.Transform) return;
|
|
869
835
|
|
|
836
|
+
const n = options.calc.overridden(player, .pp) orelse 1;
|
|
837
|
+
if (volatiles.Transform and !options.chance.pp(player, n)) return;
|
|
838
|
+
|
|
870
839
|
assert(mslot > 0 and mslot <= 4);
|
|
871
|
-
side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +%
|
|
840
|
+
side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% n;
|
|
872
841
|
}
|
|
873
842
|
|
|
874
843
|
// Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
|
|
@@ -909,20 +878,20 @@ fn doMove(
|
|
|
909
878
|
var miss = showdown and miss: {
|
|
910
879
|
immune = move.target != .Self and !status and !counter and
|
|
911
880
|
(@intFromEnum(move.type.effectiveness(foe.active.types.type1)) == 0 or
|
|
912
|
-
|
|
881
|
+
@intFromEnum(move.type.effectiveness(foe.active.types.type2)) == 0);
|
|
913
882
|
if (immune and move.effect != .Binding) break :miss true;
|
|
914
883
|
if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) {
|
|
915
884
|
battle.last_damage = 0;
|
|
916
885
|
break :miss true;
|
|
917
886
|
}
|
|
918
887
|
break :miss (move.target != .Self and
|
|
919
|
-
!moveHit(battle, player, move, &immune, &mist, options));
|
|
888
|
+
!try moveHit(battle, player, move, &immune, &mist, options));
|
|
920
889
|
};
|
|
921
890
|
assert(!immune or miss or (showdown and move.effect == .Binding));
|
|
922
891
|
|
|
923
892
|
var late = showdown and move.effect != .Explode;
|
|
924
893
|
const skip = status or immune;
|
|
925
|
-
if ((!showdown or (!skip or counter)) and !miss)
|
|
894
|
+
if ((!showdown or (!skip or counter)) and !miss) damage: {
|
|
926
895
|
if (showdown and move.effect.isMulti()) {
|
|
927
896
|
try Effects.multiHit(battle, player, move, options);
|
|
928
897
|
hits = side.active.volatiles.attacks;
|
|
@@ -944,9 +913,10 @@ fn doMove(
|
|
|
944
913
|
ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
|
|
945
914
|
// This can overflow after adjustDamage, but will still be sufficient to OHKO
|
|
946
915
|
battle.last_damage = if (ohko) 65535 else 0;
|
|
947
|
-
if (showdown) break :
|
|
948
|
-
} else if (!calcDamage(battle, player, player.foe(), move, crit)) {
|
|
949
|
-
|
|
916
|
+
if (showdown) break :damage; // skip adjustDamage / randomizeDamage
|
|
917
|
+
} else if (!calcDamage(battle, player, player.foe(), move, crit, options)) {
|
|
918
|
+
if (!showdown) try options.chance.commit(player, .err);
|
|
919
|
+
return .Error;
|
|
950
920
|
}
|
|
951
921
|
if (battle.last_damage == 0) {
|
|
952
922
|
immune = true;
|
|
@@ -959,11 +929,12 @@ fn doMove(
|
|
|
959
929
|
}
|
|
960
930
|
}
|
|
961
931
|
|
|
962
|
-
var
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
932
|
+
var missed: bool = undefined;
|
|
933
|
+
const zero = battle.last_damage == 0;
|
|
934
|
+
if (!showdown and !skip) {
|
|
935
|
+
missed = !try moveHit(battle, player, move, &immune, &mist, options);
|
|
936
|
+
miss = missed or zero;
|
|
937
|
+
}
|
|
967
938
|
|
|
968
939
|
assert(showdown or miss or battle.last_damage > 0 or skip);
|
|
969
940
|
assert((!showdown and miss) or !(ohko and immune));
|
|
@@ -987,16 +958,37 @@ fn doMove(
|
|
|
987
958
|
ohko = (!showdown or (!immune and !invulnerable)) and
|
|
988
959
|
move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
|
|
989
960
|
if (ohko) {
|
|
990
|
-
try log.immune(foe_ident, .OHKO);
|
|
961
|
+
try log.immune(.{ foe_ident, .OHKO });
|
|
991
962
|
} else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
|
|
992
|
-
try log.immune(foe_ident, .None);
|
|
963
|
+
try log.immune(.{ foe_ident, .None });
|
|
964
|
+
if (!showdown) {
|
|
965
|
+
if (move.effect == .Binding) {
|
|
966
|
+
// Rather obnoxiously, the cartridge sets the Binding volatile in canMove and
|
|
967
|
+
// then simply clears it later on if the move happens to miss in moveHit.
|
|
968
|
+
// However, moveHit can return true and we'll still end up in the outer miss
|
|
969
|
+
// handling block because attacking into an immune Pokemon does zero damage
|
|
970
|
+
// (considered missing)
|
|
971
|
+
assert(zero);
|
|
972
|
+
// Thus we still need to actually save the hit result (but not the crit roll).
|
|
973
|
+
// Using .miss for this commit would accomplish what we want, but is somewhat
|
|
974
|
+
// misleading as its entirely possible the move actually did hit (and the target
|
|
975
|
+
// is now trapped) so we use the .binding value instead which is effectively
|
|
976
|
+
// identical, just doesn't claim to know about the actual results of the hit
|
|
977
|
+
// roll)
|
|
978
|
+
try options.chance.commit(player, .binding);
|
|
979
|
+
} else {
|
|
980
|
+
// The .glitch option allows for conditionally committing if we are in a
|
|
981
|
+
// division-by-zero freeze alternative timeline scenario
|
|
982
|
+
try options.chance.commit(player, .glitch);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
993
985
|
} else if (mist) {
|
|
994
|
-
if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
|
|
995
|
-
try log.fail(foe_ident, .None);
|
|
986
|
+
if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
|
|
987
|
+
try log.fail(.{ foe_ident, .None });
|
|
996
988
|
} else {
|
|
997
|
-
if (showdown
|
|
998
|
-
try log.lastmiss();
|
|
999
|
-
try log.miss(battle.active(player));
|
|
989
|
+
if (!showdown) try options.chance.commit(player, if (zero and !missed) .hit else .miss);
|
|
990
|
+
try log.lastmiss(.{});
|
|
991
|
+
try log.miss(.{battle.active(player)});
|
|
1000
992
|
}
|
|
1001
993
|
if (move.effect == .JumpKick) {
|
|
1002
994
|
// Recoil is supposed to be damage/8 but damage will always be 0 here
|
|
@@ -1009,17 +1001,15 @@ fn doMove(
|
|
|
1009
1001
|
try buildRage(battle, player.foe(), options);
|
|
1010
1002
|
} else if (showdown and move.effect == .Disable) {
|
|
1011
1003
|
try buildRage(battle, player.foe(), options);
|
|
1012
|
-
} else if (!showdown and immune and move.effect == .Binding) {
|
|
1013
|
-
try options.chance.commit(player, .binding);
|
|
1014
1004
|
}
|
|
1015
1005
|
return null;
|
|
1016
|
-
} else {
|
|
1006
|
+
} else if (!showdown) {
|
|
1017
1007
|
try options.chance.commit(player, .hit);
|
|
1018
1008
|
}
|
|
1019
1009
|
|
|
1020
1010
|
// On the cartridge MultiHit doesn't get set up until after damage has been applied for the
|
|
1021
1011
|
// first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
|
|
1022
|
-
// it up above before damage calculation)
|
|
1012
|
+
// it up above before damage calculation)
|
|
1023
1013
|
if (!showdown and move.effect.isMulti()) {
|
|
1024
1014
|
try Effects.multiHit(battle, player, move, options);
|
|
1025
1015
|
hits = side.active.volatiles.attacks;
|
|
@@ -1029,18 +1019,18 @@ fn doMove(
|
|
|
1029
1019
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1030
1020
|
|
|
1031
1021
|
var nullified = false;
|
|
1032
|
-
var hit:
|
|
1022
|
+
var hit: usize = 0;
|
|
1033
1023
|
while (hit < hits) {
|
|
1034
1024
|
if (hit == 0) {
|
|
1035
|
-
if (crit) try log.crit(battle.active(player.foe()));
|
|
1025
|
+
if (crit) try log.crit(.{battle.active(player.foe())});
|
|
1036
1026
|
if (effectiveness > Effectiveness.neutral) {
|
|
1037
|
-
try log.supereffective(battle.active(player.foe()));
|
|
1027
|
+
try log.supereffective(.{battle.active(player.foe())});
|
|
1038
1028
|
} else if (effectiveness < Effectiveness.neutral) {
|
|
1039
|
-
try log.resisted(battle.active(player.foe()));
|
|
1029
|
+
try log.resisted(.{battle.active(player.foe())});
|
|
1040
1030
|
}
|
|
1041
1031
|
}
|
|
1042
1032
|
if (!skip) nullified = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
1043
|
-
if (hit == 0 and ohko) try log.ohko();
|
|
1033
|
+
if (hit == 0 and ohko) try log.ohko(.{});
|
|
1044
1034
|
hit += 1;
|
|
1045
1035
|
if (foe.stored().hp == 0) break;
|
|
1046
1036
|
if (!late and (!sub or move.effect == .Explode)) {
|
|
@@ -1057,33 +1047,31 @@ fn doMove(
|
|
|
1057
1047
|
if (showdown and move.effect == .Twineedle and !nullified and foe.stored().hp > 0) {
|
|
1058
1048
|
try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
1059
1049
|
}
|
|
1060
|
-
try log.hitcount(battle.active(player.foe()), hit);
|
|
1061
|
-
} else if (showdown) {
|
|
1050
|
+
try log.hitcount(.{ battle.active(player.foe()), @as(u4, @intCast(hit)) });
|
|
1051
|
+
} else if (showdown and move.effect == .Binding) {
|
|
1062
1052
|
// 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;
|
|
1053
|
+
try Effects.binding(battle, player, rewrap, options);
|
|
1054
|
+
if (immune) {
|
|
1055
|
+
battle.last_damage = 0;
|
|
1056
|
+
assert(foe.stored().hp > 0);
|
|
1057
|
+
// Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
|
|
1058
|
+
if (sub) {
|
|
1059
|
+
try log.activate(.{ battle.active(player.foe()), .Substitute });
|
|
1060
|
+
} else {
|
|
1061
|
+
try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
|
|
1062
|
+
try buildRage(battle, player.foe(), options);
|
|
1076
1063
|
}
|
|
1064
|
+
return null;
|
|
1077
1065
|
}
|
|
1078
1066
|
}
|
|
1079
1067
|
|
|
1080
1068
|
// 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
|
|
1069
|
+
// if an effect was intended to "always happen" it will still get skipped
|
|
1082
1070
|
if (nullified) return null;
|
|
1083
1071
|
|
|
1084
1072
|
// On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
|
|
1085
1073
|
// 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
|
|
1074
|
+
// setup before the loop means we can avoid having to waste time doing no-op handler searches
|
|
1087
1075
|
if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
|
|
1088
1076
|
|
|
1089
1077
|
if (foe.stored().hp == 0) return null;
|
|
@@ -1098,7 +1086,7 @@ fn doMove(
|
|
|
1098
1086
|
// below). For Twineedle we change the data to that of one with PoisonChance1 given its
|
|
1099
1087
|
// MultiHit behavior is complete after the loop above, though Pokémon Showdown handles the
|
|
1100
1088
|
// Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
|
|
1101
|
-
// puts the |-status| message before |-hitcount| instead of after
|
|
1089
|
+
// puts the |-status| message before |-hitcount| instead of after
|
|
1102
1090
|
if (move.effect == .Twineedle) {
|
|
1103
1091
|
if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
1104
1092
|
} else if (move.effect == .Disable) {
|
|
@@ -1117,12 +1105,14 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
|
|
|
1117
1105
|
const side = battle.side(player);
|
|
1118
1106
|
|
|
1119
1107
|
// Base speed is used for the critical hit calculation, even when Transform-ed
|
|
1120
|
-
var rate =
|
|
1108
|
+
var rate: u16 = Species.chance(side.stored().species);
|
|
1121
1109
|
// GLITCH: Focus Energy reduces critical hit rate instead of increasing it
|
|
1122
1110
|
rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
|
|
1123
1111
|
rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
|
|
1124
1112
|
|
|
1125
|
-
|
|
1113
|
+
const spurious =
|
|
1114
|
+
!showdown and ((move.bp == 0 and move.effect != .OHKO) or move.target == .Depends);
|
|
1115
|
+
return Rolls.criticalHit(battle, player, @intCast(rate), spurious, options);
|
|
1126
1116
|
}
|
|
1127
1117
|
|
|
1128
1118
|
fn calcDamage(
|
|
@@ -1131,6 +1121,7 @@ fn calcDamage(
|
|
|
1131
1121
|
target_player: Player,
|
|
1132
1122
|
m: ?Move.Data,
|
|
1133
1123
|
crit: bool,
|
|
1124
|
+
options: anytype,
|
|
1134
1125
|
) bool {
|
|
1135
1126
|
// Confusion (indicated when m == null) just needs a 40 BP physical move
|
|
1136
1127
|
const cfz = m == null;
|
|
@@ -1142,28 +1133,8 @@ fn calcDamage(
|
|
|
1142
1133
|
|
|
1143
1134
|
const special = move.type.special();
|
|
1144
1135
|
|
|
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
|
|
1136
|
+
var atk = attack(side, crit, special);
|
|
1137
|
+
var def = defense(target, crit, special, cfz);
|
|
1167
1138
|
|
|
1168
1139
|
// Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
|
|
1169
1140
|
// will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
|
|
@@ -1171,12 +1142,12 @@ fn calcDamage(
|
|
|
1171
1142
|
if ((!showdown or !cfz) and (atk > 255 or def > 255)) {
|
|
1172
1143
|
atk = @max((atk / 4) & 255, 1);
|
|
1173
1144
|
// 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 (
|
|
1145
|
+
def = @max((def / 4) & 255, if (pkmn.options.mod) 1 else 0);
|
|
1175
1146
|
}
|
|
1176
1147
|
|
|
1177
|
-
const lvl =
|
|
1148
|
+
const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
|
|
1178
1149
|
|
|
1179
|
-
def =
|
|
1150
|
+
def = if (move.effect == .Explode) @max(def / 2, 1) else def;
|
|
1180
1151
|
|
|
1181
1152
|
if (def == 0) return false;
|
|
1182
1153
|
|
|
@@ -1188,11 +1159,41 @@ fn calcDamage(
|
|
|
1188
1159
|
d = @min(997, d);
|
|
1189
1160
|
d += 2;
|
|
1190
1161
|
|
|
1162
|
+
// Whether or not a critical hit occurred can change whether a division-by-zero freeze is
|
|
1163
|
+
// encountered so we need to make sure that in the event of a miss we commit the status of the
|
|
1164
|
+
// crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
|
|
1165
|
+
// that results in counter intuitive probabilities in the common case
|
|
1166
|
+
if (pkmn.options.chance and !pkmn.options.mod and move.effect != .Explode) {
|
|
1167
|
+
atk = attack(side, !crit, special);
|
|
1168
|
+
def = defense(target, !crit, special, false);
|
|
1169
|
+
if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1191
1172
|
battle.last_damage = @intCast(d);
|
|
1192
1173
|
|
|
1193
1174
|
return true;
|
|
1194
1175
|
}
|
|
1195
1176
|
|
|
1177
|
+
fn attack(side: *Side, crit: bool, special: bool) u32 {
|
|
1178
|
+
return if (crit)
|
|
1179
|
+
if (special) side.stored().stats.spc else side.stored().stats.atk
|
|
1180
|
+
else if (special) side.active.stats.spc else side.active.stats.atk;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
fn defense(target: *Side, crit: bool, special: bool, cfz: bool) u32 {
|
|
1184
|
+
return if (crit)
|
|
1185
|
+
if (special) target.stored().stats.spc else target.stored().stats.def
|
|
1186
|
+
else
|
|
1187
|
+
// GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
|
|
1188
|
+
if (special)
|
|
1189
|
+
target.active.stats.spc *
|
|
1190
|
+
@as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
|
|
1191
|
+
else
|
|
1192
|
+
// Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
|
|
1193
|
+
target.active.stats.def *
|
|
1194
|
+
@as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1196
1197
|
fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
1197
1198
|
const side = battle.side(player);
|
|
1198
1199
|
const foe = battle.foe(player);
|
|
@@ -1237,7 +1238,6 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1237
1238
|
const foe = battle.foe(player);
|
|
1238
1239
|
|
|
1239
1240
|
if (!try checkHit(battle, player, move, options)) return null;
|
|
1240
|
-
try options.chance.commit(player, .hit);
|
|
1241
1241
|
|
|
1242
1242
|
battle.last_damage = switch (side.last_selected_move) {
|
|
1243
1243
|
.SuperFang => @max(foe.stored().hp / 2, 1),
|
|
@@ -1248,13 +1248,13 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1248
1248
|
.Psywave => power: {
|
|
1249
1249
|
const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
|
|
1250
1250
|
// GLITCH: Psywave infinite glitch loop
|
|
1251
|
-
if (!
|
|
1251
|
+
if (!pkmn.options.mod and max <= 1) return .Error;
|
|
1252
1252
|
break :power try Rolls.psywave(battle, player, max, options);
|
|
1253
1253
|
},
|
|
1254
1254
|
else => unreachable,
|
|
1255
1255
|
};
|
|
1256
1256
|
|
|
1257
|
-
if (battle.last_damage == 0) return if (
|
|
1257
|
+
if (battle.last_damage == 0) return if (pkmn.options.mod) null else .Error;
|
|
1258
1258
|
|
|
1259
1259
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1260
1260
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
@@ -1267,44 +1267,43 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1267
1267
|
const foe = battle.foe(player);
|
|
1268
1268
|
|
|
1269
1269
|
if (battle.last_damage == 0) {
|
|
1270
|
-
try options.log.fail(battle.active(player), .None);
|
|
1270
|
+
try options.log.fail(.{ battle.active(player), .None });
|
|
1271
1271
|
return null;
|
|
1272
1272
|
}
|
|
1273
1273
|
|
|
1274
1274
|
// Pretend Splash was used as a stand-in when no move has been used to fail below with 0 BP
|
|
1275
1275
|
const foe_last_selected_move =
|
|
1276
1276
|
Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
const used = (if (player.foe() == .P1)
|
|
1282
|
-
battle.last_moves.p1_counterable
|
|
1283
|
-
else
|
|
1284
|
-
battle.last_moves.p2_counterable) != 0;
|
|
1277
|
+
.Splash
|
|
1278
|
+
else
|
|
1279
|
+
foe.last_selected_move);
|
|
1285
1280
|
|
|
1281
|
+
const used = battle.lastMove(player.foe()).counterable != 0;
|
|
1286
1282
|
const selected = foe_last_selected_move.bp > 0 and
|
|
1287
1283
|
foe.last_selected_move != .Counter and
|
|
1288
1284
|
(foe_last_selected_move.type == .Normal or
|
|
1289
|
-
|
|
1285
|
+
foe_last_selected_move.type == .Fighting);
|
|
1290
1286
|
|
|
1291
1287
|
if (!used and !selected) {
|
|
1292
|
-
try options.log.fail(battle.active(player), .None);
|
|
1288
|
+
try options.log.fail(.{ battle.active(player), .None });
|
|
1293
1289
|
return null;
|
|
1294
1290
|
}
|
|
1295
1291
|
|
|
1296
1292
|
if (!used or !selected) {
|
|
1297
1293
|
// GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
|
|
1298
|
-
if (!
|
|
1299
|
-
try options.log.fail(battle.active(player), .None);
|
|
1294
|
+
if (!pkmn.options.mod) return .Error;
|
|
1295
|
+
try options.log.fail(.{ battle.active(player), .None });
|
|
1300
1296
|
return null;
|
|
1301
1297
|
}
|
|
1302
1298
|
|
|
1303
|
-
|
|
1299
|
+
// NOTE: llvm/llvm-project#58557
|
|
1300
|
+
battle.last_damage = if (comptime builtin.target.cpu.arch.isWasm())
|
|
1301
|
+
@max(battle.last_damage *% 2, std.math.maxInt(u16))
|
|
1302
|
+
else
|
|
1303
|
+
battle.last_damage *| 2;
|
|
1304
1304
|
|
|
1305
|
-
// Pokémon Showdown calls
|
|
1305
|
+
// Pokémon Showdown calls moveHit before Counter
|
|
1306
1306
|
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
1307
|
-
try options.chance.commit(player, .hit);
|
|
1308
1307
|
|
|
1309
1308
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1310
1309
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
@@ -1332,20 +1331,20 @@ fn applyDamage(
|
|
|
1332
1331
|
subbed.active.volatiles.substitute = 0;
|
|
1333
1332
|
subbed.active.volatiles.Substitute = false;
|
|
1334
1333
|
// 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(
|
|
1334
|
+
try options.log.end(.{ battle.active(sub_player), .Substitute });
|
|
1335
|
+
if (reason != .Confusion) options.calc.capped(sub_player);
|
|
1337
1336
|
return true;
|
|
1338
1337
|
} else {
|
|
1339
1338
|
// Safe to truncate since less than subbed.volatiles.substitute which is a u8
|
|
1340
1339
|
subbed.active.volatiles.substitute -= @intCast(battle.last_damage);
|
|
1341
|
-
try options.log.activate(battle.active(sub_player), .Substitute);
|
|
1340
|
+
try options.log.activate(.{ battle.active(sub_player), .Substitute });
|
|
1342
1341
|
return false;
|
|
1343
1342
|
}
|
|
1344
1343
|
}
|
|
1345
1344
|
|
|
1346
1345
|
if (battle.last_damage > target.stored().hp) battle.last_damage = target.stored().hp;
|
|
1347
1346
|
target.stored().hp -= battle.last_damage;
|
|
1348
|
-
try options.log.damage(battle.active(target_player), target.stored(), reason);
|
|
1347
|
+
try options.log.damage(.{ battle.active(target_player), target.stored(), reason });
|
|
1349
1348
|
return false;
|
|
1350
1349
|
}
|
|
1351
1350
|
|
|
@@ -1357,17 +1356,17 @@ fn mirrorMove(
|
|
|
1357
1356
|
auto: bool,
|
|
1358
1357
|
residual: *bool,
|
|
1359
1358
|
options: anytype,
|
|
1360
|
-
) !?Result {
|
|
1359
|
+
) anyerror!?Result {
|
|
1361
1360
|
var side = battle.side(player);
|
|
1362
1361
|
const foe = battle.foe(player);
|
|
1363
1362
|
|
|
1364
1363
|
side.last_selected_move = foe.last_used_move;
|
|
1365
1364
|
|
|
1366
1365
|
if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
|
|
1367
|
-
try options.log.fail(battle.active(player), .None);
|
|
1366
|
+
try options.log.fail(.{ battle.active(player), .None });
|
|
1368
1367
|
return null;
|
|
1369
1368
|
} else if (!showdown or foe.last_used_move != .Struggle) {
|
|
1370
|
-
incrementPP(side, mslot);
|
|
1369
|
+
incrementPP(side, player, mslot, options);
|
|
1371
1370
|
}
|
|
1372
1371
|
|
|
1373
1372
|
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
|
|
@@ -1384,11 +1383,11 @@ fn metronome(
|
|
|
1384
1383
|
auto: bool,
|
|
1385
1384
|
residual: *bool,
|
|
1386
1385
|
options: anytype,
|
|
1387
|
-
) !?Result {
|
|
1386
|
+
) anyerror!?Result {
|
|
1388
1387
|
var side = battle.side(player);
|
|
1389
1388
|
|
|
1390
1389
|
side.last_selected_move = try Rolls.metronome(battle, player, options);
|
|
1391
|
-
incrementPP(side, mslot);
|
|
1390
|
+
incrementPP(side, player, mslot, options);
|
|
1392
1391
|
|
|
1393
1392
|
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
|
|
1394
1393
|
return null;
|
|
@@ -1400,20 +1399,23 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
|
|
|
1400
1399
|
var immune = false;
|
|
1401
1400
|
var mist = false;
|
|
1402
1401
|
|
|
1403
|
-
if (moveHit(battle, player, move, &immune, &mist, options))
|
|
1402
|
+
if (try moveHit(battle, player, move, &immune, &mist, options)) {
|
|
1403
|
+
if (!showdown) try options.chance.commit(player, .hit);
|
|
1404
|
+
return true;
|
|
1405
|
+
}
|
|
1404
1406
|
|
|
1405
1407
|
assert(!immune);
|
|
1406
1408
|
if (mist) {
|
|
1407
1409
|
assert(!showdown);
|
|
1408
1410
|
const foe_ident = battle.active(player.foe());
|
|
1409
|
-
try options.log.activate(foe_ident, .Mist);
|
|
1410
|
-
try options.log.fail(foe_ident, .None);
|
|
1411
|
+
try options.log.activate(.{ foe_ident, .Mist });
|
|
1412
|
+
try options.log.fail(.{ foe_ident, .None });
|
|
1411
1413
|
} else {
|
|
1412
|
-
try options.
|
|
1413
|
-
try options.log.
|
|
1414
|
-
try options.log.miss(battle.active(player));
|
|
1414
|
+
try options.log.lastmiss(.{});
|
|
1415
|
+
try options.log.miss(.{battle.active(player)});
|
|
1415
1416
|
}
|
|
1416
1417
|
|
|
1418
|
+
if (!showdown) try options.chance.commit(player, .miss);
|
|
1417
1419
|
return false;
|
|
1418
1420
|
}
|
|
1419
1421
|
|
|
@@ -1424,7 +1426,7 @@ fn moveHit(
|
|
|
1424
1426
|
immune: *bool,
|
|
1425
1427
|
mist: *bool,
|
|
1426
1428
|
options: anytype,
|
|
1427
|
-
) bool {
|
|
1429
|
+
) !bool {
|
|
1428
1430
|
var side = battle.side(player);
|
|
1429
1431
|
const foe = battle.foe(player);
|
|
1430
1432
|
|
|
@@ -1444,7 +1446,16 @@ fn moveHit(
|
|
|
1444
1446
|
break :miss true;
|
|
1445
1447
|
}
|
|
1446
1448
|
if (!showdown) {
|
|
1447
|
-
if (move.effect == .Swift)
|
|
1449
|
+
if (move.effect == .Swift) {
|
|
1450
|
+
// If Swift could cause a division-by-zero freeze (which doesn't exist on Pokémon
|
|
1451
|
+
// Showdown, hence why the block above doesn't include this code), we need the
|
|
1452
|
+
// damage roll to be committed. However, the commit code will not save this roll if
|
|
1453
|
+
// the glitch occurs and the move was not marked as having hit. To work around this
|
|
1454
|
+
// we flag that the move hit (as Swift trivially does), but pass the probability of
|
|
1455
|
+
// the hit as zero to ensure it doesn't actually get saved.
|
|
1456
|
+
try options.chance.hit(player, true, 0);
|
|
1457
|
+
return true;
|
|
1458
|
+
}
|
|
1448
1459
|
if (foe.active.volatiles.Invulnerable) break :miss true;
|
|
1449
1460
|
}
|
|
1450
1461
|
|
|
@@ -1463,7 +1474,7 @@ fn moveHit(
|
|
|
1463
1474
|
const overwritten = overwrite and state > 0;
|
|
1464
1475
|
assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
|
|
1465
1476
|
|
|
1466
|
-
var accuracy = if (overwritten) state else
|
|
1477
|
+
var accuracy: u16 = if (overwritten) state else move.accuracy;
|
|
1467
1478
|
var boost = BOOSTS[@as(u4, @intCast(@as(i8, side.active.boosts.accuracy) + 6))];
|
|
1468
1479
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1469
1480
|
boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
|
|
@@ -1475,7 +1486,7 @@ fn moveHit(
|
|
|
1475
1486
|
if (overwrite) side.active.volatiles.state = accuracy;
|
|
1476
1487
|
|
|
1477
1488
|
// GLITCH: max accuracy is 255 so 1/256 chance of miss
|
|
1478
|
-
break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
|
|
1489
|
+
break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
|
|
1479
1490
|
};
|
|
1480
1491
|
|
|
1481
1492
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1486,13 +1497,17 @@ fn moveHit(
|
|
|
1486
1497
|
|
|
1487
1498
|
if (!miss) return true;
|
|
1488
1499
|
if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
|
|
1500
|
+
|
|
1489
1501
|
side.active.volatiles.Binding = false;
|
|
1502
|
+
options.chance.observe(.binding, player, .None);
|
|
1503
|
+
|
|
1490
1504
|
return false;
|
|
1491
1505
|
}
|
|
1492
1506
|
|
|
1493
1507
|
fn checkFaint(
|
|
1494
1508
|
battle: anytype,
|
|
1495
1509
|
player: Player,
|
|
1510
|
+
cap: bool,
|
|
1496
1511
|
options: anytype,
|
|
1497
1512
|
) @TypeOf(options.log).Error!?Result {
|
|
1498
1513
|
const side = battle.side(player);
|
|
@@ -1502,33 +1517,32 @@ fn checkFaint(
|
|
|
1502
1517
|
const foe_fainted = foe.stored().hp == 0;
|
|
1503
1518
|
|
|
1504
1519
|
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;
|
|
1520
|
+
const foe_out = foe_fainted and findFirstAlive(foe) == 0;
|
|
1521
|
+
const more = player_out or foe_out;
|
|
1508
1522
|
|
|
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;
|
|
1523
|
+
if (try faint(battle, player, !(more or foe_fainted), cap, options)) |r| return r;
|
|
1524
|
+
if (foe_fainted) if (try faint(battle, player.foe(), !more, false, options)) |r| return r;
|
|
1511
1525
|
|
|
1512
1526
|
assert(!side.active.volatiles.MultiHit);
|
|
1513
1527
|
assert(!foe.active.volatiles.MultiHit);
|
|
1514
1528
|
|
|
1515
|
-
if (
|
|
1516
|
-
try options.log.tie();
|
|
1517
|
-
return
|
|
1529
|
+
if (player_out and foe_out) {
|
|
1530
|
+
try options.log.tie(.{});
|
|
1531
|
+
return .Tie;
|
|
1518
1532
|
} else if (player_out) {
|
|
1519
|
-
try options.log.win(player.foe());
|
|
1520
|
-
return if (player == .P1)
|
|
1533
|
+
try options.log.win(.{player.foe()});
|
|
1534
|
+
return if (player == .P1) .Lose else .Win;
|
|
1521
1535
|
} else if (foe_out) {
|
|
1522
|
-
try options.log.win(player);
|
|
1523
|
-
return if (player == .P1)
|
|
1536
|
+
try options.log.win(.{player});
|
|
1537
|
+
return if (player == .P1) .Win else .Lose;
|
|
1524
1538
|
}
|
|
1525
1539
|
|
|
1526
1540
|
const foe_choice: Choice.Type = if (foe_fainted) .Switch else .Pass;
|
|
1527
|
-
if (player == .P1) return
|
|
1528
|
-
return
|
|
1541
|
+
if (player == .P1) return .{ .p1 = .Switch, .p2 = foe_choice };
|
|
1542
|
+
return .{ .p1 = foe_choice, .p2 = .Switch };
|
|
1529
1543
|
}
|
|
1530
1544
|
|
|
1531
|
-
fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
|
|
1545
|
+
fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytype) !?Result {
|
|
1532
1546
|
var side = battle.side(player);
|
|
1533
1547
|
var foe = battle.foe(player);
|
|
1534
1548
|
assert(side.stored().hp == 0);
|
|
@@ -1537,8 +1551,8 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
|
|
|
1537
1551
|
assert(!foe_volatiles.MultiHit);
|
|
1538
1552
|
if (foe_volatiles.Bide) {
|
|
1539
1553
|
assert(!foe_volatiles.Thrashing and !foe_volatiles.Rage);
|
|
1540
|
-
foe_volatiles.state = if (
|
|
1541
|
-
if (foe_volatiles.state != 0) return
|
|
1554
|
+
foe_volatiles.state = if (pkmn.options.mod) 0 else foe_volatiles.state & 255;
|
|
1555
|
+
if (foe_volatiles.state != 0) return .Error;
|
|
1542
1556
|
}
|
|
1543
1557
|
|
|
1544
1558
|
// Clearing these is not strictly necessary as provided the battle hasn't ended the side that
|
|
@@ -1561,8 +1575,10 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
|
|
|
1561
1575
|
side.stored().stats.spe;
|
|
1562
1576
|
}
|
|
1563
1577
|
|
|
1564
|
-
try options.log.faint(battle.active(player), done);
|
|
1565
|
-
|
|
1578
|
+
try options.log.faint(.{ battle.active(player), done });
|
|
1579
|
+
// We can only safely set capped if the faint occured as the direct result of an opponent's
|
|
1580
|
+
// move (as opposed to due to residual damage, recoil, Explosion, etc)
|
|
1581
|
+
if (cap) options.calc.capped(player);
|
|
1566
1582
|
return null;
|
|
1567
1583
|
}
|
|
1568
1584
|
|
|
@@ -1573,21 +1589,19 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
|
|
|
1573
1589
|
var volatiles = &side.active.volatiles;
|
|
1574
1590
|
|
|
1575
1591
|
const brn = Status.is(stored.status, .BRN);
|
|
1576
|
-
if (brn or Status.is(stored.status, .PSN))
|
|
1592
|
+
if (brn or Status.is(stored.status, .PSN)) {
|
|
1577
1593
|
var damage = @max(stored.stats.hp / 16, 1);
|
|
1578
|
-
|
|
1579
1594
|
if (volatiles.Toxic) {
|
|
1580
1595
|
volatiles.toxic += 1;
|
|
1581
1596
|
damage *= volatiles.toxic;
|
|
1582
1597
|
}
|
|
1598
|
+
damage = @min(damage, stored.hp);
|
|
1583
1599
|
|
|
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);
|
|
1600
|
+
if (!showdown or damage > 0) {
|
|
1601
|
+
stored.hp -= damage;
|
|
1602
|
+
// Pokémon Showdown uses damage [of] here but its not relevant in Generation I
|
|
1603
|
+
try options.log.damage(.{ ident, stored, if (brn) Damage.Burn else Damage.Poison });
|
|
1604
|
+
}
|
|
1591
1605
|
}
|
|
1592
1606
|
|
|
1593
1607
|
if (volatiles.LeechSeed) {
|
|
@@ -1611,14 +1625,14 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
|
|
|
1611
1625
|
const amount = @min(damage, stored.hp);
|
|
1612
1626
|
stored.hp -= amount;
|
|
1613
1627
|
|
|
1614
|
-
// As above, Pokémon Showdown uses
|
|
1615
|
-
if (amount > 0) try options.log.damage(ident, stored, .LeechSeed);
|
|
1628
|
+
// As above, Pokémon Showdown uses damage [of] but its not relevant
|
|
1629
|
+
if (amount > 0) try options.log.damage(.{ ident, stored, .LeechSeed });
|
|
1616
1630
|
|
|
1617
1631
|
const before = foe_stored.hp;
|
|
1618
1632
|
// Uncapped damage is added back to the foe
|
|
1619
1633
|
foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
|
|
1620
1634
|
// 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);
|
|
1635
|
+
if (foe_stored.hp > before) try options.log.heal(.{ foe_ident, foe_stored, .Silent });
|
|
1622
1636
|
}
|
|
1623
1637
|
}
|
|
1624
1638
|
|
|
@@ -1628,21 +1642,21 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
|
|
|
1628
1642
|
|
|
1629
1643
|
battle.turn += 1;
|
|
1630
1644
|
|
|
1631
|
-
if (
|
|
1632
|
-
try options.log.tie();
|
|
1633
|
-
return
|
|
1645
|
+
if (pkmn.options.mod and pkmn.options.ebc and checkEBC(battle)) {
|
|
1646
|
+
try options.log.tie(.{});
|
|
1647
|
+
return .Tie;
|
|
1634
1648
|
}
|
|
1635
1649
|
|
|
1636
|
-
if (
|
|
1637
|
-
try options.log.tie();
|
|
1638
|
-
return
|
|
1650
|
+
if (pkmn.options.mod and battle.turn >= 1000) {
|
|
1651
|
+
try options.log.tie(.{});
|
|
1652
|
+
return .Tie;
|
|
1639
1653
|
} else if (battle.turn >= 65535) {
|
|
1640
|
-
return
|
|
1654
|
+
return .Error;
|
|
1641
1655
|
}
|
|
1642
1656
|
|
|
1643
|
-
try options.log.turn(battle.turn);
|
|
1657
|
+
try options.log.turn(.{battle.turn});
|
|
1644
1658
|
|
|
1645
|
-
return
|
|
1659
|
+
return .Default;
|
|
1646
1660
|
}
|
|
1647
1661
|
|
|
1648
1662
|
fn checkEBC(battle: anytype) bool {
|
|
@@ -1700,9 +1714,9 @@ fn checkEBC(battle: anytype) bool {
|
|
|
1700
1714
|
}
|
|
1701
1715
|
|
|
1702
1716
|
fn disabled(side: *Side, ident: ID, options: anytype) !bool {
|
|
1703
|
-
if (side.active.volatiles.
|
|
1717
|
+
if (side.active.volatiles.disable_move != 0) {
|
|
1704
1718
|
// A Pokémon that transforms after being disabled may end up with less move slots
|
|
1705
|
-
const m = side.active.moves[side.active.volatiles.
|
|
1719
|
+
const m = side.active.moves[side.active.volatiles.disable_move - 1].id;
|
|
1706
1720
|
// side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
|
|
1707
1721
|
const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
|
|
1708
1722
|
m
|
|
@@ -1710,32 +1724,21 @@ fn disabled(side: *Side, ident: ID, options: anytype) !bool {
|
|
|
1710
1724
|
side.last_selected_move;
|
|
1711
1725
|
if (m != .None and m == last) {
|
|
1712
1726
|
side.active.volatiles.Charging = false;
|
|
1713
|
-
try options.log.
|
|
1727
|
+
try options.log.cant(.{ ident, .Disable, last });
|
|
1714
1728
|
return true;
|
|
1715
1729
|
}
|
|
1716
1730
|
}
|
|
1717
1731
|
return false;
|
|
1718
1732
|
}
|
|
1719
1733
|
|
|
1720
|
-
|
|
1734
|
+
fn buildRage(battle: anytype, who: Player, options: anytype) !void {
|
|
1721
1735
|
const side = battle.side(who);
|
|
1722
1736
|
if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
|
|
1723
1737
|
try Effects.boost(battle, who, Move.get(.Rage), options);
|
|
1724
1738
|
}
|
|
1725
1739
|
}
|
|
1726
1740
|
|
|
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(
|
|
1741
|
+
fn onBegin(
|
|
1739
1742
|
battle: anytype,
|
|
1740
1743
|
player: Player,
|
|
1741
1744
|
move: Move.Data,
|
|
@@ -1743,7 +1746,6 @@ inline fn onBegin(
|
|
|
1743
1746
|
residual: *bool,
|
|
1744
1747
|
options: anytype,
|
|
1745
1748
|
) !void {
|
|
1746
|
-
assert(move.effect.onBegin());
|
|
1747
1749
|
return switch (move.effect) {
|
|
1748
1750
|
.Confusion => Effects.confusion(battle, player, move, options),
|
|
1749
1751
|
.Conversion => Effects.conversion(battle, player, options),
|
|
@@ -1765,8 +1767,7 @@ inline fn onBegin(
|
|
|
1765
1767
|
};
|
|
1766
1768
|
}
|
|
1767
1769
|
|
|
1768
|
-
|
|
1769
|
-
assert(move.effect.onEnd());
|
|
1770
|
+
fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1770
1771
|
return switch (move.effect) {
|
|
1771
1772
|
.Bide => Effects.bide(battle, player, options),
|
|
1772
1773
|
// zig fmt: off
|
|
@@ -1781,14 +1782,13 @@ inline fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anyty
|
|
|
1781
1782
|
};
|
|
1782
1783
|
}
|
|
1783
1784
|
|
|
1784
|
-
|
|
1785
|
+
fn alwaysHappens(
|
|
1785
1786
|
battle: anytype,
|
|
1786
1787
|
player: Player,
|
|
1787
1788
|
move: Move.Data,
|
|
1788
1789
|
residual: *bool,
|
|
1789
1790
|
options: anytype,
|
|
1790
1791
|
) !void {
|
|
1791
|
-
assert(move.effect.alwaysHappens());
|
|
1792
1792
|
return switch (move.effect) {
|
|
1793
1793
|
.DrainHP, .DreamEater => Effects.drainHP(battle, player, options),
|
|
1794
1794
|
.Explode => Effects.explode(battle, player),
|
|
@@ -1800,7 +1800,7 @@ inline fn alwaysHappens(
|
|
|
1800
1800
|
};
|
|
1801
1801
|
}
|
|
1802
1802
|
|
|
1803
|
-
|
|
1803
|
+
fn moveEffect(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1804
1804
|
return switch (move.effect) {
|
|
1805
1805
|
.BurnChance1, .BurnChance2 => Effects.burnChance(battle, player, move, options),
|
|
1806
1806
|
.ConfusionChance => Effects.confusion(battle, player, move, options),
|
|
@@ -1824,10 +1824,12 @@ pub const Effects = struct {
|
|
|
1824
1824
|
|
|
1825
1825
|
side.active.volatiles.Bide = true;
|
|
1826
1826
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1827
|
+
|
|
1827
1828
|
side.active.volatiles.state = 0;
|
|
1828
1829
|
side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
1830
|
+
options.chance.observe(.attacking, player, .started);
|
|
1829
1831
|
|
|
1830
|
-
try options.log.start(battle.active(player), .Bide);
|
|
1832
|
+
try options.log.start(.{ battle.active(player), .Bide });
|
|
1831
1833
|
}
|
|
1832
1834
|
|
|
1833
1835
|
fn burnChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -1841,11 +1843,11 @@ pub const Effects = struct {
|
|
|
1841
1843
|
// GLITCH: Freeze top move selection desync can occur if thawed Pokémon is slower
|
|
1842
1844
|
if (Status.is(foe_stored.status, .FRZ)) {
|
|
1843
1845
|
assert(move.type == .Fire);
|
|
1844
|
-
try options.log.curestatus(
|
|
1846
|
+
try options.log.curestatus(.{
|
|
1845
1847
|
battle.active(player.foe()),
|
|
1846
1848
|
foe_stored.status,
|
|
1847
1849
|
.Message,
|
|
1848
|
-
);
|
|
1850
|
+
});
|
|
1849
1851
|
foe_stored.status = 0;
|
|
1850
1852
|
}
|
|
1851
1853
|
return;
|
|
@@ -1859,7 +1861,7 @@ pub const Effects = struct {
|
|
|
1859
1861
|
foe_stored.status = Status.init(.BRN);
|
|
1860
1862
|
foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
|
|
1861
1863
|
|
|
1862
|
-
try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
|
|
1864
|
+
try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
|
|
1863
1865
|
}
|
|
1864
1866
|
|
|
1865
1867
|
fn charge(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -1869,8 +1871,8 @@ pub const Effects = struct {
|
|
|
1869
1871
|
volatiles.Charging = true;
|
|
1870
1872
|
const move = side.last_selected_move;
|
|
1871
1873
|
if (move == .Fly or move == .Dig) volatiles.Invulnerable = true;
|
|
1872
|
-
try options.log.laststill();
|
|
1873
|
-
try options.log.prepare(battle.active(player), move);
|
|
1874
|
+
try options.log.laststill(.{});
|
|
1875
|
+
try options.log.prepare(.{ battle.active(player), move });
|
|
1874
1876
|
}
|
|
1875
1877
|
|
|
1876
1878
|
fn confusion(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -1881,42 +1883,39 @@ pub const Effects = struct {
|
|
|
1881
1883
|
if (!try Rolls.confusionChance(battle, player, options)) return;
|
|
1882
1884
|
} else {
|
|
1883
1885
|
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
|
-
}
|
|
1886
|
+
if (!try checkHit(battle, player, move, options)) return;
|
|
1887
|
+
if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
1889
1888
|
} else {
|
|
1890
|
-
if (sub) {
|
|
1891
|
-
|
|
1892
|
-
} else if (!try checkHit(battle, player, move, options)) {
|
|
1893
|
-
return;
|
|
1894
|
-
}
|
|
1889
|
+
if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
1890
|
+
if (!try checkHit(battle, player, move, options)) return;
|
|
1895
1891
|
}
|
|
1896
|
-
try options.chance.commit(player, .hit);
|
|
1897
1892
|
}
|
|
1898
1893
|
|
|
1899
1894
|
if (foe.active.volatiles.Confusion) return;
|
|
1900
1895
|
foe.active.volatiles.Confusion = true;
|
|
1901
|
-
foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
|
|
1902
1896
|
|
|
1903
|
-
|
|
1897
|
+
foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
1898
|
+
options.chance.observe(.confusion, player.foe(), .started);
|
|
1899
|
+
options.calc.confusion(player.foe());
|
|
1900
|
+
|
|
1901
|
+
try options.log.start(.{ battle.active(player.foe()), .Confusion });
|
|
1904
1902
|
}
|
|
1905
1903
|
|
|
1906
1904
|
fn conversion(battle: anytype, player: Player, options: anytype) !void {
|
|
1907
1905
|
const foe = battle.foe(player);
|
|
1908
1906
|
|
|
1909
1907
|
if (foe.active.volatiles.Invulnerable) {
|
|
1910
|
-
try options.log.lastmiss();
|
|
1911
|
-
return options.log.miss(battle.active(player));
|
|
1908
|
+
try options.log.lastmiss(.{});
|
|
1909
|
+
return options.log.miss(.{battle.active(player)});
|
|
1912
1910
|
}
|
|
1913
1911
|
|
|
1914
1912
|
battle.side(player).active.types = foe.active.types;
|
|
1915
|
-
return options.log.
|
|
1913
|
+
return options.log.start(.{
|
|
1916
1914
|
battle.active(player),
|
|
1915
|
+
.TypeChange,
|
|
1917
1916
|
foe.active.types,
|
|
1918
1917
|
battle.active(player.foe()),
|
|
1919
|
-
);
|
|
1918
|
+
});
|
|
1920
1919
|
}
|
|
1921
1920
|
|
|
1922
1921
|
fn disable(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
|
|
@@ -1925,13 +1924,10 @@ pub const Effects = struct {
|
|
|
1925
1924
|
const foe_ident = battle.active(player.foe());
|
|
1926
1925
|
|
|
1927
1926
|
// 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
|
-
}
|
|
1927
|
+
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
1932
1928
|
|
|
1933
|
-
if (volatiles.
|
|
1934
|
-
try options.log.fail(foe_ident, .None);
|
|
1929
|
+
if (volatiles.disable_move != 0) {
|
|
1930
|
+
try options.log.fail(.{ foe_ident, .None });
|
|
1935
1931
|
return null;
|
|
1936
1932
|
}
|
|
1937
1933
|
|
|
@@ -1946,27 +1942,27 @@ pub const Effects = struct {
|
|
|
1946
1942
|
|
|
1947
1943
|
// Technically this is still considered simply a "miss" on the cartridge,
|
|
1948
1944
|
// but diverging from Pokémon Showdown here would mostly just be pedantic
|
|
1949
|
-
if (n == 0) {
|
|
1950
|
-
try options.log.fail(foe_ident, .None);
|
|
1945
|
+
if (n == 0 or (pkmn.options.mod and err)) {
|
|
1946
|
+
try options.log.fail(.{ foe_ident, .None });
|
|
1951
1947
|
return null;
|
|
1952
1948
|
} else if (err) {
|
|
1953
1949
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
1954
|
-
|
|
1955
|
-
return Result.Error;
|
|
1950
|
+
return .Error;
|
|
1956
1951
|
}
|
|
1957
1952
|
|
|
1958
|
-
volatiles.
|
|
1953
|
+
volatiles.disable_move =
|
|
1959
1954
|
@intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
|
|
1960
|
-
volatiles.
|
|
1955
|
+
volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
|
|
1956
|
+
options.chance.observe(.disable, player.foe(), .started);
|
|
1961
1957
|
|
|
1962
|
-
const id = foe.active.move(volatiles.
|
|
1963
|
-
try options.log.
|
|
1958
|
+
const id = foe.active.move(volatiles.disable_move).id;
|
|
1959
|
+
try options.log.start(.{ foe_ident, .Disable, id });
|
|
1964
1960
|
return null;
|
|
1965
1961
|
}
|
|
1966
1962
|
|
|
1967
1963
|
fn drainHP(battle: anytype, player: Player, options: anytype) !void {
|
|
1968
|
-
var
|
|
1969
|
-
|
|
1964
|
+
var stored = battle.side(player).stored();
|
|
1965
|
+
const ident = battle.active(player);
|
|
1970
1966
|
|
|
1971
1967
|
if (battle.last_damage == 0) {
|
|
1972
1968
|
assert(showdown);
|
|
@@ -1979,7 +1975,7 @@ pub const Effects = struct {
|
|
|
1979
1975
|
if (stored.hp == stored.stats.hp) return;
|
|
1980
1976
|
stored.hp = @min(stored.stats.hp, stored.hp + drain);
|
|
1981
1977
|
|
|
1982
|
-
try options.log.
|
|
1978
|
+
try options.log.heal(.{ ident, stored, .Drain, battle.active(player.foe()) });
|
|
1983
1979
|
}
|
|
1984
1980
|
|
|
1985
1981
|
fn explode(battle: anytype, player: Player) !void {
|
|
@@ -2011,7 +2007,7 @@ pub const Effects = struct {
|
|
|
2011
2007
|
if (side.active.volatiles.FocusEnergy) return;
|
|
2012
2008
|
side.active.volatiles.FocusEnergy = true;
|
|
2013
2009
|
|
|
2014
|
-
try options.log.start(battle.active(player), .FocusEnergy);
|
|
2010
|
+
try options.log.start(.{ battle.active(player), .FocusEnergy });
|
|
2015
2011
|
}
|
|
2016
2012
|
|
|
2017
2013
|
fn freezeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2028,12 +2024,12 @@ pub const Effects = struct {
|
|
|
2028
2024
|
if (foe.active.types.includes(move.type)) return;
|
|
2029
2025
|
if (!try Rolls.secondaryChance(battle, player, true, options)) return;
|
|
2030
2026
|
// Freeze Clause Mod
|
|
2031
|
-
if (
|
|
2027
|
+
if (pkmn.options.mod) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
2032
2028
|
|
|
2033
2029
|
foe_stored.status = Status.init(.FRZ);
|
|
2034
2030
|
// GLITCH: Hyper Beam recharging status is not cleared
|
|
2035
2031
|
|
|
2036
|
-
try options.log.status(foe_ident, foe_stored.status, .None);
|
|
2032
|
+
try options.log.status(.{ foe_ident, foe_stored.status, .None });
|
|
2037
2033
|
}
|
|
2038
2034
|
|
|
2039
2035
|
fn haze(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -2041,7 +2037,7 @@ pub const Effects = struct {
|
|
|
2041
2037
|
var side = battle.side(player);
|
|
2042
2038
|
var foe = battle.foe(player);
|
|
2043
2039
|
|
|
2044
|
-
|
|
2040
|
+
const side_stored = side.stored();
|
|
2045
2041
|
var foe_stored = foe.stored();
|
|
2046
2042
|
|
|
2047
2043
|
const player_ident = battle.active(player);
|
|
@@ -2053,8 +2049,8 @@ pub const Effects = struct {
|
|
|
2053
2049
|
side.active.stats = side_stored.stats;
|
|
2054
2050
|
foe.active.stats = foe_stored.stats;
|
|
2055
2051
|
|
|
2056
|
-
try log.activate(player_ident, .Haze);
|
|
2057
|
-
try log.clearallboost();
|
|
2052
|
+
try log.activate(.{ player_ident, .Haze });
|
|
2053
|
+
try log.clearallboost(.{});
|
|
2058
2054
|
|
|
2059
2055
|
// Pokémon Showdown clears P1 then P2 instead of status -> side -> foe
|
|
2060
2056
|
if (showdown) {
|
|
@@ -2062,24 +2058,26 @@ pub const Effects = struct {
|
|
|
2062
2058
|
const p: Player = @enumFromInt(i);
|
|
2063
2059
|
// Pokémon Showdown incorrectly does not prevent sleep/freeze from moving
|
|
2064
2060
|
if (p != player and Status.any(s.stored().status)) {
|
|
2065
|
-
try log.curestatus(foe_ident, foe_stored.status, .Silent);
|
|
2061
|
+
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
2066
2062
|
s.stored().status = 0;
|
|
2063
|
+
options.chance.observe(.sleep, p, .None);
|
|
2067
2064
|
} else if (showdown and s.stored().status == Status.TOX) {
|
|
2068
2065
|
s.stored().status = Status.init(.PSN);
|
|
2069
|
-
try log.status(battle.active(p), s.stored().status, .None);
|
|
2066
|
+
try log.status(.{ battle.active(p), s.stored().status, .None });
|
|
2070
2067
|
}
|
|
2071
|
-
try clearVolatiles(battle, p,
|
|
2068
|
+
try clearVolatiles(battle, p, options);
|
|
2072
2069
|
}
|
|
2073
2070
|
} else {
|
|
2074
2071
|
if (Status.any(foe_stored.status)) {
|
|
2075
2072
|
if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
|
|
2076
2073
|
foe.last_selected_move = .SKIP_TURN;
|
|
2074
|
+
options.chance.observe(.sleep, player.foe(), .None);
|
|
2077
2075
|
}
|
|
2078
|
-
try log.curestatus(foe_ident, foe_stored.status, .Silent);
|
|
2076
|
+
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
2079
2077
|
foe_stored.status = 0;
|
|
2080
2078
|
}
|
|
2081
|
-
try clearVolatiles(battle, player,
|
|
2082
|
-
try clearVolatiles(battle, player.foe(),
|
|
2079
|
+
try clearVolatiles(battle, player, options);
|
|
2080
|
+
try clearVolatiles(battle, player.foe(), options);
|
|
2083
2081
|
}
|
|
2084
2082
|
}
|
|
2085
2083
|
|
|
@@ -2091,7 +2089,7 @@ pub const Effects = struct {
|
|
|
2091
2089
|
// GLITCH: HP recovery move failure glitches
|
|
2092
2090
|
const delta = stored.stats.hp - stored.hp;
|
|
2093
2091
|
if (delta == 0 or (delta & 255 == 255 and stored.hp % 256 != 0)) {
|
|
2094
|
-
return
|
|
2092
|
+
return options.log.fail(.{ ident, .None });
|
|
2095
2093
|
}
|
|
2096
2094
|
|
|
2097
2095
|
const rest = side.last_selected_move == .Rest;
|
|
@@ -2099,17 +2097,17 @@ pub const Effects = struct {
|
|
|
2099
2097
|
// Adding the sleep status runs the sleep condition handler to roll duration
|
|
2100
2098
|
if (showdown) battle.rng.advance(1);
|
|
2101
2099
|
stored.status = Status.slf(2);
|
|
2102
|
-
try options.log.
|
|
2100
|
+
try options.log.status(.{ ident, stored.status, .From, Move.Rest });
|
|
2103
2101
|
stored.hp = stored.stats.hp;
|
|
2104
2102
|
} else {
|
|
2105
2103
|
stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
|
|
2106
2104
|
}
|
|
2107
|
-
try options.log.heal(ident, stored, if (rest) Heal.Silent else Heal.None);
|
|
2105
|
+
try options.log.heal(.{ ident, stored, if (rest) Heal.Silent else Heal.None });
|
|
2108
2106
|
}
|
|
2109
2107
|
|
|
2110
2108
|
fn hyperBeam(battle: anytype, player: Player, options: anytype) !void {
|
|
2111
2109
|
battle.side(player).active.volatiles.Recharging = true;
|
|
2112
|
-
try options.log.mustrecharge(battle.active(player));
|
|
2110
|
+
try options.log.mustrecharge(.{battle.active(player)});
|
|
2113
2111
|
}
|
|
2114
2112
|
|
|
2115
2113
|
fn leechSeed(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2118,33 +2116,31 @@ pub const Effects = struct {
|
|
|
2118
2116
|
if (showdown) {
|
|
2119
2117
|
// Invulnerability trumps type immunity on Pokémon Showdown
|
|
2120
2118
|
if (!foe.active.volatiles.Invulnerable and foe.active.types.includes(.Grass)) {
|
|
2121
|
-
return options.log.immune(battle.active(player.foe()), .None);
|
|
2119
|
+
return options.log.immune(.{ battle.active(player.foe()), .None });
|
|
2122
2120
|
}
|
|
2123
2121
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2124
2122
|
if (foe.active.volatiles.LeechSeed) return;
|
|
2125
2123
|
} else {
|
|
2126
2124
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2127
2125
|
if (foe.active.types.includes(.Grass) or foe.active.volatiles.LeechSeed) {
|
|
2128
|
-
try options.log.lastmiss();
|
|
2129
|
-
return options.log.miss(battle.active(player));
|
|
2126
|
+
try options.log.lastmiss(.{});
|
|
2127
|
+
return options.log.miss(.{battle.active(player)});
|
|
2130
2128
|
}
|
|
2131
2129
|
}
|
|
2132
2130
|
|
|
2133
|
-
try options.chance.commit(player, .hit);
|
|
2134
2131
|
foe.active.volatiles.LeechSeed = true;
|
|
2135
2132
|
|
|
2136
|
-
try options.log.start(battle.active(player.foe()), .LeechSeed);
|
|
2133
|
+
try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
|
|
2137
2134
|
}
|
|
2138
2135
|
|
|
2139
2136
|
fn lightScreen(battle: anytype, player: Player, options: anytype) !void {
|
|
2140
2137
|
var side = battle.side(player);
|
|
2138
|
+
const ident = battle.active(player);
|
|
2141
2139
|
|
|
2142
|
-
if (side.active.volatiles.LightScreen) {
|
|
2143
|
-
return options.log.fail(battle.active(player), .None);
|
|
2144
|
-
}
|
|
2140
|
+
if (side.active.volatiles.LightScreen) return options.log.fail(.{ ident, .None });
|
|
2145
2141
|
side.active.volatiles.LightScreen = true;
|
|
2146
2142
|
|
|
2147
|
-
try options.log.start(
|
|
2143
|
+
try options.log.start(.{ ident, .LightScreen });
|
|
2148
2144
|
}
|
|
2149
2145
|
|
|
2150
2146
|
fn mimic(battle: anytype, player: Player, move: Move.Data, mslot: u8, options: anytype) !void {
|
|
@@ -2172,17 +2168,15 @@ pub const Effects = struct {
|
|
|
2172
2168
|
if (!has_mimic) {
|
|
2173
2169
|
// Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
|
|
2174
2170
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2175
|
-
|
|
2176
|
-
return try options.log.fail(battle.active(player.foe()), .None);
|
|
2171
|
+
return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
2177
2172
|
}
|
|
2178
2173
|
}
|
|
2179
2174
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2180
|
-
try options.chance.commit(player, .hit);
|
|
2181
2175
|
|
|
2182
2176
|
const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
|
|
2183
2177
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
2184
2178
|
|
|
2185
|
-
try options.log.
|
|
2179
|
+
try options.log.start(.{ battle.active(player), .Mimic, side.active.move(oslot).id });
|
|
2186
2180
|
}
|
|
2187
2181
|
|
|
2188
2182
|
fn mist(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -2191,7 +2185,7 @@ pub const Effects = struct {
|
|
|
2191
2185
|
if (side.active.volatiles.Mist) return;
|
|
2192
2186
|
side.active.volatiles.Mist = true;
|
|
2193
2187
|
|
|
2194
|
-
try options.log.start(battle.active(player), .Mist);
|
|
2188
|
+
try options.log.start(.{ battle.active(player), .Mist });
|
|
2195
2189
|
}
|
|
2196
2190
|
|
|
2197
2191
|
fn multiHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2217,25 +2211,25 @@ pub const Effects = struct {
|
|
|
2217
2211
|
|
|
2218
2212
|
if (showdown) {
|
|
2219
2213
|
// Invulnerability trumps type immunity on Pokémon Showdown
|
|
2220
|
-
if (immune and !foe.active.volatiles.Invulnerable)
|
|
2221
|
-
|
|
2214
|
+
if (immune and !foe.active.volatiles.Invulnerable) {
|
|
2215
|
+
return log.immune(.{ foe_ident, .None });
|
|
2216
|
+
} else if (!try checkHit(battle, player, move, options)) return;
|
|
2222
2217
|
}
|
|
2223
2218
|
if (Status.any(foe_stored.status)) {
|
|
2224
|
-
return log.fail(
|
|
2219
|
+
return log.fail(.{
|
|
2225
2220
|
foe_ident,
|
|
2226
|
-
if (Status.is(foe_stored.status, .PAR)) .Paralysis else .None,
|
|
2227
|
-
);
|
|
2221
|
+
if (Status.is(foe_stored.status, .PAR)) Fail.Paralysis else Fail.None,
|
|
2222
|
+
});
|
|
2228
2223
|
}
|
|
2229
2224
|
if (!showdown) {
|
|
2230
|
-
if (immune) return log.immune(foe_ident, .None);
|
|
2225
|
+
if (immune) return log.immune(.{ foe_ident, .None });
|
|
2231
2226
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2232
2227
|
}
|
|
2233
|
-
try options.chance.commit(player, .hit);
|
|
2234
2228
|
|
|
2235
2229
|
foe_stored.status = Status.init(.PAR);
|
|
2236
2230
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
2237
2231
|
|
|
2238
|
-
try log.status(foe_ident, foe_stored.status, .None);
|
|
2232
|
+
try log.status(.{ foe_ident, foe_stored.status, .None });
|
|
2239
2233
|
}
|
|
2240
2234
|
|
|
2241
2235
|
fn paralyzeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2257,12 +2251,12 @@ pub const Effects = struct {
|
|
|
2257
2251
|
foe_stored.status = Status.init(.PAR);
|
|
2258
2252
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
2259
2253
|
|
|
2260
|
-
try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
|
|
2254
|
+
try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
|
|
2261
2255
|
}
|
|
2262
2256
|
|
|
2263
2257
|
fn payDay(battle: anytype, player: Player, options: anytype) !void {
|
|
2264
2258
|
if (!showdown or !battle.foe(player).active.volatiles.Substitute) {
|
|
2265
|
-
try options.log.fieldactivate();
|
|
2259
|
+
try options.log.fieldactivate(.{});
|
|
2266
2260
|
}
|
|
2267
2261
|
}
|
|
2268
2262
|
|
|
@@ -2277,28 +2271,27 @@ pub const Effects = struct {
|
|
|
2277
2271
|
return;
|
|
2278
2272
|
} else if (foe.active.volatiles.Substitute) {
|
|
2279
2273
|
if (move.effect != .Poison) return;
|
|
2280
|
-
return log.fail(foe_ident, .None);
|
|
2274
|
+
return log.fail(.{ foe_ident, .None });
|
|
2281
2275
|
} else if (Status.any(foe_stored.status)) {
|
|
2282
2276
|
if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
|
|
2283
2277
|
// Pokémon Showdown considers Toxic to be a status even in Generation I and so
|
|
2284
2278
|
// 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))
|
|
2279
|
+
return log.fail(.{ foe_ident, if (Status.is(foe_stored.status, .PSN))
|
|
2286
2280
|
if (!showdown)
|
|
2287
|
-
.Poison
|
|
2281
|
+
Fail.Poison
|
|
2288
2282
|
else if (toxic == (foe_stored.status == Status.TOX))
|
|
2289
|
-
if (toxic) .Toxic else .Poison
|
|
2283
|
+
if (toxic) Fail.Toxic else Fail.Poison
|
|
2290
2284
|
else
|
|
2291
|
-
.None
|
|
2285
|
+
Fail.None
|
|
2292
2286
|
else
|
|
2293
|
-
.None);
|
|
2287
|
+
Fail.None });
|
|
2294
2288
|
} else if (foe.active.types.includes(.Poison)) {
|
|
2295
2289
|
if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
|
|
2296
|
-
return log.immune(foe_ident, .None);
|
|
2290
|
+
return log.immune(.{ foe_ident, .None });
|
|
2297
2291
|
}
|
|
2298
2292
|
|
|
2299
2293
|
if (move.effect == .Poison) {
|
|
2300
2294
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2301
|
-
try options.chance.commit(player, .hit);
|
|
2302
2295
|
} else {
|
|
2303
2296
|
if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
|
|
2304
2297
|
return;
|
|
@@ -2312,7 +2305,7 @@ pub const Effects = struct {
|
|
|
2312
2305
|
foe.active.volatiles.toxic = 0;
|
|
2313
2306
|
}
|
|
2314
2307
|
|
|
2315
|
-
try log.status(foe_ident, foe_stored.status, .None);
|
|
2308
|
+
try log.status(.{ foe_ident, foe_stored.status, .None });
|
|
2316
2309
|
}
|
|
2317
2310
|
|
|
2318
2311
|
fn rage(battle: anytype, player: Player) !void {
|
|
@@ -2332,22 +2325,23 @@ pub const Effects = struct {
|
|
|
2332
2325
|
@as(u8, if (side.last_selected_move == .Struggle) 2 else 4), 1));
|
|
2333
2326
|
stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
|
|
2334
2327
|
|
|
2335
|
-
try options.log.
|
|
2328
|
+
try options.log.damage(.{
|
|
2336
2329
|
battle.active(player),
|
|
2337
2330
|
stored,
|
|
2338
2331
|
.RecoilOf,
|
|
2339
2332
|
battle.active(player.foe()),
|
|
2340
|
-
);
|
|
2333
|
+
});
|
|
2341
2334
|
if (showdown and stored.hp == 0) residual.* = false;
|
|
2342
2335
|
}
|
|
2343
2336
|
|
|
2344
2337
|
fn reflect(battle: anytype, player: Player, options: anytype) !void {
|
|
2345
2338
|
var side = battle.side(player);
|
|
2339
|
+
const ident = battle.active(player);
|
|
2346
2340
|
|
|
2347
|
-
if (side.active.volatiles.Reflect) return options.log.fail(
|
|
2341
|
+
if (side.active.volatiles.Reflect) return options.log.fail(.{ ident, .None });
|
|
2348
2342
|
side.active.volatiles.Reflect = true;
|
|
2349
2343
|
|
|
2350
|
-
try options.log.start(
|
|
2344
|
+
try options.log.start(.{ ident, .Reflect });
|
|
2351
2345
|
}
|
|
2352
2346
|
|
|
2353
2347
|
fn sleep(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2361,18 +2355,17 @@ pub const Effects = struct {
|
|
|
2361
2355
|
// the Sleep Clause Mod might activate, causing us to not actually bypass
|
|
2362
2356
|
} else {
|
|
2363
2357
|
if (Status.any(foe_stored.status)) {
|
|
2364
|
-
return options.log.fail(
|
|
2358
|
+
return options.log.fail(.{
|
|
2365
2359
|
foe_ident,
|
|
2366
|
-
if (Status.is(foe_stored.status, .SLP)) .Sleep else .None,
|
|
2367
|
-
);
|
|
2360
|
+
if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
|
|
2361
|
+
});
|
|
2368
2362
|
}
|
|
2369
|
-
// If
|
|
2363
|
+
// If moveHit in doMove didn't return true Pokémon Showdown wouldn't be in here
|
|
2370
2364
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2371
|
-
try options.chance.commit(player, .hit);
|
|
2372
2365
|
}
|
|
2373
2366
|
|
|
2374
2367
|
// Sleep Clause Mod
|
|
2375
|
-
if (
|
|
2368
|
+
if (pkmn.options.mod) {
|
|
2376
2369
|
for (foe.pokemon) |p| {
|
|
2377
2370
|
if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
|
|
2378
2371
|
}
|
|
@@ -2380,19 +2373,20 @@ pub const Effects = struct {
|
|
|
2380
2373
|
foe.active.volatiles.Recharging = false;
|
|
2381
2374
|
|
|
2382
2375
|
foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
|
|
2376
|
+
options.chance.observe(.sleep, player.foe(), .started);
|
|
2377
|
+
|
|
2383
2378
|
const last = battle.side(player).last_selected_move;
|
|
2384
|
-
try options.log.
|
|
2379
|
+
try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
|
|
2385
2380
|
}
|
|
2386
2381
|
|
|
2387
2382
|
fn splash(battle: anytype, player: Player, options: anytype) !void {
|
|
2388
|
-
try options.log.activate(battle.active(player), .Splash);
|
|
2383
|
+
try options.log.activate(.{ battle.active(player), .Splash });
|
|
2389
2384
|
}
|
|
2390
2385
|
|
|
2391
2386
|
fn substitute(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
|
|
2392
2387
|
var side = battle.side(player);
|
|
2393
2388
|
if (side.active.volatiles.Substitute) {
|
|
2394
|
-
|
|
2395
|
-
return;
|
|
2389
|
+
return options.log.fail(.{ battle.active(player), .Substitute });
|
|
2396
2390
|
}
|
|
2397
2391
|
|
|
2398
2392
|
assert(side.stored().stats.hp <= 1023);
|
|
@@ -2404,28 +2398,30 @@ pub const Effects = struct {
|
|
|
2404
2398
|
// avoid having to convert to floating point)
|
|
2405
2399
|
const required_hp = if (showdown) @divFloor(side.stored().stats.hp - 1, 4) + 1 else hp;
|
|
2406
2400
|
if (side.stored().hp < required_hp) {
|
|
2407
|
-
|
|
2408
|
-
return;
|
|
2401
|
+
return options.log.fail(.{ battle.active(player), .Weak });
|
|
2409
2402
|
}
|
|
2410
2403
|
|
|
2411
2404
|
// GLITCH: can leave the user with 0 HP (faints later) because didn't check '<=' above
|
|
2412
2405
|
side.stored().hp -= hp;
|
|
2413
2406
|
side.active.volatiles.substitute = hp + 1;
|
|
2414
2407
|
side.active.volatiles.Substitute = true;
|
|
2415
|
-
try options.log.start(battle.active(player), .Substitute);
|
|
2408
|
+
try options.log.start(.{ battle.active(player), .Substitute });
|
|
2416
2409
|
if (hp > 0) {
|
|
2417
|
-
try options.log.damage(battle.active(player), side.stored(), .None);
|
|
2410
|
+
try options.log.damage(.{ battle.active(player), side.stored(), .None });
|
|
2418
2411
|
if (showdown and side.stored().hp == 0) residual.* = false;
|
|
2419
2412
|
}
|
|
2420
2413
|
}
|
|
2421
2414
|
|
|
2422
2415
|
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
2416
|
// Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
|
|
2426
|
-
if (
|
|
2427
|
-
|
|
2428
|
-
|
|
2417
|
+
if (showdown) {
|
|
2418
|
+
if (battle.side(player).last_selected_move == .Teleport) return;
|
|
2419
|
+
_ = try checkHit(battle, player, move, options);
|
|
2420
|
+
battle.last_damage = 0;
|
|
2421
|
+
} else {
|
|
2422
|
+
try options.log.fail(.{ battle.active(player), .None });
|
|
2423
|
+
try options.log.laststill(.{});
|
|
2424
|
+
}
|
|
2429
2425
|
}
|
|
2430
2426
|
|
|
2431
2427
|
fn thrashing(battle: anytype, player: Player, options: anytype) void {
|
|
@@ -2435,6 +2431,8 @@ pub const Effects = struct {
|
|
|
2435
2431
|
|
|
2436
2432
|
volatiles.Thrashing = true;
|
|
2437
2433
|
volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
2434
|
+
|
|
2435
|
+
options.chance.observe(.attacking, player, .started);
|
|
2438
2436
|
}
|
|
2439
2437
|
|
|
2440
2438
|
fn transform(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -2463,7 +2461,7 @@ pub const Effects = struct {
|
|
|
2463
2461
|
side.active.moves[i].pp = if (m.id != .None) 5 else 0;
|
|
2464
2462
|
}
|
|
2465
2463
|
|
|
2466
|
-
try options.log.transform(battle.active(player), foe_ident);
|
|
2464
|
+
try options.log.transform(.{ battle.active(player), foe_ident });
|
|
2467
2465
|
}
|
|
2468
2466
|
|
|
2469
2467
|
fn binding(battle: anytype, player: Player, rewrap: bool, options: anytype) !void {
|
|
@@ -2473,13 +2471,14 @@ pub const Effects = struct {
|
|
|
2473
2471
|
if (side.active.volatiles.Binding) return;
|
|
2474
2472
|
side.active.volatiles.Binding = true;
|
|
2475
2473
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2476
|
-
// (Pokémon Showdown
|
|
2474
|
+
// (Pokémon Showdown clears Recharging in onTryMove instead)
|
|
2477
2475
|
if (!showdown) {
|
|
2478
2476
|
foe.active.volatiles.Recharging = false;
|
|
2479
2477
|
} else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
|
|
2480
2478
|
|
|
2481
2479
|
side.active.volatiles.attacks =
|
|
2482
2480
|
try Rolls.distribution(battle, .Binding, player, options) - 1;
|
|
2481
|
+
options.chance.observe(.binding, player, .started);
|
|
2483
2482
|
}
|
|
2484
2483
|
|
|
2485
2484
|
fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2493,87 +2492,87 @@ pub const Effects = struct {
|
|
|
2493
2492
|
switch (move.effect) {
|
|
2494
2493
|
.AttackUp1, .AttackUp2, .Rage => {
|
|
2495
2494
|
assert(boosts.atk >= -6 and boosts.atk <= 6);
|
|
2496
|
-
if (boosts.atk == 6) return
|
|
2495
|
+
if (boosts.atk == 6) return log.fail(.{ ident, .None });
|
|
2497
2496
|
const n: u2 = if (move.effect == .AttackUp2) 2 else 1;
|
|
2498
2497
|
boosts.atk = @as(i4, @intCast(@min(6, @as(i8, boosts.atk) + n)));
|
|
2499
|
-
const reason = if (move.effect == .Rage)
|
|
2498
|
+
const reason: Boost = if (move.effect == .Rage) .Rage else .Attack;
|
|
2500
2499
|
if (stats.atk == MAX_STAT_VALUE) {
|
|
2501
2500
|
boosts.atk -= 1;
|
|
2502
2501
|
if (showdown) {
|
|
2503
|
-
try log.boost(ident, reason, n);
|
|
2504
|
-
try log.boost(ident,
|
|
2502
|
+
try log.boost(.{ ident, reason, n });
|
|
2503
|
+
try log.boost(.{ ident, .Attack, -1 });
|
|
2505
2504
|
if (move.effect == .Rage) return;
|
|
2506
2505
|
}
|
|
2507
|
-
return
|
|
2506
|
+
return log.fail(.{ ident, .None });
|
|
2508
2507
|
}
|
|
2509
|
-
|
|
2508
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
|
|
2510
2509
|
const stat = unmodifiedStats(battle, side).atk;
|
|
2511
2510
|
stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2512
|
-
try log.boost(ident, reason, n);
|
|
2511
|
+
try log.boost(.{ ident, reason, n });
|
|
2513
2512
|
// Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
|
|
2514
2513
|
if (showdown and move.effect == .Rage) return;
|
|
2515
2514
|
},
|
|
2516
2515
|
.DefenseUp1, .DefenseUp2 => {
|
|
2517
2516
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
2518
|
-
if (boosts.def == 6) return
|
|
2517
|
+
if (boosts.def == 6) return log.fail(.{ ident, .None });
|
|
2519
2518
|
const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
|
|
2520
2519
|
boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
|
|
2521
2520
|
if (stats.def == MAX_STAT_VALUE) {
|
|
2522
2521
|
boosts.def -= 1;
|
|
2523
2522
|
if (showdown) {
|
|
2524
|
-
try log.boost(ident, .Defense, n);
|
|
2525
|
-
try log.boost(ident, .Defense, -1);
|
|
2523
|
+
try log.boost(.{ ident, .Defense, n });
|
|
2524
|
+
try log.boost(.{ ident, .Defense, -1 });
|
|
2526
2525
|
}
|
|
2527
|
-
return
|
|
2526
|
+
return log.fail(.{ ident, .None });
|
|
2528
2527
|
}
|
|
2529
|
-
|
|
2528
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
|
|
2530
2529
|
const stat = unmodifiedStats(battle, side).def;
|
|
2531
2530
|
stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2532
|
-
try log.boost(ident, .Defense, n);
|
|
2531
|
+
try log.boost(.{ ident, .Defense, n });
|
|
2533
2532
|
},
|
|
2534
2533
|
.SpeedUp2 => {
|
|
2535
2534
|
assert(boosts.spe >= -6 and boosts.spe <= 6);
|
|
2536
|
-
if (boosts.spe == 6) return
|
|
2535
|
+
if (boosts.spe == 6) return log.fail(.{ ident, .None });
|
|
2537
2536
|
boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
|
|
2538
2537
|
if (stats.spe == MAX_STAT_VALUE) {
|
|
2539
2538
|
boosts.spe -= 1;
|
|
2540
2539
|
if (showdown) {
|
|
2541
|
-
try log.boost(ident, .Speed, 2);
|
|
2542
|
-
try log.boost(ident, .Speed, -1);
|
|
2540
|
+
try log.boost(.{ ident, .Speed, 2 });
|
|
2541
|
+
try log.boost(.{ ident, .Speed, -1 });
|
|
2543
2542
|
}
|
|
2544
|
-
return
|
|
2543
|
+
return log.fail(.{ ident, .None });
|
|
2545
2544
|
}
|
|
2546
|
-
|
|
2545
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
|
|
2547
2546
|
const stat = unmodifiedStats(battle, side).spe;
|
|
2548
2547
|
stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2549
|
-
try log.boost(ident, .Speed, 2);
|
|
2548
|
+
try log.boost(.{ ident, .Speed, 2 });
|
|
2550
2549
|
},
|
|
2551
2550
|
.SpecialUp1, .SpecialUp2 => {
|
|
2552
2551
|
assert(boosts.spc >= -6 and boosts.spc <= 6);
|
|
2553
|
-
if (boosts.spc == 6) return
|
|
2552
|
+
if (boosts.spc == 6) return log.fail(.{ ident, .None });
|
|
2554
2553
|
const n: u2 = if (move.effect == .SpecialUp2) 2 else 1;
|
|
2555
2554
|
boosts.spc = @intCast(@min(6, @as(i8, boosts.spc) + n));
|
|
2556
2555
|
if (stats.spc == MAX_STAT_VALUE) {
|
|
2557
2556
|
boosts.spc -= 1;
|
|
2558
2557
|
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);
|
|
2558
|
+
try log.boost(.{ ident, .SpecialAttack, n });
|
|
2559
|
+
try log.boost(.{ ident, .SpecialAttack, -1 });
|
|
2560
|
+
try log.boost(.{ ident, .SpecialDefense, n });
|
|
2561
|
+
try log.boost(.{ ident, .SpecialDefense, -1 });
|
|
2563
2562
|
}
|
|
2564
|
-
return
|
|
2563
|
+
return log.fail(.{ ident, .None });
|
|
2565
2564
|
}
|
|
2566
|
-
|
|
2565
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
|
|
2567
2566
|
const stat = unmodifiedStats(battle, side).spc;
|
|
2568
2567
|
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);
|
|
2568
|
+
try log.boost(.{ ident, .SpecialAttack, n });
|
|
2569
|
+
try log.boost(.{ ident, .SpecialDefense, n });
|
|
2571
2570
|
},
|
|
2572
2571
|
.EvasionUp1 => {
|
|
2573
2572
|
assert(boosts.evasion >= -6 and boosts.evasion <= 6);
|
|
2574
|
-
if (boosts.evasion == 6) return
|
|
2573
|
+
if (boosts.evasion == 6) return log.fail(.{ ident, .None });
|
|
2575
2574
|
boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
|
|
2576
|
-
try log.boost(ident, .Evasion, 1);
|
|
2575
|
+
try log.boost(.{ ident, .Evasion, 1 });
|
|
2577
2576
|
},
|
|
2578
2577
|
else => unreachable,
|
|
2579
2578
|
}
|
|
@@ -2588,7 +2587,7 @@ pub const Effects = struct {
|
|
|
2588
2587
|
const foe_ident = battle.active(player.foe());
|
|
2589
2588
|
const secondary = move.effect.isStatDownChance();
|
|
2590
2589
|
|
|
2591
|
-
if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(foe_ident, .None);
|
|
2590
|
+
if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(.{ foe_ident, .None });
|
|
2592
2591
|
|
|
2593
2592
|
if (secondary) {
|
|
2594
2593
|
const proc = try Rolls.unboost(battle, player, options);
|
|
@@ -2596,7 +2595,6 @@ pub const Effects = struct {
|
|
|
2596
2595
|
} else {
|
|
2597
2596
|
// checkHit already checks for Invulnerable
|
|
2598
2597
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2599
|
-
try options.chance.commit(player, .hit);
|
|
2600
2598
|
}
|
|
2601
2599
|
|
|
2602
2600
|
var stats = &foe.active.stats;
|
|
@@ -2606,82 +2604,82 @@ pub const Effects = struct {
|
|
|
2606
2604
|
switch (move.effect) {
|
|
2607
2605
|
.AttackDown1, .AttackDownChance => {
|
|
2608
2606
|
assert(boosts.atk >= -6 and boosts.atk <= 6);
|
|
2609
|
-
if (boosts.atk == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2607
|
+
if (boosts.atk == -6) return if (fail) try log.fail(.{ foe_ident, .None });
|
|
2610
2608
|
boosts.atk = @intCast(@max(-6, @as(i8, boosts.atk) - 1));
|
|
2611
2609
|
if (stats.atk == 1) {
|
|
2612
2610
|
boosts.atk += 1;
|
|
2613
2611
|
if (showdown) {
|
|
2614
|
-
try log.boost(foe_ident, .Attack, -1);
|
|
2615
|
-
try log.boost(foe_ident, .Attack, 1);
|
|
2612
|
+
try log.boost(.{ foe_ident, .Attack, -1 });
|
|
2613
|
+
try log.boost(.{ foe_ident, .Attack, 1 });
|
|
2616
2614
|
}
|
|
2617
|
-
return
|
|
2615
|
+
return log.fail(.{ foe_ident, .None });
|
|
2618
2616
|
}
|
|
2619
|
-
|
|
2617
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
|
|
2620
2618
|
const stat = unmodifiedStats(battle, foe).atk;
|
|
2621
2619
|
stats.atk = @max(1, stat * mod[0] / mod[1]);
|
|
2622
|
-
try log.boost(foe_ident, .Attack, -1);
|
|
2620
|
+
try log.boost(.{ foe_ident, .Attack, -1 });
|
|
2623
2621
|
},
|
|
2624
2622
|
.DefenseDown1, .DefenseDown2, .DefenseDownChance => {
|
|
2625
2623
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
2626
|
-
if (boosts.def == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2624
|
+
if (boosts.def == -6) return if (fail) try log.fail(.{ foe_ident, .None });
|
|
2627
2625
|
const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
|
|
2628
2626
|
boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
|
|
2629
2627
|
if (stats.def == 1) {
|
|
2630
2628
|
boosts.def += 1;
|
|
2631
2629
|
if (showdown) {
|
|
2632
|
-
try log.boost(foe_ident, .Defense, -@as(i8, n));
|
|
2633
|
-
try log.boost(foe_ident, .Defense, 1);
|
|
2630
|
+
try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
|
|
2631
|
+
try log.boost(.{ foe_ident, .Defense, 1 });
|
|
2634
2632
|
}
|
|
2635
|
-
return
|
|
2633
|
+
return log.fail(.{ foe_ident, .None });
|
|
2636
2634
|
}
|
|
2637
|
-
|
|
2635
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
|
|
2638
2636
|
const stat = unmodifiedStats(battle, foe).def;
|
|
2639
2637
|
stats.def = @max(1, stat * mod[0] / mod[1]);
|
|
2640
|
-
try log.boost(foe_ident, .Defense, -@as(i8, n));
|
|
2638
|
+
try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
|
|
2641
2639
|
},
|
|
2642
2640
|
.SpeedDown1, .SpeedDownChance => {
|
|
2643
2641
|
assert(boosts.spe >= -6 and boosts.spe <= 6);
|
|
2644
|
-
if (boosts.spe == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2642
|
+
if (boosts.spe == -6) return if (fail) try log.fail(.{ foe_ident, .None });
|
|
2645
2643
|
boosts.spe = @intCast(@max(-6, @as(i8, boosts.spe) - 1));
|
|
2646
2644
|
if (stats.spe == 1) {
|
|
2647
2645
|
boosts.spe += 1;
|
|
2648
2646
|
if (showdown) {
|
|
2649
|
-
try log.boost(foe_ident, .Speed, -1);
|
|
2650
|
-
try log.boost(foe_ident, .Speed, 1);
|
|
2647
|
+
try log.boost(.{ foe_ident, .Speed, -1 });
|
|
2648
|
+
try log.boost(.{ foe_ident, .Speed, 1 });
|
|
2651
2649
|
}
|
|
2652
|
-
return
|
|
2650
|
+
return log.fail(.{ foe_ident, .None });
|
|
2653
2651
|
}
|
|
2654
|
-
|
|
2652
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
|
|
2655
2653
|
const stat = unmodifiedStats(battle, foe).spe;
|
|
2656
2654
|
stats.spe = @max(1, stat * mod[0] / mod[1]);
|
|
2657
|
-
try log.boost(foe_ident, .Speed, -1);
|
|
2655
|
+
try log.boost(.{ foe_ident, .Speed, -1 });
|
|
2658
2656
|
assert(boosts.spe >= -6);
|
|
2659
2657
|
},
|
|
2660
2658
|
.SpecialDownChance => {
|
|
2661
2659
|
assert(boosts.spc >= -6 and boosts.spc <= 6);
|
|
2662
|
-
if (boosts.spc == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2660
|
+
if (boosts.spc == -6) return if (fail) try log.fail(.{ foe_ident, .None });
|
|
2663
2661
|
boosts.spc = @intCast(@max(-6, @as(i8, boosts.spc) - 1));
|
|
2664
2662
|
if (stats.spc == 1) {
|
|
2665
2663
|
boosts.spc += 1;
|
|
2666
2664
|
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);
|
|
2665
|
+
try log.boost(.{ foe_ident, .SpecialAttack, -1 });
|
|
2666
|
+
try log.boost(.{ foe_ident, .SpecialAttack, 1 });
|
|
2667
|
+
try log.boost(.{ foe_ident, .SpecialDefense, -1 });
|
|
2668
|
+
try log.boost(.{ foe_ident, .SpecialDefense, 1 });
|
|
2671
2669
|
}
|
|
2672
|
-
return
|
|
2670
|
+
return log.fail(.{ foe_ident, .None });
|
|
2673
2671
|
}
|
|
2674
|
-
|
|
2672
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
|
|
2675
2673
|
const stat = unmodifiedStats(battle, foe).spc;
|
|
2676
2674
|
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);
|
|
2675
|
+
try log.boost(.{ foe_ident, .SpecialAttack, -1 });
|
|
2676
|
+
try log.boost(.{ foe_ident, .SpecialDefense, -1 });
|
|
2679
2677
|
},
|
|
2680
2678
|
.AccuracyDown1 => {
|
|
2681
2679
|
assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
|
|
2682
|
-
if (boosts.accuracy == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2680
|
+
if (boosts.accuracy == -6) return if (fail) try log.fail(.{ foe_ident, .None });
|
|
2683
2681
|
boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
|
|
2684
|
-
try log.boost(foe_ident, .Accuracy, -1);
|
|
2682
|
+
try log.boost(.{ foe_ident, .Accuracy, -1 });
|
|
2685
2683
|
},
|
|
2686
2684
|
else => unreachable,
|
|
2687
2685
|
}
|
|
@@ -2705,40 +2703,40 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
|
|
|
2705
2703
|
}
|
|
2706
2704
|
}
|
|
2707
2705
|
|
|
2708
|
-
|
|
2706
|
+
fn isForced(active: anytype) bool {
|
|
2709
2707
|
return active.volatiles.Recharging or active.volatiles.Rage or
|
|
2710
2708
|
active.volatiles.Thrashing or active.volatiles.Charging;
|
|
2711
2709
|
}
|
|
2712
2710
|
|
|
2713
|
-
fn clearVolatiles(battle: anytype, who: Player,
|
|
2711
|
+
fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
|
|
2714
2712
|
var log = options.log;
|
|
2715
2713
|
var side = battle.side(who);
|
|
2716
2714
|
var volatiles = &side.active.volatiles;
|
|
2717
2715
|
const ident = battle.active(who);
|
|
2718
2716
|
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
try log.end(ident, .DisableSilent);
|
|
2717
|
+
if (volatiles.disable_move != 0) {
|
|
2718
|
+
volatiles.disable_move = 0;
|
|
2719
|
+
volatiles.disable_duration = 0;
|
|
2720
|
+
options.chance.observe(.disable, who, .None);
|
|
2721
|
+
try log.end(.{ ident, .DisableSilent });
|
|
2725
2722
|
}
|
|
2726
2723
|
if (volatiles.Confusion) {
|
|
2727
2724
|
// volatiles.confusion is left unchanged
|
|
2728
2725
|
volatiles.Confusion = false;
|
|
2729
|
-
|
|
2726
|
+
options.chance.observe(.confusion, who, .None);
|
|
2727
|
+
try log.end(.{ ident, .ConfusionSilent });
|
|
2730
2728
|
}
|
|
2731
2729
|
if (volatiles.Mist) {
|
|
2732
2730
|
volatiles.Mist = false;
|
|
2733
|
-
try log.end(ident, .
|
|
2731
|
+
try log.end(.{ ident, .MistSilent });
|
|
2734
2732
|
}
|
|
2735
2733
|
if (volatiles.FocusEnergy) {
|
|
2736
2734
|
volatiles.FocusEnergy = false;
|
|
2737
|
-
try log.end(ident, .
|
|
2735
|
+
try log.end(.{ ident, .FocusEnergySilent });
|
|
2738
2736
|
}
|
|
2739
2737
|
if (volatiles.LeechSeed) {
|
|
2740
2738
|
volatiles.LeechSeed = false;
|
|
2741
|
-
try log.end(ident, .
|
|
2739
|
+
try log.end(.{ ident, .LeechSeedSilent });
|
|
2742
2740
|
}
|
|
2743
2741
|
if (!showdown and volatiles.Toxic) {
|
|
2744
2742
|
volatiles.Toxic = false;
|
|
@@ -2746,24 +2744,39 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
|
|
|
2746
2744
|
}
|
|
2747
2745
|
if (volatiles.LightScreen) {
|
|
2748
2746
|
volatiles.LightScreen = false;
|
|
2749
|
-
try log.end(ident, .
|
|
2747
|
+
try log.end(.{ ident, .LightScreenSilent });
|
|
2750
2748
|
}
|
|
2751
2749
|
if (volatiles.Reflect) {
|
|
2752
2750
|
volatiles.Reflect = false;
|
|
2753
|
-
try log.end(ident, .
|
|
2751
|
+
try log.end(.{ ident, .ReflectSilent });
|
|
2754
2752
|
}
|
|
2755
2753
|
if (showdown and volatiles.Toxic) {
|
|
2756
2754
|
volatiles.Toxic = false;
|
|
2757
2755
|
// Pokémon Showdown erroneously clears the toxic counter
|
|
2758
2756
|
volatiles.toxic = 0;
|
|
2759
|
-
try log.end(ident, .
|
|
2757
|
+
try log.end(.{ ident, .ToxicSilent });
|
|
2760
2758
|
}
|
|
2761
2759
|
}
|
|
2762
2760
|
|
|
2761
|
+
fn decrement(
|
|
2762
|
+
comptime field: chance.Action.Field,
|
|
2763
|
+
player: Player,
|
|
2764
|
+
options: anytype,
|
|
2765
|
+
n: anytype,
|
|
2766
|
+
) @TypeOf(n) {
|
|
2767
|
+
return if (options.calc.overridden(player, field)) |obs| switch (obs) {
|
|
2768
|
+
.started, .ended => 0,
|
|
2769
|
+
.continuing => if (n > 1) n - 1 else n,
|
|
2770
|
+
else => unreachable,
|
|
2771
|
+
} else n - 1;
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2763
2774
|
pub const Rolls = struct {
|
|
2764
|
-
|
|
2775
|
+
fn speedTie(battle: anytype, options: anytype) !bool {
|
|
2765
2776
|
const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
|
|
2766
2777
|
player == .P1
|
|
2778
|
+
else if (@hasDecl(@TypeOf(battle.rng), "speedTie"))
|
|
2779
|
+
battle.rng.speedTie()
|
|
2767
2780
|
else if (showdown)
|
|
2768
2781
|
battle.rng.range(u8, 0, 2) == 0
|
|
2769
2782
|
else
|
|
@@ -2773,21 +2786,31 @@ pub const Rolls = struct {
|
|
|
2773
2786
|
return p1;
|
|
2774
2787
|
}
|
|
2775
2788
|
|
|
2776
|
-
|
|
2789
|
+
fn criticalHit(
|
|
2790
|
+
battle: anytype,
|
|
2791
|
+
player: Player,
|
|
2792
|
+
rate: u8,
|
|
2793
|
+
spurious: bool,
|
|
2794
|
+
options: anytype,
|
|
2795
|
+
) !bool {
|
|
2777
2796
|
const crit = if (options.calc.overridden(player, .critical_hit)) |val|
|
|
2778
2797
|
val == .true
|
|
2798
|
+
else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
|
|
2799
|
+
battle.rng.criticalHit(player, rate)
|
|
2779
2800
|
else if (showdown)
|
|
2780
2801
|
battle.rng.chance(u8, rate, 256)
|
|
2781
2802
|
else
|
|
2782
2803
|
std.math.rotl(u8, battle.rng.next(), 3) < rate;
|
|
2783
2804
|
|
|
2784
|
-
try options.chance.criticalHit(player, crit, rate);
|
|
2805
|
+
if (!spurious) try options.chance.criticalHit(player, crit, rate);
|
|
2785
2806
|
return crit;
|
|
2786
2807
|
}
|
|
2787
2808
|
|
|
2788
|
-
|
|
2809
|
+
fn damage(battle: anytype, player: Player, options: anytype) !u8 {
|
|
2789
2810
|
const roll = if (options.calc.overridden(player, .damage)) |val|
|
|
2790
2811
|
val
|
|
2812
|
+
else if (@hasDecl(@TypeOf(battle.rng), "damage"))
|
|
2813
|
+
battle.rng.damage(player)
|
|
2791
2814
|
else roll: {
|
|
2792
2815
|
if (showdown) break :roll battle.rng.range(u8, 217, 256);
|
|
2793
2816
|
while (true) {
|
|
@@ -2796,25 +2819,32 @@ pub const Rolls = struct {
|
|
|
2796
2819
|
}
|
|
2797
2820
|
};
|
|
2798
2821
|
|
|
2822
|
+
assert(roll >= 217 and roll <= 255);
|
|
2799
2823
|
try options.chance.damage(player, roll);
|
|
2800
2824
|
return roll;
|
|
2801
2825
|
}
|
|
2802
2826
|
|
|
2803
|
-
|
|
2827
|
+
const uN = if (pkmn.options.miss) u8 else u9;
|
|
2828
|
+
fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) !bool {
|
|
2829
|
+
const acc: uN = if (pkmn.options.miss) accuracy else @as(uN, accuracy) + 1;
|
|
2804
2830
|
const ok = if (options.calc.overridden(player, .hit)) |val|
|
|
2805
2831
|
val == .true
|
|
2832
|
+
else if (@hasDecl(@TypeOf(battle.rng), "hit"))
|
|
2833
|
+
battle.rng.hit(player, acc)
|
|
2806
2834
|
else if (showdown)
|
|
2807
|
-
battle.rng.chance(
|
|
2835
|
+
battle.rng.chance(uN, acc, 256)
|
|
2808
2836
|
else
|
|
2809
|
-
battle.rng.next() <
|
|
2837
|
+
battle.rng.next() < acc;
|
|
2810
2838
|
|
|
2811
|
-
options.chance.hit(ok,
|
|
2839
|
+
try options.chance.hit(player, ok, acc);
|
|
2812
2840
|
return ok;
|
|
2813
2841
|
}
|
|
2814
2842
|
|
|
2815
|
-
|
|
2843
|
+
fn confused(battle: anytype, player: Player, options: anytype) !bool {
|
|
2816
2844
|
const cfz = if (options.calc.overridden(player, .confused)) |val|
|
|
2817
2845
|
val == .true
|
|
2846
|
+
else if (@hasDecl(@TypeOf(battle.rng), "confused"))
|
|
2847
|
+
battle.rng.confused(player)
|
|
2818
2848
|
else if (showdown)
|
|
2819
2849
|
!battle.rng.chance(u8, 128, 256)
|
|
2820
2850
|
else
|
|
@@ -2824,9 +2854,11 @@ pub const Rolls = struct {
|
|
|
2824
2854
|
return cfz;
|
|
2825
2855
|
}
|
|
2826
2856
|
|
|
2827
|
-
|
|
2857
|
+
fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
|
|
2828
2858
|
const par = if (options.calc.overridden(player, .paralyzed)) |val|
|
|
2829
2859
|
val == .true
|
|
2860
|
+
else if (@hasDecl(@TypeOf(battle.rng), "paralyzed"))
|
|
2861
|
+
battle.rng.paralyzed(player)
|
|
2830
2862
|
else if (showdown)
|
|
2831
2863
|
battle.rng.chance(u8, 63, 256)
|
|
2832
2864
|
else
|
|
@@ -2836,9 +2868,11 @@ pub const Rolls = struct {
|
|
|
2836
2868
|
return par;
|
|
2837
2869
|
}
|
|
2838
2870
|
|
|
2839
|
-
|
|
2871
|
+
fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
|
|
2840
2872
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2841
2873
|
val == .true
|
|
2874
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2875
|
+
battle.rng.secondaryChance(player, 25)
|
|
2842
2876
|
else if (showdown)
|
|
2843
2877
|
battle.rng.chance(u8, 25, 256)
|
|
2844
2878
|
else
|
|
@@ -2848,11 +2882,13 @@ pub const Rolls = struct {
|
|
|
2848
2882
|
return proc;
|
|
2849
2883
|
}
|
|
2850
2884
|
|
|
2851
|
-
|
|
2885
|
+
fn secondaryChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
|
|
2852
2886
|
const rate: u8 = if (low) 26 else 77;
|
|
2853
2887
|
|
|
2854
2888
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2855
2889
|
val == .true
|
|
2890
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2891
|
+
battle.rng.secondaryChance(player, rate)
|
|
2856
2892
|
else if (showdown)
|
|
2857
2893
|
battle.rng.chance(u8, rate, 256)
|
|
2858
2894
|
else
|
|
@@ -2862,11 +2898,13 @@ pub const Rolls = struct {
|
|
|
2862
2898
|
return proc;
|
|
2863
2899
|
}
|
|
2864
2900
|
|
|
2865
|
-
|
|
2901
|
+
fn poisonChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
|
|
2866
2902
|
const rate: u8 = if (low) 52 else 103;
|
|
2867
2903
|
|
|
2868
2904
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2869
2905
|
val == .true
|
|
2906
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2907
|
+
battle.rng.secondaryChance(player, rate)
|
|
2870
2908
|
else if (showdown)
|
|
2871
2909
|
battle.rng.chance(u8, rate, 256)
|
|
2872
2910
|
else
|
|
@@ -2876,9 +2914,11 @@ pub const Rolls = struct {
|
|
|
2876
2914
|
return proc;
|
|
2877
2915
|
}
|
|
2878
2916
|
|
|
2879
|
-
|
|
2917
|
+
fn unboost(battle: anytype, player: Player, options: anytype) !bool {
|
|
2880
2918
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2881
2919
|
val == .true
|
|
2920
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2921
|
+
battle.rng.secondaryChance(player, 85)
|
|
2882
2922
|
else if (showdown)
|
|
2883
2923
|
battle.rng.chance(u8, 85, 256)
|
|
2884
2924
|
else
|
|
@@ -2888,14 +2928,14 @@ pub const Rolls = struct {
|
|
|
2888
2928
|
return proc;
|
|
2889
2929
|
}
|
|
2890
2930
|
|
|
2891
|
-
|
|
2931
|
+
fn metronome(battle: anytype, player: Player, options: anytype) !Move {
|
|
2892
2932
|
const move: Move = if (options.calc.overridden(player, .metronome)) |val|
|
|
2893
2933
|
val
|
|
2894
|
-
else if (
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2934
|
+
else if (@hasDecl(@TypeOf(battle.rng), "metronome"))
|
|
2935
|
+
battle.rng.metronome(player)
|
|
2936
|
+
else if (showdown)
|
|
2937
|
+
Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
|
|
2938
|
+
else move: {
|
|
2899
2939
|
while (true) {
|
|
2900
2940
|
const r = battle.rng.next();
|
|
2901
2941
|
if (r == 0 or r == @intFromEnum(Move.Metronome)) continue;
|
|
@@ -2908,9 +2948,11 @@ pub const Rolls = struct {
|
|
|
2908
2948
|
return move;
|
|
2909
2949
|
}
|
|
2910
2950
|
|
|
2911
|
-
|
|
2951
|
+
fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
|
|
2912
2952
|
const power = if (options.calc.overridden(player, .psywave)) |val|
|
|
2913
2953
|
val - 1
|
|
2954
|
+
else if (@hasDecl(@TypeOf(battle.rng), "psywave"))
|
|
2955
|
+
battle.rng.psywave(player)
|
|
2914
2956
|
else if (showdown)
|
|
2915
2957
|
battle.rng.range(u8, 0, max)
|
|
2916
2958
|
else power: {
|
|
@@ -2920,15 +2962,19 @@ pub const Rolls = struct {
|
|
|
2920
2962
|
}
|
|
2921
2963
|
};
|
|
2922
2964
|
|
|
2965
|
+
assert(power < max);
|
|
2923
2966
|
try options.chance.psywave(player, power, max);
|
|
2924
2967
|
return power;
|
|
2925
2968
|
}
|
|
2926
2969
|
|
|
2927
|
-
|
|
2928
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2929
|
-
@intCast(val)
|
|
2970
|
+
fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2971
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2972
|
+
if (val >= 1 and val <= 7) @as(u3, @intCast(val)) else null
|
|
2973
|
+
else
|
|
2974
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
|
|
2975
|
+
battle.rng.sleepDuration(player)
|
|
2930
2976
|
else if (showdown)
|
|
2931
|
-
|
|
2977
|
+
battle.rng.range(u3, 1, 8)
|
|
2932
2978
|
else duration: {
|
|
2933
2979
|
while (true) {
|
|
2934
2980
|
const r = battle.rng.next() & 7;
|
|
@@ -2936,64 +2982,83 @@ pub const Rolls = struct {
|
|
|
2936
2982
|
}
|
|
2937
2983
|
};
|
|
2938
2984
|
|
|
2939
|
-
|
|
2985
|
+
assert(duration >= 1 and duration <= 7);
|
|
2986
|
+
options.chance.duration(player, duration);
|
|
2940
2987
|
return duration;
|
|
2941
2988
|
}
|
|
2942
2989
|
|
|
2943
|
-
|
|
2944
|
-
const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
|
|
2945
|
-
val
|
|
2990
|
+
fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
|
|
2991
|
+
const duration: u4 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2992
|
+
if (val >= 1 and val <= 8) val else null
|
|
2993
|
+
else
|
|
2994
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
|
|
2995
|
+
battle.rng.disableDuration(player)
|
|
2946
2996
|
else if (showdown)
|
|
2947
|
-
|
|
2997
|
+
battle.rng.range(u4, 1, 9)
|
|
2948
2998
|
else
|
|
2949
2999
|
@intCast((battle.rng.next() & 7) + 1);
|
|
2950
3000
|
|
|
2951
|
-
|
|
3001
|
+
assert(duration >= 1 and duration <= 8);
|
|
3002
|
+
options.chance.duration(player, duration);
|
|
2952
3003
|
return duration;
|
|
2953
3004
|
}
|
|
2954
3005
|
|
|
2955
|
-
|
|
2956
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2957
|
-
@intCast(val)
|
|
3006
|
+
fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
3007
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
3008
|
+
if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
|
|
3009
|
+
else
|
|
3010
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
|
|
3011
|
+
battle.rng.confusionDuration(player)
|
|
2958
3012
|
else if (showdown)
|
|
2959
|
-
|
|
3013
|
+
battle.rng.range(u3, 2, 6)
|
|
2960
3014
|
else
|
|
2961
3015
|
@intCast((battle.rng.next() & 3) + 2);
|
|
2962
3016
|
|
|
2963
|
-
|
|
3017
|
+
assert(duration >= 2 and duration <= 5);
|
|
3018
|
+
options.chance.duration(player, duration);
|
|
2964
3019
|
return duration;
|
|
2965
3020
|
}
|
|
2966
3021
|
|
|
2967
|
-
|
|
2968
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2969
|
-
@intCast(val)
|
|
3022
|
+
fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
3023
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
3024
|
+
if (val >= 2 and val <= 3) @as(u3, @intCast(val)) else null
|
|
3025
|
+
else
|
|
3026
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
|
|
3027
|
+
battle.rng.attackingDuration(player)
|
|
2970
3028
|
else if (showdown)
|
|
2971
|
-
|
|
3029
|
+
battle.rng.range(u3, 2, 4)
|
|
2972
3030
|
else
|
|
2973
3031
|
@intCast((battle.rng.next() & 1) + 2);
|
|
2974
3032
|
|
|
2975
|
-
|
|
3033
|
+
assert(duration >= 2 and duration <= 3);
|
|
3034
|
+
options.chance.duration(player, duration);
|
|
2976
3035
|
return duration;
|
|
2977
3036
|
}
|
|
2978
3037
|
|
|
2979
|
-
|
|
3038
|
+
const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
|
|
3039
|
+
|
|
3040
|
+
fn distribution(
|
|
2980
3041
|
battle: anytype,
|
|
2981
3042
|
comptime effect: Move.Effect,
|
|
2982
3043
|
player: Player,
|
|
2983
3044
|
options: anytype,
|
|
2984
3045
|
) !u3 {
|
|
2985
3046
|
const roll = if (effect == .Binding) .duration else .multi_hit;
|
|
2986
|
-
const n: u3 = if (options.calc.overridden(player, roll)) |val|
|
|
2987
|
-
@intCast(val)
|
|
3047
|
+
const n: u3 = (if (options.calc.overridden(player, roll)) |val|
|
|
3048
|
+
if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
|
|
3049
|
+
else
|
|
3050
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "distribution"))
|
|
3051
|
+
battle.rng.distribution(player)
|
|
2988
3052
|
else if (showdown)
|
|
2989
|
-
DISTRIBUTION[battle.rng.range(
|
|
3053
|
+
DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
|
|
2990
3054
|
else n: {
|
|
2991
3055
|
const r = (battle.rng.next() & 3);
|
|
2992
3056
|
break :n @intCast((if (r < 2) r else battle.rng.next() & 3) + 2);
|
|
2993
3057
|
};
|
|
2994
3058
|
|
|
3059
|
+
assert(n >= 2 and n <= 5);
|
|
2995
3060
|
if (effect == .Binding) {
|
|
2996
|
-
options.chance.duration(
|
|
3061
|
+
options.chance.duration(player, n);
|
|
2997
3062
|
} else {
|
|
2998
3063
|
try options.chance.multiHit(player, n);
|
|
2999
3064
|
}
|
|
@@ -3008,15 +3073,15 @@ pub const Rolls = struct {
|
|
|
3008
3073
|
options: anytype,
|
|
3009
3074
|
) !u4 {
|
|
3010
3075
|
// TODO: consider throwing error instead of rerolling?
|
|
3011
|
-
const
|
|
3076
|
+
const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
|
|
3012
3077
|
if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
|
|
3013
3078
|
val
|
|
3014
3079
|
else
|
|
3015
3080
|
null
|
|
3016
3081
|
else
|
|
3017
|
-
null
|
|
3018
|
-
|
|
3019
|
-
|
|
3082
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
|
|
3083
|
+
battle.rng.moveSlot(player, moves, check_pp)
|
|
3084
|
+
else slot: {
|
|
3020
3085
|
if (showdown) {
|
|
3021
3086
|
if (check_pp == 0) {
|
|
3022
3087
|
var i: usize = moves.len;
|
|
@@ -3032,10 +3097,9 @@ pub const Rolls = struct {
|
|
|
3032
3097
|
while (i < moves.len and r > 0) : (i += 1) {
|
|
3033
3098
|
if (moves[i].pp > 0) {
|
|
3034
3099
|
r -= 1;
|
|
3035
|
-
if (r == 0) break;
|
|
3100
|
+
if (r == 0) break :slot @intCast(i + 1);
|
|
3036
3101
|
}
|
|
3037
3102
|
}
|
|
3038
|
-
break :slot @intCast(i + 1);
|
|
3039
3103
|
}
|
|
3040
3104
|
}
|
|
3041
3105
|
|
|
@@ -3057,11 +3121,11 @@ test "RNG agreement" {
|
|
|
3057
3121
|
expected[i] = @intCast(i * 0x1000000);
|
|
3058
3122
|
}
|
|
3059
3123
|
|
|
3060
|
-
var spe
|
|
3061
|
-
var cfz
|
|
3062
|
-
var par
|
|
3063
|
-
var brn
|
|
3064
|
-
var eff
|
|
3124
|
+
var spe: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
|
|
3125
|
+
var cfz: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
|
|
3126
|
+
var par: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
|
|
3127
|
+
var brn: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
|
|
3128
|
+
var eff: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
|
|
3065
3129
|
|
|
3066
3130
|
for (0..expected.len) |i| {
|
|
3067
3131
|
try expectEqual(spe.range(u8, 0, 2) == 0, i < Gen12.percent(50) + 1);
|
|
@@ -3081,11 +3145,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3081
3145
|
},
|
|
3082
3146
|
.Switch => {
|
|
3083
3147
|
const side = battle.side(player);
|
|
3084
|
-
var slot:
|
|
3148
|
+
var slot: usize = 2;
|
|
3085
3149
|
while (slot <= 6) : (slot += 1) {
|
|
3086
3150
|
const id = side.order[slot - 1];
|
|
3087
3151
|
if (id == 0 or side.pokemon[id - 1].hp == 0) continue;
|
|
3088
|
-
out[n] = .{ .type = .Switch, .data = slot };
|
|
3152
|
+
out[n] = .{ .type = .Switch, .data = @intCast(slot) };
|
|
3089
3153
|
n += 1;
|
|
3090
3154
|
}
|
|
3091
3155
|
if (n == 0) {
|
|
@@ -3097,11 +3161,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3097
3161
|
const side = battle.side(player);
|
|
3098
3162
|
const foe = battle.foe(player);
|
|
3099
3163
|
|
|
3100
|
-
|
|
3164
|
+
const active = &side.active;
|
|
3101
3165
|
const stored = side.stored();
|
|
3102
3166
|
|
|
3103
3167
|
// 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
|
|
3168
|
+
// Pokémon Showdown instead produces a list with a single move that must be chosen
|
|
3105
3169
|
//
|
|
3106
3170
|
// Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
|
|
3107
3171
|
// here when not in compatibility mode, but the engine needs to be able to differentiate
|
|
@@ -3109,18 +3173,18 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3109
3173
|
// passing due to being forced into using a move. Instead of introducing another option
|
|
3110
3174
|
// we simply repurpose Move with no move slot, even though pedantically this is not
|
|
3111
3175
|
// strictly correct as the player would not have been presented the option to move or
|
|
3112
|
-
// switch at all
|
|
3176
|
+
// switch at all
|
|
3113
3177
|
if (isForced(active)) {
|
|
3114
3178
|
out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
|
|
3115
3179
|
n += 1;
|
|
3116
3180
|
return n;
|
|
3117
3181
|
}
|
|
3118
3182
|
|
|
3119
|
-
var slot:
|
|
3183
|
+
var slot: usize = 2;
|
|
3120
3184
|
while (slot <= 6) : (slot += 1) {
|
|
3121
3185
|
const id = side.order[slot - 1];
|
|
3122
3186
|
if (id == 0 or side.pokemon[id - 1].hp == 0) continue;
|
|
3123
|
-
out[n] = .{ .type = .Switch, .data = slot };
|
|
3187
|
+
out[n] = .{ .type = .Switch, .data = @intCast(slot) };
|
|
3124
3188
|
n += 1;
|
|
3125
3189
|
}
|
|
3126
3190
|
|
|
@@ -3128,7 +3192,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3128
3192
|
// On the cartridge, all of these happen after "FIGHT" (indicating you are not
|
|
3129
3193
|
// switching) but before you are allowed to select a move. Pokémon Showdown instead
|
|
3130
3194
|
// either disables all other moves in the case of limited or requires you to select a
|
|
3131
|
-
// move normally if sleeping/frozen/bound
|
|
3195
|
+
// move normally if sleeping/frozen/bound
|
|
3132
3196
|
if (!showdown and (limited or foe.active.volatiles.Binding or
|
|
3133
3197
|
Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
|
|
3134
3198
|
{
|
|
@@ -3141,7 +3205,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3141
3205
|
// Pokémon Showdown handles Bide and Binding moves by checking if the move in question
|
|
3142
3206
|
// is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
|
|
3143
3207
|
// 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
|
|
3208
|
+
// proc-ed move in their moveset) and disabling all other moves
|
|
3145
3209
|
if (limited) {
|
|
3146
3210
|
assert(showdown);
|
|
3147
3211
|
assert(side.last_selected_move != .None);
|
|
@@ -3151,9 +3215,9 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3151
3215
|
if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
|
|
3152
3216
|
// Pokémon Showdown displays Struggle if limited to Bide but unable to pick
|
|
3153
3217
|
const struggle =
|
|
3154
|
-
m.id == .Bide and (m.pp == 0 or active.volatiles.
|
|
3218
|
+
m.id == .Bide and (m.pp == 0 or active.volatiles.disable_move == slot);
|
|
3155
3219
|
const s = if (struggle) 0 else slot;
|
|
3156
|
-
out[n] = .{ .type = .Move, .data = s };
|
|
3220
|
+
out[n] = .{ .type = .Move, .data = @intCast(s) };
|
|
3157
3221
|
n += 1;
|
|
3158
3222
|
return n;
|
|
3159
3223
|
}
|
|
@@ -3166,14 +3230,14 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3166
3230
|
const m = active.moves[slot - 1];
|
|
3167
3231
|
if (m.id == .None) break;
|
|
3168
3232
|
if (m.pp == 0) continue;
|
|
3169
|
-
if (active.volatiles.
|
|
3170
|
-
out[n] = .{ .type = .Move, .data = slot };
|
|
3233
|
+
if (active.volatiles.disable_move == slot) continue;
|
|
3234
|
+
out[n] = .{ .type = .Move, .data = @intCast(slot) };
|
|
3171
3235
|
n += 1;
|
|
3172
3236
|
}
|
|
3173
3237
|
// Struggle (Pokémon Showdown would use 'move 1' here)
|
|
3174
3238
|
if (n == before) {
|
|
3175
3239
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
3176
|
-
if (!
|
|
3240
|
+
if (!pkmn.options.mod) {
|
|
3177
3241
|
while (slot <= 4) : (slot += 1) {
|
|
3178
3242
|
if (active.moves[slot - 1].pp != 0) return n;
|
|
3179
3243
|
}
|