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