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