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