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