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