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