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