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