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