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