@pkmn/engine 0.1.0-dev.84cc1a02 → 0.1.0-dev.86ae1278
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 +300 -153
- package/package.json +101 -48
- package/src/bin/install-pkmn-engine +71 -37
- 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 +959 -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 +1313 -877
- 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 -778
- 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);
|
|
539
|
-
|
|
540
|
-
const confused = if (showdown)
|
|
541
|
-
!battle.rng.chance(u8, 128, 256)
|
|
542
|
-
else
|
|
543
|
-
battle.rng.next() >= Gen12.percent(50) + 1;
|
|
591
|
+
try log.activate(.{ ident, .Confusion });
|
|
544
592
|
|
|
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,39 @@ 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);
|
|
925
|
-
} else if (showdown) {
|
|
1044
|
+
try log.hitcount(.{ battle.active(player.foe()), hit });
|
|
1045
|
+
} else if (showdown and move.effect == .Binding) {
|
|
926
1046
|
// This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
1047
|
+
try Effects.binding(battle, player, rewrap, options);
|
|
1048
|
+
if (immune) {
|
|
1049
|
+
battle.last_damage = 0;
|
|
1050
|
+
assert(foe.stored().hp > 0);
|
|
1051
|
+
// Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
|
|
1052
|
+
if (sub) {
|
|
1053
|
+
try log.activate(.{ battle.active(player.foe()), .Substitute });
|
|
1054
|
+
} else {
|
|
1055
|
+
try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
|
|
1056
|
+
try buildRage(battle, player.foe(), options);
|
|
934
1057
|
}
|
|
1058
|
+
return null;
|
|
935
1059
|
}
|
|
936
1060
|
}
|
|
937
1061
|
|
|
938
1062
|
// 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
|
|
1063
|
+
// if an effect was intended to "always happen" it will still get skipped
|
|
940
1064
|
if (nullified) return null;
|
|
941
1065
|
|
|
942
1066
|
// On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
|
|
943
1067
|
// 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,
|
|
1068
|
+
// setup before the loop means we can avoid having to waste time doing no-op handler searches
|
|
1069
|
+
if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
|
|
946
1070
|
|
|
947
1071
|
if (foe.stored().hp == 0) return null;
|
|
948
1072
|
|
|
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(),
|
|
1073
|
+
// Pokémon Showdown builds Rage at the wrong time for non-MultiHit move
|
|
1074
|
+
if (late and !sub and move.effect != .Disable) try buildRage(battle, player.foe(), options);
|
|
951
1075
|
|
|
952
1076
|
if (!move.effect.isSpecial()) {
|
|
953
1077
|
// On the cartridge Rage is not considered to be "special" and thus gets executed for a
|
|
@@ -956,32 +1080,33 @@ fn doMove(
|
|
|
956
1080
|
// below). For Twineedle we change the data to that of one with PoisonChance1 given its
|
|
957
1081
|
// MultiHit behavior is complete after the loop above, though Pokémon Showdown handles the
|
|
958
1082
|
// Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
|
|
959
|
-
// puts the |-status| message before |-hitcount| instead of after
|
|
1083
|
+
// puts the |-status| message before |-hitcount| instead of after
|
|
960
1084
|
if (move.effect == .Twineedle) {
|
|
961
|
-
if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting),
|
|
1085
|
+
if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
962
1086
|
} else if (move.effect == .Disable) {
|
|
963
|
-
const result = try Effects.disable(battle, player, move,
|
|
964
|
-
if (showdown) try buildRage(battle, player.foe(),
|
|
1087
|
+
const result = try Effects.disable(battle, player, move, options);
|
|
1088
|
+
if (showdown) try buildRage(battle, player.foe(), options);
|
|
965
1089
|
return result;
|
|
966
1090
|
} else {
|
|
967
|
-
try moveEffect(battle, player, move,
|
|
1091
|
+
try moveEffect(battle, player, move, options);
|
|
968
1092
|
}
|
|
969
1093
|
}
|
|
970
1094
|
|
|
971
1095
|
return null;
|
|
972
1096
|
}
|
|
973
1097
|
|
|
974
|
-
fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
|
|
1098
|
+
fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !bool {
|
|
975
1099
|
const side = battle.side(player);
|
|
976
1100
|
|
|
977
1101
|
// 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
|
-
|
|
1102
|
+
var rate: u16 = Species.chance(side.stored().species);
|
|
1103
|
+
// GLITCH: Focus Energy reduces critical hit rate instead of increasing it
|
|
1104
|
+
rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
|
|
1105
|
+
rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
|
|
1106
|
+
|
|
1107
|
+
const spurious =
|
|
1108
|
+
!showdown and ((move.bp == 0 and move.effect != .OHKO) or move.target == .Depends);
|
|
1109
|
+
return Rolls.criticalHit(battle, player, @intCast(rate), spurious, options);
|
|
985
1110
|
}
|
|
986
1111
|
|
|
987
1112
|
fn calcDamage(
|
|
@@ -990,6 +1115,7 @@ fn calcDamage(
|
|
|
990
1115
|
target_player: Player,
|
|
991
1116
|
m: ?Move.Data,
|
|
992
1117
|
crit: bool,
|
|
1118
|
+
options: anytype,
|
|
993
1119
|
) bool {
|
|
994
1120
|
// Confusion (indicated when m == null) just needs a 40 BP physical move
|
|
995
1121
|
const cfz = m == null;
|
|
@@ -1001,28 +1127,8 @@ fn calcDamage(
|
|
|
1001
1127
|
|
|
1002
1128
|
const special = move.type.special();
|
|
1003
1129
|
|
|
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
|
|
1130
|
+
var atk = attack(side, crit, special);
|
|
1131
|
+
var def = defense(target, crit, special, cfz);
|
|
1026
1132
|
|
|
1027
1133
|
// Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
|
|
1028
1134
|
// will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
|
|
@@ -1033,9 +1139,9 @@ fn calcDamage(
|
|
|
1033
1139
|
def = @max((def / 4) & 255, if (showdown) 1 else 0);
|
|
1034
1140
|
}
|
|
1035
1141
|
|
|
1036
|
-
const lvl =
|
|
1142
|
+
const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
|
|
1037
1143
|
|
|
1038
|
-
def =
|
|
1144
|
+
def = if (move.effect == .Explode) @max(def / 2, 1) else def;
|
|
1039
1145
|
|
|
1040
1146
|
if (def == 0) return false;
|
|
1041
1147
|
|
|
@@ -1047,11 +1153,41 @@ fn calcDamage(
|
|
|
1047
1153
|
d = @min(997, d);
|
|
1048
1154
|
d += 2;
|
|
1049
1155
|
|
|
1050
|
-
|
|
1156
|
+
// Whether or not a critical hit occurred can change whether a division-by-zero freeze is
|
|
1157
|
+
// encountered so we need to make sure that in the event of a miss we commit the status of the
|
|
1158
|
+
// crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
|
|
1159
|
+
// that results in counter intuitive probabilities in the common case
|
|
1160
|
+
if (pkmn.options.chance and !showdown) {
|
|
1161
|
+
atk = attack(side, !crit, special);
|
|
1162
|
+
def = defense(target, !crit, special, false);
|
|
1163
|
+
if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
battle.last_damage = @intCast(d);
|
|
1051
1167
|
|
|
1052
1168
|
return true;
|
|
1053
1169
|
}
|
|
1054
1170
|
|
|
1171
|
+
fn attack(side: *Side, crit: bool, special: bool) u32 {
|
|
1172
|
+
return if (crit)
|
|
1173
|
+
if (special) side.stored().stats.spc else side.stored().stats.atk
|
|
1174
|
+
else if (special) side.active.stats.spc else side.active.stats.atk;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
fn defense(target: *Side, crit: bool, special: bool, cfz: bool) u32 {
|
|
1178
|
+
return if (crit)
|
|
1179
|
+
if (special) target.stored().stats.spc else target.stored().stats.def
|
|
1180
|
+
else
|
|
1181
|
+
// GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
|
|
1182
|
+
if (special)
|
|
1183
|
+
target.active.stats.spc *
|
|
1184
|
+
@as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
|
|
1185
|
+
else
|
|
1186
|
+
// Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
|
|
1187
|
+
target.active.stats.def *
|
|
1188
|
+
@as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1055
1191
|
fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
1056
1192
|
const side = battle.side(player);
|
|
1057
1193
|
const foe = battle.foe(player);
|
|
@@ -1061,9 +1197,9 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
|
1061
1197
|
var d = battle.last_damage;
|
|
1062
1198
|
if (side.active.types.includes(move.type)) d +%= d / 2;
|
|
1063
1199
|
|
|
1064
|
-
const neutral = @
|
|
1065
|
-
const eff1: u16 = @
|
|
1066
|
-
const eff2: u16 = @
|
|
1200
|
+
const neutral = @intFromEnum(Effectiveness.Neutral);
|
|
1201
|
+
const eff1: u16 = @intFromEnum(move.type.effectiveness(types.type1));
|
|
1202
|
+
const eff2: u16 = @intFromEnum(move.type.effectiveness(types.type2));
|
|
1067
1203
|
|
|
1068
1204
|
// Type effectiveness matchup precedence only matters with (NVE, SE)
|
|
1069
1205
|
if (!showdown and (eff1 + eff2) == Effectiveness.mismatch and
|
|
@@ -1083,101 +1219,85 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
|
1083
1219
|
return if (types.type1 == types.type2) eff1 * neutral else eff1 * eff2;
|
|
1084
1220
|
}
|
|
1085
1221
|
|
|
1086
|
-
fn randomizeDamage(battle: anytype) void {
|
|
1222
|
+
fn randomizeDamage(battle: anytype, player: Player, options: anytype) !void {
|
|
1087
1223
|
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);
|
|
1224
|
+
const random = try Rolls.damage(battle, player, options);
|
|
1225
|
+
options.calc.base(player.foe(), battle.last_damage);
|
|
1226
|
+
battle.last_damage = @intCast(@as(u32, battle.last_damage) *% random / 255);
|
|
1227
|
+
options.calc.final(player.foe(), battle.last_damage);
|
|
1099
1228
|
}
|
|
1100
1229
|
|
|
1101
|
-
fn specialDamage(battle: anytype, player: Player, move: Move.Data,
|
|
1230
|
+
fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
|
|
1102
1231
|
const side = battle.side(player);
|
|
1232
|
+
const foe = battle.foe(player);
|
|
1103
1233
|
|
|
1104
|
-
if (!try checkHit(battle, player, move,
|
|
1234
|
+
if (!try checkHit(battle, player, move, options)) return null;
|
|
1105
1235
|
|
|
1106
1236
|
battle.last_damage = switch (side.last_selected_move) {
|
|
1107
|
-
.SuperFang => @max(
|
|
1237
|
+
.SuperFang => @max(foe.stored().hp / 2, 1),
|
|
1108
1238
|
.SeismicToss, .NightShade => side.stored().level,
|
|
1109
1239
|
.SonicBoom => 20,
|
|
1110
1240
|
.DragonRage => 40,
|
|
1111
1241
|
// GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
|
|
1112
1242
|
.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
|
-
}
|
|
1243
|
+
const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
|
|
1244
|
+
// GLITCH: Psywave infinite glitch loop
|
|
1245
|
+
if (!showdown and max <= 1) return Result.Error;
|
|
1246
|
+
break :power try Rolls.psywave(battle, player, max, options);
|
|
1124
1247
|
},
|
|
1125
1248
|
else => unreachable,
|
|
1126
1249
|
};
|
|
1127
1250
|
|
|
1128
1251
|
if (battle.last_damage == 0) return if (showdown) null else Result.Error;
|
|
1129
1252
|
|
|
1130
|
-
|
|
1131
|
-
|
|
1253
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1254
|
+
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
1255
|
+
if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), options);
|
|
1132
1256
|
|
|
1133
1257
|
return null;
|
|
1134
1258
|
}
|
|
1135
1259
|
|
|
1136
|
-
fn counterDamage(battle: anytype, player: Player, move: Move.Data,
|
|
1260
|
+
fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
|
|
1137
1261
|
const foe = battle.foe(player);
|
|
1138
1262
|
|
|
1139
1263
|
if (battle.last_damage == 0) {
|
|
1140
|
-
try log.fail(battle.active(player), .None);
|
|
1264
|
+
try options.log.fail(.{ battle.active(player), .None });
|
|
1141
1265
|
return null;
|
|
1142
1266
|
}
|
|
1143
1267
|
|
|
1144
|
-
// Pretend
|
|
1145
|
-
const foe_last_used_move =
|
|
1146
|
-
Move.get(if (foe.last_used_move == .None) .Counter else foe.last_used_move);
|
|
1268
|
+
// Pretend Splash was used as a stand-in when no move has been used to fail below with 0 BP
|
|
1147
1269
|
const foe_last_selected_move =
|
|
1148
1270
|
Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
|
|
1149
|
-
.
|
|
1271
|
+
.Splash
|
|
1150
1272
|
else
|
|
1151
1273
|
foe.last_selected_move);
|
|
1152
1274
|
|
|
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
|
-
|
|
1275
|
+
const used = battle.lastMove(player.foe()).counterable != 0;
|
|
1158
1276
|
const selected = foe_last_selected_move.bp > 0 and
|
|
1159
1277
|
foe.last_selected_move != .Counter and
|
|
1160
1278
|
(foe_last_selected_move.type == .Normal or
|
|
1161
1279
|
foe_last_selected_move.type == .Fighting);
|
|
1162
1280
|
|
|
1163
1281
|
if (!used and !selected) {
|
|
1164
|
-
try log.fail(battle.active(player), .None);
|
|
1282
|
+
try options.log.fail(.{ battle.active(player), .None });
|
|
1165
1283
|
return null;
|
|
1166
1284
|
}
|
|
1167
1285
|
|
|
1168
1286
|
if (!used or !selected) {
|
|
1169
1287
|
// GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
|
|
1170
1288
|
if (!showdown) return Result.Error;
|
|
1171
|
-
try log.fail(battle.active(player), .None);
|
|
1289
|
+
try options.log.fail(.{ battle.active(player), .None });
|
|
1172
1290
|
return null;
|
|
1173
1291
|
}
|
|
1174
1292
|
|
|
1175
|
-
battle.last_damage
|
|
1293
|
+
battle.last_damage *|= 2;
|
|
1176
1294
|
|
|
1177
|
-
// Pokémon Showdown calls
|
|
1178
|
-
if (!showdown and !try checkHit(battle, player, move,
|
|
1295
|
+
// Pokémon Showdown calls moveHit before Counter
|
|
1296
|
+
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
1179
1297
|
|
|
1180
|
-
|
|
1298
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1299
|
+
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
1300
|
+
if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), options);
|
|
1181
1301
|
return null;
|
|
1182
1302
|
}
|
|
1183
1303
|
|
|
@@ -1186,7 +1306,7 @@ fn applyDamage(
|
|
|
1186
1306
|
target_player: Player,
|
|
1187
1307
|
sub_player: Player,
|
|
1188
1308
|
reason: Damage,
|
|
1189
|
-
|
|
1309
|
+
options: anytype,
|
|
1190
1310
|
) !bool {
|
|
1191
1311
|
assert(showdown or battle.last_damage != 0);
|
|
1192
1312
|
|
|
@@ -1201,19 +1321,20 @@ fn applyDamage(
|
|
|
1201
1321
|
subbed.active.volatiles.substitute = 0;
|
|
1202
1322
|
subbed.active.volatiles.Substitute = false;
|
|
1203
1323
|
// battle.last_damage is not updated with the amount of HP the Substitute had
|
|
1204
|
-
try log.end(battle.active(sub_player), .Substitute);
|
|
1324
|
+
try options.log.end(.{ battle.active(sub_player), .Substitute });
|
|
1325
|
+
if (reason != .Confusion) options.calc.capped(sub_player);
|
|
1205
1326
|
return true;
|
|
1206
1327
|
} else {
|
|
1207
1328
|
// 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);
|
|
1329
|
+
subbed.active.volatiles.substitute -= @intCast(battle.last_damage);
|
|
1330
|
+
try options.log.activate(.{ battle.active(sub_player), .Substitute });
|
|
1210
1331
|
return false;
|
|
1211
1332
|
}
|
|
1212
1333
|
}
|
|
1213
1334
|
|
|
1214
1335
|
if (battle.last_damage > target.stored().hp) battle.last_damage = target.stored().hp;
|
|
1215
1336
|
target.stored().hp -= battle.last_damage;
|
|
1216
|
-
try log.damage(battle.active(target_player), target.stored(), reason);
|
|
1337
|
+
try options.log.damage(.{ battle.active(target_player), target.stored(), reason });
|
|
1217
1338
|
return false;
|
|
1218
1339
|
}
|
|
1219
1340
|
|
|
@@ -1221,73 +1342,81 @@ fn mirrorMove(
|
|
|
1221
1342
|
battle: anytype,
|
|
1222
1343
|
player: Player,
|
|
1223
1344
|
mslot: u4,
|
|
1345
|
+
rewrap: bool,
|
|
1224
1346
|
auto: bool,
|
|
1225
1347
|
residual: *bool,
|
|
1226
|
-
|
|
1227
|
-
) !?Result {
|
|
1348
|
+
options: anytype,
|
|
1349
|
+
) anyerror!?Result {
|
|
1228
1350
|
var side = battle.side(player);
|
|
1229
1351
|
const foe = battle.foe(player);
|
|
1230
1352
|
|
|
1231
1353
|
side.last_selected_move = foe.last_used_move;
|
|
1232
1354
|
|
|
1233
1355
|
if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
|
|
1234
|
-
try log.fail(battle.active(player), .None);
|
|
1356
|
+
try options.log.fail(.{ battle.active(player), .None });
|
|
1235
1357
|
return null;
|
|
1358
|
+
} else if (!showdown or foe.last_used_move != .Struggle) {
|
|
1359
|
+
incrementPP(side, player, mslot, options);
|
|
1236
1360
|
}
|
|
1237
1361
|
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
return doMove(battle, player, mslot, auto, residual,
|
|
1362
|
+
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
|
|
1363
|
+
return null;
|
|
1364
|
+
}
|
|
1365
|
+
return doMove(battle, player, mslot, rewrap, auto, residual, options);
|
|
1242
1366
|
}
|
|
1243
1367
|
|
|
1244
1368
|
fn metronome(
|
|
1245
1369
|
battle: anytype,
|
|
1246
1370
|
player: Player,
|
|
1247
1371
|
mslot: u4,
|
|
1372
|
+
rewrap: bool,
|
|
1248
1373
|
auto: bool,
|
|
1249
1374
|
residual: *bool,
|
|
1250
|
-
|
|
1251
|
-
) !?Result {
|
|
1375
|
+
options: anytype,
|
|
1376
|
+
) anyerror!?Result {
|
|
1252
1377
|
var side = battle.side(player);
|
|
1253
1378
|
|
|
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
|
-
};
|
|
1266
|
-
|
|
1267
|
-
incrementPP(side, mslot);
|
|
1379
|
+
side.last_selected_move = try Rolls.metronome(battle, player, options);
|
|
1380
|
+
incrementPP(side, player, mslot, options);
|
|
1268
1381
|
|
|
1269
|
-
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual,
|
|
1270
|
-
|
|
1382
|
+
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
|
|
1383
|
+
return null;
|
|
1384
|
+
}
|
|
1385
|
+
return doMove(battle, player, mslot, rewrap, auto, residual, options);
|
|
1271
1386
|
}
|
|
1272
1387
|
|
|
1273
|
-
fn checkHit(battle: anytype, player: Player, move: Move.Data,
|
|
1388
|
+
fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !bool {
|
|
1274
1389
|
var immune = false;
|
|
1275
1390
|
var mist = false;
|
|
1276
|
-
|
|
1391
|
+
|
|
1392
|
+
if (try moveHit(battle, player, move, &immune, &mist, options)) {
|
|
1393
|
+
if (!showdown) try options.chance.commit(player, .hit);
|
|
1394
|
+
return true;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1277
1397
|
assert(!immune);
|
|
1278
1398
|
if (mist) {
|
|
1279
1399
|
assert(!showdown);
|
|
1280
1400
|
const foe_ident = battle.active(player.foe());
|
|
1281
|
-
try log.activate(foe_ident, .Mist);
|
|
1282
|
-
try log.fail(foe_ident, .None);
|
|
1401
|
+
try options.log.activate(.{ foe_ident, .Mist });
|
|
1402
|
+
try options.log.fail(.{ foe_ident, .None });
|
|
1283
1403
|
} else {
|
|
1284
|
-
try log.lastmiss();
|
|
1285
|
-
try log.miss(battle.active(player));
|
|
1404
|
+
try options.log.lastmiss(.{});
|
|
1405
|
+
try options.log.miss(.{battle.active(player)});
|
|
1286
1406
|
}
|
|
1407
|
+
|
|
1408
|
+
if (!showdown) try options.chance.commit(player, .miss);
|
|
1287
1409
|
return false;
|
|
1288
1410
|
}
|
|
1289
1411
|
|
|
1290
|
-
fn moveHit(
|
|
1412
|
+
fn moveHit(
|
|
1413
|
+
battle: anytype,
|
|
1414
|
+
player: Player,
|
|
1415
|
+
move: Move.Data,
|
|
1416
|
+
immune: *bool,
|
|
1417
|
+
mist: *bool,
|
|
1418
|
+
options: anytype,
|
|
1419
|
+
) !bool {
|
|
1291
1420
|
var side = battle.side(player);
|
|
1292
1421
|
const foe = battle.foe(player);
|
|
1293
1422
|
|
|
@@ -1321,23 +1450,24 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
|
|
|
1321
1450
|
}
|
|
1322
1451
|
|
|
1323
1452
|
// GLITCH: Thrash / Petal Dance / Rage get their accuracy overwritten on subsequent hits
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1453
|
+
const state = side.active.volatiles.state;
|
|
1454
|
+
var overwrite = move.effect == .Rage or move.effect == .Thrashing;
|
|
1455
|
+
const overwritten = overwrite and state > 0;
|
|
1456
|
+
assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
|
|
1457
|
+
|
|
1458
|
+
var accuracy: u16 = if (overwritten) state else move.accuracy;
|
|
1459
|
+
var boost = BOOSTS[@as(u4, @intCast(@as(i8, side.active.boosts.accuracy) + 6))];
|
|
1329
1460
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1330
|
-
boost = BOOSTS[@
|
|
1461
|
+
boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
|
|
1331
1462
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1332
1463
|
accuracy = @min(255, @max(1, accuracy));
|
|
1333
1464
|
|
|
1334
|
-
|
|
1465
|
+
// Pokémon Showdown only overwrites if the volatile is present
|
|
1466
|
+
if (showdown) overwrite = side.active.volatiles.Rage or side.active.volatiles.Thrashing;
|
|
1467
|
+
if (overwrite) side.active.volatiles.state = accuracy;
|
|
1335
1468
|
|
|
1336
1469
|
// 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;
|
|
1470
|
+
break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
|
|
1341
1471
|
};
|
|
1342
1472
|
|
|
1343
1473
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1347,16 +1477,20 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
|
|
|
1347
1477
|
}
|
|
1348
1478
|
|
|
1349
1479
|
if (!miss) return true;
|
|
1350
|
-
battle.last_damage = 0;
|
|
1480
|
+
if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
|
|
1481
|
+
|
|
1351
1482
|
side.active.volatiles.Binding = false;
|
|
1483
|
+
options.chance.observe(.binding, player, .None);
|
|
1484
|
+
|
|
1352
1485
|
return false;
|
|
1353
1486
|
}
|
|
1354
1487
|
|
|
1355
1488
|
fn checkFaint(
|
|
1356
1489
|
battle: anytype,
|
|
1357
1490
|
player: Player,
|
|
1358
|
-
|
|
1359
|
-
|
|
1491
|
+
cap: bool,
|
|
1492
|
+
options: anytype,
|
|
1493
|
+
) @TypeOf(options.log).Error!?Result {
|
|
1360
1494
|
const side = battle.side(player);
|
|
1361
1495
|
if (side.stored().hp > 0) return null;
|
|
1362
1496
|
|
|
@@ -1364,33 +1498,32 @@ fn checkFaint(
|
|
|
1364
1498
|
const foe_fainted = foe.stored().hp == 0;
|
|
1365
1499
|
|
|
1366
1500
|
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;
|
|
1501
|
+
const foe_out = foe_fainted and findFirstAlive(foe) == 0;
|
|
1502
|
+
const more = player_out or foe_out;
|
|
1370
1503
|
|
|
1371
|
-
if (try faint(battle, player,
|
|
1372
|
-
if (foe_fainted) if (try faint(battle, player.foe(),
|
|
1504
|
+
if (try faint(battle, player, !(more or foe_fainted), cap, options)) |r| return r;
|
|
1505
|
+
if (foe_fainted) if (try faint(battle, player.foe(), !more, false, options)) |r| return r;
|
|
1373
1506
|
|
|
1374
1507
|
assert(!side.active.volatiles.MultiHit);
|
|
1375
1508
|
assert(!foe.active.volatiles.MultiHit);
|
|
1376
1509
|
|
|
1377
|
-
if (
|
|
1378
|
-
try log.tie();
|
|
1510
|
+
if (player_out and foe_out) {
|
|
1511
|
+
try options.log.tie(.{});
|
|
1379
1512
|
return Result.Tie;
|
|
1380
1513
|
} else if (player_out) {
|
|
1381
|
-
try log.win(player.foe());
|
|
1514
|
+
try options.log.win(.{player.foe()});
|
|
1382
1515
|
return if (player == .P1) Result.Lose else Result.Win;
|
|
1383
1516
|
} else if (foe_out) {
|
|
1384
|
-
try log.win(player);
|
|
1517
|
+
try options.log.win(.{player});
|
|
1385
1518
|
return if (player == .P1) Result.Win else Result.Lose;
|
|
1386
1519
|
}
|
|
1387
1520
|
|
|
1388
1521
|
const foe_choice: Choice.Type = if (foe_fainted) .Switch else .Pass;
|
|
1389
|
-
if (player == .P1) return
|
|
1390
|
-
return
|
|
1522
|
+
if (player == .P1) return .{ .p1 = .Switch, .p2 = foe_choice };
|
|
1523
|
+
return .{ .p1 = foe_choice, .p2 = .Switch };
|
|
1391
1524
|
}
|
|
1392
1525
|
|
|
1393
|
-
fn faint(battle: anytype, player: Player,
|
|
1526
|
+
fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytype) !?Result {
|
|
1394
1527
|
var side = battle.side(player);
|
|
1395
1528
|
var foe = battle.foe(player);
|
|
1396
1529
|
assert(side.stored().hp == 0);
|
|
@@ -1423,32 +1556,33 @@ fn faint(battle: anytype, player: Player, log: anytype, done: bool) !?Result {
|
|
|
1423
1556
|
side.stored().stats.spe;
|
|
1424
1557
|
}
|
|
1425
1558
|
|
|
1426
|
-
try log.faint(battle.active(player), done);
|
|
1559
|
+
try options.log.faint(.{ battle.active(player), done });
|
|
1560
|
+
// We can only safely set capped if the faint occured as the direct result of an opponent's
|
|
1561
|
+
// move (as opposed to due to residual damage, recoil, Explosion, etc)
|
|
1562
|
+
if (cap) options.calc.capped(player);
|
|
1427
1563
|
return null;
|
|
1428
1564
|
}
|
|
1429
1565
|
|
|
1430
|
-
fn handleResidual(battle: anytype, player: Player,
|
|
1566
|
+
fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
|
|
1431
1567
|
var side = battle.side(player);
|
|
1432
1568
|
var stored = side.stored();
|
|
1433
1569
|
const ident = battle.active(player);
|
|
1434
1570
|
var volatiles = &side.active.volatiles;
|
|
1435
1571
|
|
|
1436
1572
|
const brn = Status.is(stored.status, .BRN);
|
|
1437
|
-
if (brn or Status.is(stored.status, .PSN))
|
|
1573
|
+
if (brn or Status.is(stored.status, .PSN)) {
|
|
1438
1574
|
var damage = @max(stored.stats.hp / 16, 1);
|
|
1439
|
-
|
|
1440
1575
|
if (volatiles.Toxic) {
|
|
1441
1576
|
volatiles.toxic += 1;
|
|
1442
1577
|
damage *= volatiles.toxic;
|
|
1443
1578
|
}
|
|
1579
|
+
damage = @min(damage, stored.hp);
|
|
1444
1580
|
|
|
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);
|
|
1581
|
+
if (!showdown or damage > 0) {
|
|
1582
|
+
stored.hp -= damage;
|
|
1583
|
+
// Pokémon Showdown uses damage [of] here but its not relevant in Generation I
|
|
1584
|
+
try options.log.damage(.{ ident, stored, if (brn) Damage.Burn else Damage.Poison });
|
|
1585
|
+
}
|
|
1452
1586
|
}
|
|
1453
1587
|
|
|
1454
1588
|
if (volatiles.LeechSeed) {
|
|
@@ -1469,49 +1603,46 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
|
|
|
1469
1603
|
damage *= volatiles.toxic;
|
|
1470
1604
|
}
|
|
1471
1605
|
|
|
1472
|
-
// Pokémon Showdown erroneously updates last damage with uncapped Leech Seed damage
|
|
1473
|
-
if (showdown) battle.last_damage = damage;
|
|
1474
|
-
|
|
1475
1606
|
const amount = @min(damage, stored.hp);
|
|
1476
1607
|
stored.hp -= amount;
|
|
1477
1608
|
|
|
1478
|
-
// As above, Pokémon Showdown uses
|
|
1479
|
-
if (amount > 0) try log.damage(ident, stored, .LeechSeed);
|
|
1609
|
+
// As above, Pokémon Showdown uses damage [of] but its not relevant
|
|
1610
|
+
if (amount > 0) try options.log.damage(.{ ident, stored, .LeechSeed });
|
|
1480
1611
|
|
|
1481
1612
|
const before = foe_stored.hp;
|
|
1482
1613
|
// Uncapped damage is added back to the foe
|
|
1483
1614
|
foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
|
|
1484
1615
|
// 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);
|
|
1616
|
+
if (foe_stored.hp > before) try options.log.heal(.{ foe_ident, foe_stored, .Silent });
|
|
1486
1617
|
}
|
|
1487
1618
|
}
|
|
1488
1619
|
|
|
1489
|
-
fn endTurn(battle: anytype,
|
|
1620
|
+
fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result {
|
|
1490
1621
|
assert(!battle.side(.P1).active.volatiles.MultiHit);
|
|
1491
1622
|
assert(!battle.side(.P2).active.volatiles.MultiHit);
|
|
1492
1623
|
|
|
1493
|
-
|
|
1494
|
-
|
|
1624
|
+
battle.turn += 1;
|
|
1625
|
+
|
|
1626
|
+
if (showdown and pkmn.options.ebc and checkEBC(battle)) {
|
|
1627
|
+
try options.log.tie(.{});
|
|
1495
1628
|
return Result.Tie;
|
|
1496
1629
|
}
|
|
1497
1630
|
|
|
1498
|
-
battle.turn += 1;
|
|
1499
|
-
|
|
1500
1631
|
if (showdown and battle.turn >= 1000) {
|
|
1501
|
-
try log.tie();
|
|
1632
|
+
try options.log.tie(.{});
|
|
1502
1633
|
return Result.Tie;
|
|
1503
1634
|
} else if (battle.turn >= 65535) {
|
|
1504
1635
|
return Result.Error;
|
|
1505
1636
|
}
|
|
1506
1637
|
|
|
1507
|
-
try log.turn(battle.turn);
|
|
1638
|
+
try options.log.turn(.{battle.turn});
|
|
1508
1639
|
|
|
1509
1640
|
return Result.Default;
|
|
1510
1641
|
}
|
|
1511
1642
|
|
|
1512
1643
|
fn checkEBC(battle: anytype) bool {
|
|
1513
1644
|
for (battle.sides, 0..) |side, i| {
|
|
1514
|
-
const foe = battle.sides[~@
|
|
1645
|
+
const foe = battle.sides[~@as(u1, @intCast(i))];
|
|
1515
1646
|
|
|
1516
1647
|
var foe_all_ghosts = true;
|
|
1517
1648
|
var foe_all_transform = true;
|
|
@@ -1563,105 +1694,126 @@ fn checkEBC(battle: anytype) bool {
|
|
|
1563
1694
|
return false;
|
|
1564
1695
|
}
|
|
1565
1696
|
|
|
1566
|
-
|
|
1697
|
+
fn disabled(side: *Side, ident: ID, options: anytype) !bool {
|
|
1698
|
+
if (side.active.volatiles.disable_move != 0) {
|
|
1699
|
+
// A Pokémon that transforms after being disabled may end up with less move slots
|
|
1700
|
+
const m = side.active.moves[side.active.volatiles.disable_move - 1].id;
|
|
1701
|
+
// side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
|
|
1702
|
+
const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
|
|
1703
|
+
m
|
|
1704
|
+
else
|
|
1705
|
+
side.last_selected_move;
|
|
1706
|
+
if (m != .None and m == last) {
|
|
1707
|
+
side.active.volatiles.Charging = false;
|
|
1708
|
+
try options.log.cant(.{ ident, .Disable, last });
|
|
1709
|
+
return true;
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
return false;
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
fn buildRage(battle: anytype, who: Player, options: anytype) !void {
|
|
1716
|
+
const side = battle.side(who);
|
|
1717
|
+
if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
|
|
1718
|
+
try Effects.boost(battle, who, Move.get(.Rage), options);
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
fn onBegin(
|
|
1567
1723
|
battle: anytype,
|
|
1568
1724
|
player: Player,
|
|
1569
1725
|
move: Move.Data,
|
|
1570
1726
|
mslot: u8,
|
|
1571
1727
|
residual: *bool,
|
|
1572
|
-
|
|
1728
|
+
options: anytype,
|
|
1573
1729
|
) !void {
|
|
1574
|
-
assert(move.effect.onBegin());
|
|
1575
1730
|
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,
|
|
1731
|
+
.Confusion => Effects.confusion(battle, player, move, options),
|
|
1732
|
+
.Conversion => Effects.conversion(battle, player, options),
|
|
1733
|
+
.FocusEnergy => Effects.focusEnergy(battle, player, options),
|
|
1734
|
+
.Haze => Effects.haze(battle, player, options),
|
|
1735
|
+
.Heal => Effects.heal(battle, player, options),
|
|
1736
|
+
.LeechSeed => Effects.leechSeed(battle, player, move, options),
|
|
1737
|
+
.LightScreen => Effects.lightScreen(battle, player, options),
|
|
1738
|
+
.Mimic => Effects.mimic(battle, player, move, mslot, options),
|
|
1739
|
+
.Mist => Effects.mist(battle, player, options),
|
|
1740
|
+
.Paralyze => Effects.paralyze(battle, player, move, options),
|
|
1741
|
+
.Poison => Effects.poison(battle, player, move, options),
|
|
1742
|
+
.Reflect => Effects.reflect(battle, player, options),
|
|
1743
|
+
.Splash => Effects.splash(battle, player, options),
|
|
1744
|
+
.Substitute => Effects.substitute(battle, player, residual, options),
|
|
1745
|
+
.SwitchAndTeleport => Effects.switchAndTeleport(battle, player, move, options),
|
|
1746
|
+
.Transform => Effects.transform(battle, player, options),
|
|
1592
1747
|
else => unreachable,
|
|
1593
1748
|
};
|
|
1594
1749
|
}
|
|
1595
1750
|
|
|
1596
|
-
|
|
1597
|
-
assert(move.effect.onEnd());
|
|
1751
|
+
fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1598
1752
|
return switch (move.effect) {
|
|
1599
|
-
.Bide => Effects.bide(battle, player,
|
|
1753
|
+
.Bide => Effects.bide(battle, player, options),
|
|
1600
1754
|
// zig fmt: off
|
|
1601
1755
|
.AttackUp1, .AttackUp2, .DefenseUp1, .DefenseUp2,
|
|
1602
1756
|
.EvasionUp1, .SpecialUp1, .SpecialUp2, .SpeedUp2 =>
|
|
1603
|
-
Effects.boost(battle, player, move,
|
|
1757
|
+
Effects.boost(battle, player, move, options),
|
|
1604
1758
|
.AccuracyDown1, .AttackDown1, .DefenseDown1, .DefenseDown2, .SpeedDown1 =>
|
|
1605
|
-
Effects.unboost(battle, player, move,
|
|
1759
|
+
Effects.unboost(battle, player, move, options),
|
|
1606
1760
|
// zig fmt: on
|
|
1607
|
-
.Sleep => Effects.sleep(battle, player, move,
|
|
1761
|
+
.Sleep => Effects.sleep(battle, player, move, options),
|
|
1608
1762
|
else => unreachable,
|
|
1609
1763
|
};
|
|
1610
1764
|
}
|
|
1611
1765
|
|
|
1612
|
-
|
|
1766
|
+
fn alwaysHappens(
|
|
1613
1767
|
battle: anytype,
|
|
1614
1768
|
player: Player,
|
|
1615
1769
|
move: Move.Data,
|
|
1616
1770
|
residual: *bool,
|
|
1617
|
-
|
|
1771
|
+
options: anytype,
|
|
1618
1772
|
) !void {
|
|
1619
|
-
assert(move.effect.alwaysHappens());
|
|
1620
1773
|
return switch (move.effect) {
|
|
1621
|
-
.DrainHP, .DreamEater => Effects.drainHP(battle, player,
|
|
1774
|
+
.DrainHP, .DreamEater => Effects.drainHP(battle, player, options),
|
|
1622
1775
|
.Explode => Effects.explode(battle, player),
|
|
1623
|
-
.PayDay => Effects.payDay(battle, player,
|
|
1776
|
+
.PayDay => Effects.payDay(battle, player, options),
|
|
1624
1777
|
.Rage => Effects.rage(battle, player),
|
|
1625
|
-
.Recoil => Effects.recoil(battle, player, residual,
|
|
1778
|
+
.Recoil => Effects.recoil(battle, player, residual, options),
|
|
1626
1779
|
.JumpKick, .Binding => {},
|
|
1627
1780
|
else => unreachable,
|
|
1628
1781
|
};
|
|
1629
1782
|
}
|
|
1630
1783
|
|
|
1631
|
-
|
|
1784
|
+
fn moveEffect(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1632
1785
|
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,
|
|
1786
|
+
.BurnChance1, .BurnChance2 => Effects.burnChance(battle, player, move, options),
|
|
1787
|
+
.ConfusionChance => Effects.confusion(battle, player, move, options),
|
|
1788
|
+
.FlinchChance1, .FlinchChance2 => Effects.flinchChance(battle, player, move, options),
|
|
1789
|
+
.FreezeChance => Effects.freezeChance(battle, player, move, options),
|
|
1790
|
+
.HyperBeam => Effects.hyperBeam(battle, player, options),
|
|
1638
1791
|
.MultiHit, .DoubleHit, .Twineedle => unreachable,
|
|
1639
|
-
.ParalyzeChance1, .ParalyzeChance2 => Effects.paralyzeChance(battle, player, move,
|
|
1640
|
-
.PoisonChance1, .PoisonChance2 => Effects.poison(battle, player, move,
|
|
1792
|
+
.ParalyzeChance1, .ParalyzeChance2 => Effects.paralyzeChance(battle, player, move, options),
|
|
1793
|
+
.PoisonChance1, .PoisonChance2 => Effects.poison(battle, player, move, options),
|
|
1641
1794
|
// zig fmt: off
|
|
1642
1795
|
.AttackDownChance, .DefenseDownChance, .SpecialDownChance, .SpeedDownChance =>
|
|
1643
|
-
Effects.unboost(battle, player, move,
|
|
1796
|
+
Effects.unboost(battle, player, move, options),
|
|
1644
1797
|
// zig fmt: on
|
|
1645
1798
|
else => {},
|
|
1646
1799
|
};
|
|
1647
1800
|
}
|
|
1648
1801
|
|
|
1649
1802
|
pub const Effects = struct {
|
|
1650
|
-
fn bide(battle: anytype, player: Player,
|
|
1803
|
+
fn bide(battle: anytype, player: Player, options: anytype) !void {
|
|
1651
1804
|
var side = battle.side(player);
|
|
1652
1805
|
|
|
1653
1806
|
side.active.volatiles.Bide = true;
|
|
1654
1807
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1808
|
+
|
|
1655
1809
|
side.active.volatiles.state = 0;
|
|
1656
|
-
side.active.volatiles.attacks =
|
|
1657
|
-
|
|
1658
|
-
else
|
|
1659
|
-
(battle.rng.next() & 1) + 2);
|
|
1810
|
+
side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
1811
|
+
options.chance.observe(.attacking, player, .started);
|
|
1660
1812
|
|
|
1661
|
-
try log.start(battle.active(player), .Bide);
|
|
1813
|
+
try options.log.start(.{ battle.active(player), .Bide });
|
|
1662
1814
|
}
|
|
1663
1815
|
|
|
1664
|
-
fn burnChance(battle: anytype, player: Player, move: Move.Data,
|
|
1816
|
+
fn burnChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1665
1817
|
var foe = battle.foe(player);
|
|
1666
1818
|
var foe_stored = foe.stored();
|
|
1667
1819
|
|
|
@@ -1672,96 +1824,90 @@ pub const Effects = struct {
|
|
|
1672
1824
|
// GLITCH: Freeze top move selection desync can occur if thawed Pokémon is slower
|
|
1673
1825
|
if (Status.is(foe_stored.status, .FRZ)) {
|
|
1674
1826
|
assert(move.type == .Fire);
|
|
1675
|
-
try log.curestatus(
|
|
1827
|
+
try options.log.curestatus(.{
|
|
1828
|
+
battle.active(player.foe()),
|
|
1829
|
+
foe_stored.status,
|
|
1830
|
+
.Message,
|
|
1831
|
+
});
|
|
1676
1832
|
foe_stored.status = 0;
|
|
1677
1833
|
}
|
|
1678
1834
|
return;
|
|
1679
1835
|
}
|
|
1680
1836
|
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
Gen12.percent(10)
|
|
1686
|
-
else
|
|
1687
|
-
Gen12.percent(30));
|
|
1688
|
-
if (!chance) return;
|
|
1837
|
+
if (foe.active.types.includes(move.type)) return;
|
|
1838
|
+
if (!try Rolls.secondaryChance(battle, player, move.effect == .BurnChance1, options)) {
|
|
1839
|
+
return;
|
|
1840
|
+
}
|
|
1689
1841
|
|
|
1690
1842
|
foe_stored.status = Status.init(.BRN);
|
|
1691
1843
|
foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
|
|
1692
1844
|
|
|
1693
|
-
try log.status(battle.active(player.foe()), foe_stored.status, .None);
|
|
1845
|
+
try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
|
|
1694
1846
|
}
|
|
1695
1847
|
|
|
1696
|
-
fn charge(battle: anytype, player: Player,
|
|
1848
|
+
fn charge(battle: anytype, player: Player, options: anytype) !void {
|
|
1697
1849
|
var side = battle.side(player);
|
|
1698
1850
|
var volatiles = &side.active.volatiles;
|
|
1699
1851
|
|
|
1700
1852
|
volatiles.Charging = true;
|
|
1701
1853
|
const move = side.last_selected_move;
|
|
1702
1854
|
if (move == .Fly or move == .Dig) volatiles.Invulnerable = true;
|
|
1703
|
-
try log.laststill();
|
|
1704
|
-
try log.prepare(battle.active(player), move);
|
|
1855
|
+
try options.log.laststill(.{});
|
|
1856
|
+
try options.log.prepare(.{ battle.active(player), move });
|
|
1705
1857
|
}
|
|
1706
1858
|
|
|
1707
|
-
fn confusion(battle: anytype, player: Player, move: Move.Data,
|
|
1859
|
+
fn confusion(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1708
1860
|
var foe = battle.foe(player);
|
|
1709
1861
|
const sub = foe.active.volatiles.Substitute;
|
|
1710
1862
|
|
|
1711
1863
|
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;
|
|
1864
|
+
if (!try Rolls.confusionChance(battle, player, options)) return;
|
|
1717
1865
|
} else {
|
|
1718
1866
|
if (showdown) {
|
|
1719
|
-
if (!try checkHit(battle, player, move,
|
|
1720
|
-
|
|
1721
|
-
} else if (sub) {
|
|
1722
|
-
return log.fail(battle.active(player.foe()), .None);
|
|
1723
|
-
}
|
|
1867
|
+
if (!try checkHit(battle, player, move, options)) return;
|
|
1868
|
+
if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
1724
1869
|
} else {
|
|
1725
|
-
if (sub) {
|
|
1726
|
-
|
|
1727
|
-
} else if (!try checkHit(battle, player, move, log)) {
|
|
1728
|
-
return;
|
|
1729
|
-
}
|
|
1870
|
+
if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
1871
|
+
if (!try checkHit(battle, player, move, options)) return;
|
|
1730
1872
|
}
|
|
1731
1873
|
}
|
|
1732
1874
|
|
|
1733
1875
|
if (foe.active.volatiles.Confusion) return;
|
|
1734
1876
|
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
1877
|
|
|
1740
|
-
|
|
1878
|
+
foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
1879
|
+
options.chance.observe(.confusion, player.foe(), .started);
|
|
1880
|
+
|
|
1881
|
+
try options.log.start(.{ battle.active(player.foe()), .Confusion });
|
|
1741
1882
|
}
|
|
1742
1883
|
|
|
1743
|
-
fn conversion(battle: anytype, player: Player,
|
|
1884
|
+
fn conversion(battle: anytype, player: Player, options: anytype) !void {
|
|
1744
1885
|
const foe = battle.foe(player);
|
|
1745
1886
|
|
|
1746
1887
|
if (foe.active.volatiles.Invulnerable) {
|
|
1747
|
-
try log.lastmiss();
|
|
1748
|
-
return log.miss(battle.active(player));
|
|
1888
|
+
try options.log.lastmiss(.{});
|
|
1889
|
+
return options.log.miss(.{battle.active(player)});
|
|
1749
1890
|
}
|
|
1750
1891
|
|
|
1751
1892
|
battle.side(player).active.types = foe.active.types;
|
|
1752
|
-
return log.
|
|
1893
|
+
return options.log.start(.{
|
|
1894
|
+
battle.active(player),
|
|
1895
|
+
.TypeChange,
|
|
1896
|
+
foe.active.types,
|
|
1897
|
+
battle.active(player.foe()),
|
|
1898
|
+
});
|
|
1753
1899
|
}
|
|
1754
1900
|
|
|
1755
|
-
fn disable(battle: anytype, player: Player, move: Move.Data,
|
|
1901
|
+
fn disable(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
|
|
1756
1902
|
var foe = battle.foe(player);
|
|
1757
1903
|
var volatiles = &foe.active.volatiles;
|
|
1758
1904
|
const foe_ident = battle.active(player.foe());
|
|
1759
1905
|
|
|
1760
1906
|
// Pokémon Showdown handles hit/miss earlier in doMove
|
|
1761
|
-
if (!showdown and !try checkHit(battle, player, move,
|
|
1907
|
+
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
1762
1908
|
|
|
1763
|
-
if (volatiles.
|
|
1764
|
-
try log.fail(foe_ident, .None);
|
|
1909
|
+
if (volatiles.disable_move != 0) {
|
|
1910
|
+
try options.log.fail(.{ foe_ident, .None });
|
|
1765
1911
|
return null;
|
|
1766
1912
|
}
|
|
1767
1913
|
|
|
@@ -1777,7 +1923,7 @@ pub const Effects = struct {
|
|
|
1777
1923
|
// Technically this is still considered simply a "miss" on the cartridge,
|
|
1778
1924
|
// but diverging from Pokémon Showdown here would mostly just be pedantic
|
|
1779
1925
|
if (n == 0) {
|
|
1780
|
-
try log.fail(foe_ident, .None);
|
|
1926
|
+
try options.log.fail(.{ foe_ident, .None });
|
|
1781
1927
|
return null;
|
|
1782
1928
|
} else if (err) {
|
|
1783
1929
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
@@ -1785,23 +1931,23 @@ pub const Effects = struct {
|
|
|
1785
1931
|
return Result.Error;
|
|
1786
1932
|
}
|
|
1787
1933
|
|
|
1788
|
-
volatiles.
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
(battle.rng.next() & 7) + 1);
|
|
1934
|
+
volatiles.disable_move =
|
|
1935
|
+
@intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
|
|
1936
|
+
volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
|
|
1937
|
+
options.chance.observe(.disable, player.foe(), .started);
|
|
1793
1938
|
|
|
1794
|
-
|
|
1939
|
+
const id = foe.active.move(volatiles.disable_move).id;
|
|
1940
|
+
try options.log.start(.{ foe_ident, .Disable, id });
|
|
1795
1941
|
return null;
|
|
1796
1942
|
}
|
|
1797
1943
|
|
|
1798
|
-
fn drainHP(battle: anytype, player: Player,
|
|
1799
|
-
var
|
|
1800
|
-
|
|
1944
|
+
fn drainHP(battle: anytype, player: Player, options: anytype) !void {
|
|
1945
|
+
var stored = battle.side(player).stored();
|
|
1946
|
+
const ident = battle.active(player);
|
|
1801
1947
|
|
|
1802
1948
|
if (battle.last_damage == 0) {
|
|
1803
1949
|
assert(showdown);
|
|
1804
|
-
return;
|
|
1950
|
+
if (!battle.foe(player).active.volatiles.Substitute) return;
|
|
1805
1951
|
}
|
|
1806
1952
|
|
|
1807
1953
|
const drain = @max(battle.last_damage / 2, 1);
|
|
@@ -1810,7 +1956,7 @@ pub const Effects = struct {
|
|
|
1810
1956
|
if (stored.hp == stored.stats.hp) return;
|
|
1811
1957
|
stored.hp = @min(stored.stats.hp, stored.hp + drain);
|
|
1812
1958
|
|
|
1813
|
-
try log.
|
|
1959
|
+
try options.log.heal(.{ ident, stored, .Drain, battle.active(player.foe()) });
|
|
1814
1960
|
}
|
|
1815
1961
|
|
|
1816
1962
|
fn explode(battle: anytype, player: Player) !void {
|
|
@@ -1824,34 +1970,28 @@ pub const Effects = struct {
|
|
|
1824
1970
|
side.active.volatiles.LeechSeed = false;
|
|
1825
1971
|
}
|
|
1826
1972
|
|
|
1827
|
-
fn flinchChance(battle: anytype, player: Player, move: Move.Data) void {
|
|
1973
|
+
fn flinchChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1828
1974
|
var volatiles = &battle.foe(player).active.volatiles;
|
|
1829
1975
|
|
|
1830
1976
|
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;
|
|
1977
|
+
if (!try Rolls.secondaryChance(battle, player, move.effect == .FlinchChance1, options)) {
|
|
1978
|
+
return;
|
|
1979
|
+
}
|
|
1840
1980
|
|
|
1841
1981
|
volatiles.Flinch = true;
|
|
1842
1982
|
volatiles.Recharging = false;
|
|
1843
1983
|
}
|
|
1844
1984
|
|
|
1845
|
-
fn focusEnergy(battle: anytype, player: Player,
|
|
1985
|
+
fn focusEnergy(battle: anytype, player: Player, options: anytype) !void {
|
|
1846
1986
|
var side = battle.side(player);
|
|
1847
1987
|
|
|
1848
1988
|
if (side.active.volatiles.FocusEnergy) return;
|
|
1849
1989
|
side.active.volatiles.FocusEnergy = true;
|
|
1850
1990
|
|
|
1851
|
-
try log.start(battle.active(player), .FocusEnergy);
|
|
1991
|
+
try options.log.start(.{ battle.active(player), .FocusEnergy });
|
|
1852
1992
|
}
|
|
1853
1993
|
|
|
1854
|
-
fn freezeChance(battle: anytype, player: Player, move: Move.Data,
|
|
1994
|
+
fn freezeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1855
1995
|
var foe = battle.foe(player);
|
|
1856
1996
|
var foe_stored = foe.stored();
|
|
1857
1997
|
const foe_ident = battle.active(player.foe());
|
|
@@ -1862,26 +2002,23 @@ pub const Effects = struct {
|
|
|
1862
2002
|
return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
|
|
1863
2003
|
}
|
|
1864
2004
|
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
else
|
|
1868
|
-
battle.rng.next() < 1 + Gen12.percent(10);
|
|
1869
|
-
if (!chance) return;
|
|
1870
|
-
|
|
2005
|
+
if (foe.active.types.includes(move.type)) return;
|
|
2006
|
+
if (!try Rolls.secondaryChance(battle, player, true, options)) return;
|
|
1871
2007
|
// Freeze Clause Mod
|
|
1872
2008
|
if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
1873
2009
|
|
|
1874
2010
|
foe_stored.status = Status.init(.FRZ);
|
|
1875
2011
|
// GLITCH: Hyper Beam recharging status is not cleared
|
|
1876
2012
|
|
|
1877
|
-
try log.status(foe_ident, foe_stored.status, .None);
|
|
2013
|
+
try options.log.status(.{ foe_ident, foe_stored.status, .None });
|
|
1878
2014
|
}
|
|
1879
2015
|
|
|
1880
|
-
fn haze(battle: anytype, player: Player,
|
|
2016
|
+
fn haze(battle: anytype, player: Player, options: anytype) !void {
|
|
2017
|
+
var log = options.log;
|
|
1881
2018
|
var side = battle.side(player);
|
|
1882
2019
|
var foe = battle.foe(player);
|
|
1883
2020
|
|
|
1884
|
-
|
|
2021
|
+
const side_stored = side.stored();
|
|
1885
2022
|
var foe_stored = foe.stored();
|
|
1886
2023
|
|
|
1887
2024
|
const player_ident = battle.active(player);
|
|
@@ -1893,96 +2030,101 @@ pub const Effects = struct {
|
|
|
1893
2030
|
side.active.stats = side_stored.stats;
|
|
1894
2031
|
foe.active.stats = foe_stored.stats;
|
|
1895
2032
|
|
|
1896
|
-
try log.activate(player_ident, .Haze);
|
|
1897
|
-
try log.clearallboost();
|
|
2033
|
+
try log.activate(.{ player_ident, .Haze });
|
|
2034
|
+
try log.clearallboost(.{});
|
|
1898
2035
|
|
|
1899
2036
|
// Pokémon Showdown clears P1 then P2 instead of status -> side -> foe
|
|
1900
2037
|
if (showdown) {
|
|
1901
2038
|
for (&battle.sides, 0..) |*s, i| {
|
|
1902
|
-
const p = @
|
|
2039
|
+
const p: Player = @enumFromInt(i);
|
|
1903
2040
|
// Pokémon Showdown incorrectly does not prevent sleep/freeze from moving
|
|
1904
2041
|
if (p != player and Status.any(s.stored().status)) {
|
|
1905
|
-
try log.curestatus(foe_ident, foe_stored.status, .Silent);
|
|
2042
|
+
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
1906
2043
|
s.stored().status = 0;
|
|
2044
|
+
options.chance.observe(.sleep, p, .None);
|
|
1907
2045
|
} else if (showdown and s.stored().status == Status.TOX) {
|
|
1908
2046
|
s.stored().status = Status.init(.PSN);
|
|
1909
|
-
try log.status(battle.active(p), s.stored().status, .None);
|
|
2047
|
+
try log.status(.{ battle.active(p), s.stored().status, .None });
|
|
1910
2048
|
}
|
|
1911
|
-
try clearVolatiles(battle, p,
|
|
2049
|
+
try clearVolatiles(battle, p, options);
|
|
1912
2050
|
}
|
|
1913
2051
|
} else {
|
|
1914
2052
|
if (Status.any(foe_stored.status)) {
|
|
1915
2053
|
if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
|
|
1916
2054
|
foe.last_selected_move = .SKIP_TURN;
|
|
2055
|
+
options.chance.observe(.sleep, player.foe(), .None);
|
|
1917
2056
|
}
|
|
1918
|
-
try log.curestatus(foe_ident, foe_stored.status, .Silent);
|
|
2057
|
+
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
1919
2058
|
foe_stored.status = 0;
|
|
1920
2059
|
}
|
|
1921
|
-
try clearVolatiles(battle, player,
|
|
1922
|
-
try clearVolatiles(battle, player.foe(),
|
|
2060
|
+
try clearVolatiles(battle, player, options);
|
|
2061
|
+
try clearVolatiles(battle, player.foe(), options);
|
|
1923
2062
|
}
|
|
1924
2063
|
}
|
|
1925
2064
|
|
|
1926
|
-
fn heal(battle: anytype, player: Player,
|
|
2065
|
+
fn heal(battle: anytype, player: Player, options: anytype) !void {
|
|
1927
2066
|
var side = battle.side(player);
|
|
1928
2067
|
var stored = side.stored();
|
|
1929
2068
|
const ident = battle.active(player);
|
|
1930
2069
|
|
|
1931
2070
|
// GLITCH: HP recovery move failure glitches
|
|
1932
2071
|
const delta = stored.stats.hp - stored.hp;
|
|
1933
|
-
if (delta == 0 or delta & 255 == 255
|
|
2072
|
+
if (delta == 0 or (delta & 255 == 255 and stored.hp % 256 != 0)) {
|
|
2073
|
+
return options.log.fail(.{ ident, .None });
|
|
2074
|
+
}
|
|
1934
2075
|
|
|
1935
2076
|
const rest = side.last_selected_move == .Rest;
|
|
1936
2077
|
if (rest) {
|
|
1937
2078
|
// Adding the sleep status runs the sleep condition handler to roll duration
|
|
1938
2079
|
if (showdown) battle.rng.advance(1);
|
|
1939
2080
|
stored.status = Status.slf(2);
|
|
1940
|
-
try log.
|
|
2081
|
+
try options.log.status(.{ ident, stored.status, .From, Move.Rest });
|
|
1941
2082
|
stored.hp = stored.stats.hp;
|
|
1942
2083
|
} else {
|
|
1943
2084
|
stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
|
|
1944
2085
|
}
|
|
1945
|
-
try log.heal(ident, stored, if (rest) Heal.Silent else Heal.None);
|
|
2086
|
+
try options.log.heal(.{ ident, stored, if (rest) Heal.Silent else Heal.None });
|
|
1946
2087
|
}
|
|
1947
2088
|
|
|
1948
|
-
fn hyperBeam(battle: anytype, player: Player,
|
|
2089
|
+
fn hyperBeam(battle: anytype, player: Player, options: anytype) !void {
|
|
1949
2090
|
battle.side(player).active.volatiles.Recharging = true;
|
|
1950
|
-
try log.mustrecharge(battle.active(player));
|
|
2091
|
+
try options.log.mustrecharge(.{battle.active(player)});
|
|
1951
2092
|
}
|
|
1952
2093
|
|
|
1953
|
-
fn leechSeed(battle: anytype, player: Player, move: Move.Data,
|
|
2094
|
+
fn leechSeed(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1954
2095
|
var foe = battle.foe(player);
|
|
1955
2096
|
|
|
1956
2097
|
if (showdown) {
|
|
1957
2098
|
// Invulnerability trumps type immunity on Pokémon Showdown
|
|
1958
2099
|
if (!foe.active.volatiles.Invulnerable and foe.active.types.includes(.Grass)) {
|
|
1959
|
-
return log.immune(battle.active(player.foe()), .None);
|
|
2100
|
+
return options.log.immune(.{ battle.active(player.foe()), .None });
|
|
1960
2101
|
}
|
|
1961
|
-
if (!try checkHit(battle, player, move,
|
|
2102
|
+
if (!try checkHit(battle, player, move, options)) return;
|
|
1962
2103
|
if (foe.active.volatiles.LeechSeed) return;
|
|
1963
2104
|
} else {
|
|
1964
|
-
if (!try checkHit(battle, player, move,
|
|
2105
|
+
if (!try checkHit(battle, player, move, options)) return;
|
|
1965
2106
|
if (foe.active.types.includes(.Grass) or foe.active.volatiles.LeechSeed) {
|
|
1966
|
-
try log.lastmiss();
|
|
1967
|
-
return log.miss(battle.active(player));
|
|
2107
|
+
try options.log.lastmiss(.{});
|
|
2108
|
+
return options.log.miss(.{battle.active(player)});
|
|
1968
2109
|
}
|
|
1969
2110
|
}
|
|
1970
2111
|
|
|
1971
2112
|
foe.active.volatiles.LeechSeed = true;
|
|
1972
2113
|
|
|
1973
|
-
try log.start(battle.active(player.foe()), .LeechSeed);
|
|
2114
|
+
try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
|
|
1974
2115
|
}
|
|
1975
2116
|
|
|
1976
|
-
fn lightScreen(battle: anytype, player: Player,
|
|
2117
|
+
fn lightScreen(battle: anytype, player: Player, options: anytype) !void {
|
|
1977
2118
|
var side = battle.side(player);
|
|
2119
|
+
const ident = battle.active(player);
|
|
1978
2120
|
|
|
1979
|
-
if (side.active.volatiles.LightScreen) return log.fail(
|
|
2121
|
+
if (side.active.volatiles.LightScreen) return options.log.fail(.{ ident, .None });
|
|
1980
2122
|
side.active.volatiles.LightScreen = true;
|
|
1981
2123
|
|
|
1982
|
-
try log.start(
|
|
2124
|
+
try options.log.start(.{ ident, .LightScreen });
|
|
1983
2125
|
}
|
|
1984
2126
|
|
|
1985
|
-
fn mimic(battle: anytype, player: Player, move: Move.Data, mslot: u8,
|
|
2127
|
+
fn mimic(battle: anytype, player: Player, move: Move.Data, mslot: u8, options: anytype) !void {
|
|
1986
2128
|
var side = battle.side(player);
|
|
1987
2129
|
var foe = battle.foe(player);
|
|
1988
2130
|
|
|
@@ -1998,45 +2140,49 @@ pub const Effects = struct {
|
|
|
1998
2140
|
const has_mimic = has_mimic: {
|
|
1999
2141
|
for (side.active.moves, 0..) |m, i| {
|
|
2000
2142
|
if (m.id == .Mimic) {
|
|
2001
|
-
oslot = @intCast(
|
|
2143
|
+
oslot = @intCast(i + 1);
|
|
2002
2144
|
break :has_mimic true;
|
|
2003
2145
|
}
|
|
2004
2146
|
}
|
|
2005
2147
|
break :has_mimic false;
|
|
2006
2148
|
};
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
return
|
|
2149
|
+
if (!has_mimic) {
|
|
2150
|
+
// Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
|
|
2151
|
+
if (!try checkHit(battle, player, move, options)) return;
|
|
2152
|
+
return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
2011
2153
|
}
|
|
2012
2154
|
}
|
|
2013
|
-
if (!try checkHit(battle, player, move,
|
|
2155
|
+
if (!try checkHit(battle, player, move, options)) return;
|
|
2014
2156
|
|
|
2015
|
-
const rslot =
|
|
2157
|
+
const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
|
|
2016
2158
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
2017
2159
|
|
|
2018
|
-
try log.
|
|
2160
|
+
try options.log.start(.{ battle.active(player), .Mimic, side.active.move(oslot).id });
|
|
2019
2161
|
}
|
|
2020
2162
|
|
|
2021
|
-
fn mist(battle: anytype, player: Player,
|
|
2163
|
+
fn mist(battle: anytype, player: Player, options: anytype) !void {
|
|
2022
2164
|
var side = battle.side(player);
|
|
2023
2165
|
|
|
2024
2166
|
if (side.active.volatiles.Mist) return;
|
|
2025
2167
|
side.active.volatiles.Mist = true;
|
|
2026
2168
|
|
|
2027
|
-
try log.start(battle.active(player), .Mist);
|
|
2169
|
+
try options.log.start(.{ battle.active(player), .Mist });
|
|
2028
2170
|
}
|
|
2029
2171
|
|
|
2030
|
-
fn multiHit(battle: anytype, player: Player, move: Move.Data) void {
|
|
2172
|
+
fn multiHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
2031
2173
|
var side = battle.side(player);
|
|
2032
2174
|
|
|
2033
2175
|
assert(!side.active.volatiles.MultiHit);
|
|
2034
2176
|
side.active.volatiles.MultiHit = true;
|
|
2035
2177
|
|
|
2036
|
-
side.active.volatiles.attacks = if (move.effect == .MultiHit)
|
|
2178
|
+
side.active.volatiles.attacks = if (move.effect == .MultiHit)
|
|
2179
|
+
try Rolls.distribution(battle, .MultiHit, player, options)
|
|
2180
|
+
else
|
|
2181
|
+
2;
|
|
2037
2182
|
}
|
|
2038
2183
|
|
|
2039
|
-
fn paralyze(battle: anytype, player: Player, move: Move.Data,
|
|
2184
|
+
fn paralyze(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
2185
|
+
var log = options.log;
|
|
2040
2186
|
var foe = battle.foe(player);
|
|
2041
2187
|
var foe_stored = foe.stored();
|
|
2042
2188
|
const foe_ident = battle.active(player.foe());
|
|
@@ -2046,27 +2192,28 @@ pub const Effects = struct {
|
|
|
2046
2192
|
|
|
2047
2193
|
if (showdown) {
|
|
2048
2194
|
// Invulnerability trumps type immunity on Pokémon Showdown
|
|
2049
|
-
if (immune and !foe.active.volatiles.Invulnerable)
|
|
2050
|
-
|
|
2195
|
+
if (immune and !foe.active.volatiles.Invulnerable) {
|
|
2196
|
+
return log.immune(.{ foe_ident, .None });
|
|
2197
|
+
} else if (!try checkHit(battle, player, move, options)) return;
|
|
2051
2198
|
}
|
|
2052
2199
|
if (Status.any(foe_stored.status)) {
|
|
2053
|
-
return log.fail(
|
|
2200
|
+
return log.fail(.{
|
|
2054
2201
|
foe_ident,
|
|
2055
|
-
if (Status.is(foe_stored.status, .PAR)) .Paralysis else .None,
|
|
2056
|
-
);
|
|
2202
|
+
if (Status.is(foe_stored.status, .PAR)) Fail.Paralysis else Fail.None,
|
|
2203
|
+
});
|
|
2057
2204
|
}
|
|
2058
2205
|
if (!showdown) {
|
|
2059
|
-
if (immune) return log.immune(foe_ident, .None);
|
|
2060
|
-
if (!try checkHit(battle, player, move,
|
|
2206
|
+
if (immune) return log.immune(.{ foe_ident, .None });
|
|
2207
|
+
if (!try checkHit(battle, player, move, options)) return;
|
|
2061
2208
|
}
|
|
2062
2209
|
|
|
2063
2210
|
foe_stored.status = Status.init(.PAR);
|
|
2064
2211
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
2065
2212
|
|
|
2066
|
-
try log.status(foe_ident, foe_stored.status, .None);
|
|
2213
|
+
try log.status(.{ foe_ident, foe_stored.status, .None });
|
|
2067
2214
|
}
|
|
2068
2215
|
|
|
2069
|
-
fn paralyzeChance(battle: anytype, player: Player, move: Move.Data,
|
|
2216
|
+
fn paralyzeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
2070
2217
|
var foe = battle.foe(player);
|
|
2071
2218
|
var foe_stored = foe.stored();
|
|
2072
2219
|
|
|
@@ -2077,65 +2224,59 @@ pub const Effects = struct {
|
|
|
2077
2224
|
}
|
|
2078
2225
|
|
|
2079
2226
|
// 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;
|
|
2227
|
+
if (foe.active.types.includes(move.type)) return;
|
|
2228
|
+
if (!try Rolls.secondaryChance(battle, player, move.effect == .ParalyzeChance1, options)) {
|
|
2229
|
+
return;
|
|
2230
|
+
}
|
|
2088
2231
|
|
|
2089
2232
|
foe_stored.status = Status.init(.PAR);
|
|
2090
2233
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
2091
2234
|
|
|
2092
|
-
try log.status(battle.active(player.foe()), foe_stored.status, .None);
|
|
2235
|
+
try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
|
|
2093
2236
|
}
|
|
2094
2237
|
|
|
2095
|
-
fn payDay(battle: anytype, player: Player,
|
|
2096
|
-
if (!showdown or !battle.foe(player).active.volatiles.Substitute)
|
|
2238
|
+
fn payDay(battle: anytype, player: Player, options: anytype) !void {
|
|
2239
|
+
if (!showdown or !battle.foe(player).active.volatiles.Substitute) {
|
|
2240
|
+
try options.log.fieldactivate(.{});
|
|
2241
|
+
}
|
|
2097
2242
|
}
|
|
2098
2243
|
|
|
2099
|
-
fn poison(battle: anytype, player: Player, move: Move.Data,
|
|
2244
|
+
fn poison(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
2245
|
+
var log = options.log;
|
|
2100
2246
|
var foe = battle.foe(player);
|
|
2101
2247
|
var foe_stored = foe.stored();
|
|
2102
2248
|
const foe_ident = battle.active(player.foe());
|
|
2103
2249
|
const toxic = battle.side(player).last_selected_move == .Toxic;
|
|
2104
2250
|
|
|
2105
|
-
if (showdown and move.effect == .Poison and !try checkHit(battle, player, move,
|
|
2251
|
+
if (showdown and move.effect == .Poison and !try checkHit(battle, player, move, options)) {
|
|
2106
2252
|
return;
|
|
2107
2253
|
} else if (foe.active.volatiles.Substitute) {
|
|
2108
2254
|
if (move.effect != .Poison) return;
|
|
2109
|
-
return log.fail(foe_ident, .None);
|
|
2255
|
+
return log.fail(.{ foe_ident, .None });
|
|
2110
2256
|
} else if (Status.any(foe_stored.status)) {
|
|
2111
2257
|
if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
|
|
2112
2258
|
// Pokémon Showdown considers Toxic to be a status even in Generation I and so
|
|
2113
2259
|
// 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))
|
|
2260
|
+
return log.fail(.{ foe_ident, if (Status.is(foe_stored.status, .PSN))
|
|
2115
2261
|
if (!showdown)
|
|
2116
|
-
.Poison
|
|
2262
|
+
Fail.Poison
|
|
2117
2263
|
else if (toxic == (foe_stored.status == Status.TOX))
|
|
2118
|
-
if (toxic) .Toxic else .Poison
|
|
2264
|
+
if (toxic) Fail.Toxic else Fail.Poison
|
|
2119
2265
|
else
|
|
2120
|
-
.None
|
|
2266
|
+
Fail.None
|
|
2121
2267
|
else
|
|
2122
|
-
.None);
|
|
2268
|
+
Fail.None });
|
|
2123
2269
|
} else if (foe.active.types.includes(.Poison)) {
|
|
2124
2270
|
if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
|
|
2125
|
-
return log.immune(foe_ident, .None);
|
|
2271
|
+
return log.immune(.{ foe_ident, .None });
|
|
2126
2272
|
}
|
|
2127
2273
|
|
|
2128
2274
|
if (move.effect == .Poison) {
|
|
2129
|
-
if (!showdown and !try checkHit(battle, player, move,
|
|
2275
|
+
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2130
2276
|
} 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;
|
|
2277
|
+
if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
|
|
2278
|
+
return;
|
|
2279
|
+
}
|
|
2139
2280
|
}
|
|
2140
2281
|
|
|
2141
2282
|
foe_stored.status = Status.init(.PSN);
|
|
@@ -2145,39 +2286,46 @@ pub const Effects = struct {
|
|
|
2145
2286
|
foe.active.volatiles.toxic = 0;
|
|
2146
2287
|
}
|
|
2147
2288
|
|
|
2148
|
-
try log.status(foe_ident, foe_stored.status, .None);
|
|
2289
|
+
try log.status(.{ foe_ident, foe_stored.status, .None });
|
|
2149
2290
|
}
|
|
2150
2291
|
|
|
2151
2292
|
fn rage(battle: anytype, player: Player) !void {
|
|
2152
2293
|
var volatiles = &battle.side(player).active.volatiles;
|
|
2153
|
-
if (volatiles.Rage) return;
|
|
2154
|
-
volatiles.Rage = true;
|
|
2155
2294
|
assert(!volatiles.Bide);
|
|
2156
|
-
volatiles.
|
|
2295
|
+
volatiles.Rage = true;
|
|
2157
2296
|
}
|
|
2158
2297
|
|
|
2159
|
-
fn recoil(battle: anytype, player: Player, residual: *bool,
|
|
2298
|
+
fn recoil(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
|
|
2160
2299
|
var side = battle.side(player);
|
|
2161
2300
|
var stored = side.stored();
|
|
2162
2301
|
|
|
2163
|
-
|
|
2302
|
+
assert(showdown or battle.last_damage > 0);
|
|
2303
|
+
if (showdown and battle.last_damage == 0) return;
|
|
2304
|
+
|
|
2305
|
+
const damage: i16 = @intCast(@max(battle.last_damage /
|
|
2164
2306
|
@as(u8, if (side.last_selected_move == .Struggle) 2 else 4), 1));
|
|
2165
|
-
stored.hp = @intCast(
|
|
2307
|
+
stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
|
|
2166
2308
|
|
|
2167
|
-
try log.
|
|
2309
|
+
try options.log.damage(.{
|
|
2310
|
+
battle.active(player),
|
|
2311
|
+
stored,
|
|
2312
|
+
.RecoilOf,
|
|
2313
|
+
battle.active(player.foe()),
|
|
2314
|
+
});
|
|
2168
2315
|
if (showdown and stored.hp == 0) residual.* = false;
|
|
2169
2316
|
}
|
|
2170
2317
|
|
|
2171
|
-
fn reflect(battle: anytype, player: Player,
|
|
2318
|
+
fn reflect(battle: anytype, player: Player, options: anytype) !void {
|
|
2172
2319
|
var side = battle.side(player);
|
|
2320
|
+
const ident = battle.active(player);
|
|
2173
2321
|
|
|
2174
|
-
if (side.active.volatiles.Reflect) return log.fail(
|
|
2322
|
+
if (side.active.volatiles.Reflect) return options.log.fail(.{ ident, .None });
|
|
2175
2323
|
side.active.volatiles.Reflect = true;
|
|
2176
2324
|
|
|
2177
|
-
try log.start(
|
|
2325
|
+
try options.log.start(.{ ident, .Reflect });
|
|
2178
2326
|
}
|
|
2179
2327
|
|
|
2180
|
-
fn sleep(battle: anytype, player: Player, move: Move.Data,
|
|
2328
|
+
fn sleep(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
2181
2329
|
var foe = battle.foe(player);
|
|
2182
2330
|
var foe_stored = foe.stored();
|
|
2183
2331
|
const foe_ident = battle.active(player.foe());
|
|
@@ -2188,13 +2336,13 @@ pub const Effects = struct {
|
|
|
2188
2336
|
// the Sleep Clause Mod might activate, causing us to not actually bypass
|
|
2189
2337
|
} else {
|
|
2190
2338
|
if (Status.any(foe_stored.status)) {
|
|
2191
|
-
return log.fail(
|
|
2339
|
+
return options.log.fail(.{
|
|
2192
2340
|
foe_ident,
|
|
2193
|
-
if (Status.is(foe_stored.status, .SLP)) .Sleep else .None,
|
|
2194
|
-
);
|
|
2341
|
+
if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
|
|
2342
|
+
});
|
|
2195
2343
|
}
|
|
2196
|
-
// If
|
|
2197
|
-
if (!showdown and !try checkHit(battle, player, move,
|
|
2344
|
+
// If moveHit in doMove didn't return true Pokémon Showdown wouldn't be in here
|
|
2345
|
+
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2198
2346
|
}
|
|
2199
2347
|
|
|
2200
2348
|
// Sleep Clause Mod
|
|
@@ -2205,76 +2353,70 @@ pub const Effects = struct {
|
|
|
2205
2353
|
}
|
|
2206
2354
|
foe.active.volatiles.Recharging = false;
|
|
2207
2355
|
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
else loop: {
|
|
2211
|
-
while (true) {
|
|
2212
|
-
const r = battle.rng.next() & 7;
|
|
2213
|
-
if (r != 0) break :loop r;
|
|
2214
|
-
}
|
|
2215
|
-
});
|
|
2356
|
+
foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
|
|
2357
|
+
options.chance.observe(.sleep, player.foe(), .started);
|
|
2216
2358
|
|
|
2217
|
-
|
|
2218
|
-
try log.
|
|
2359
|
+
const last = battle.side(player).last_selected_move;
|
|
2360
|
+
try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
|
|
2219
2361
|
}
|
|
2220
2362
|
|
|
2221
|
-
fn splash(battle: anytype, player: Player,
|
|
2222
|
-
try log.activate(battle.active(player), .Splash);
|
|
2363
|
+
fn splash(battle: anytype, player: Player, options: anytype) !void {
|
|
2364
|
+
try options.log.activate(.{ battle.active(player), .Splash });
|
|
2223
2365
|
}
|
|
2224
2366
|
|
|
2225
|
-
fn substitute(battle: anytype, player: Player, residual: *bool,
|
|
2367
|
+
fn substitute(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
|
|
2226
2368
|
var side = battle.side(player);
|
|
2227
2369
|
if (side.active.volatiles.Substitute) {
|
|
2228
|
-
|
|
2229
|
-
return;
|
|
2370
|
+
return options.log.fail(.{ battle.active(player), .Substitute });
|
|
2230
2371
|
}
|
|
2231
2372
|
|
|
2232
2373
|
assert(side.stored().stats.hp <= 1023);
|
|
2233
2374
|
// Will be 0 if HP is <= 3 meaning that the user gets a 1 HP Substitute for "free"
|
|
2234
|
-
const hp = @intCast(
|
|
2375
|
+
const hp: u8 = @intCast(side.stored().stats.hp / 4);
|
|
2235
2376
|
// Pokénon Showdown incorrectly checks for 1/4 HP based on `target.maxhp / 4` which returns
|
|
2236
2377
|
// a floating point value and thus only correctly implements the Substitute 1/4 glitch when
|
|
2237
2378
|
// the target's HP is exactly divisible by 4 (here we're using an inlined divCeil routine to
|
|
2238
2379
|
// avoid having to convert to floating point)
|
|
2239
2380
|
const required_hp = if (showdown) @divFloor(side.stored().stats.hp - 1, 4) + 1 else hp;
|
|
2240
2381
|
if (side.stored().hp < required_hp) {
|
|
2241
|
-
|
|
2242
|
-
return;
|
|
2382
|
+
return options.log.fail(.{ battle.active(player), .Weak });
|
|
2243
2383
|
}
|
|
2244
2384
|
|
|
2245
2385
|
// GLITCH: can leave the user with 0 HP (faints later) because didn't check '<=' above
|
|
2246
2386
|
side.stored().hp -= hp;
|
|
2247
2387
|
side.active.volatiles.substitute = hp + 1;
|
|
2248
2388
|
side.active.volatiles.Substitute = true;
|
|
2249
|
-
try log.start(battle.active(player), .Substitute);
|
|
2389
|
+
try options.log.start(.{ battle.active(player), .Substitute });
|
|
2250
2390
|
if (hp > 0) {
|
|
2251
|
-
try log.damage(battle.active(player), side.stored(), .None);
|
|
2391
|
+
try options.log.damage(.{ battle.active(player), side.stored(), .None });
|
|
2252
2392
|
if (showdown and side.stored().hp == 0) residual.* = false;
|
|
2253
2393
|
}
|
|
2254
2394
|
}
|
|
2255
2395
|
|
|
2256
|
-
fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data,
|
|
2257
|
-
if (!showdown or battle.side(player).last_selected_move == .Teleport) return;
|
|
2258
|
-
|
|
2396
|
+
fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
2259
2397
|
// Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
|
|
2260
|
-
|
|
2261
|
-
|
|
2398
|
+
if (showdown) {
|
|
2399
|
+
if (battle.side(player).last_selected_move == .Teleport) return;
|
|
2400
|
+
_ = try checkHit(battle, player, move, options);
|
|
2401
|
+
battle.last_damage = 0;
|
|
2402
|
+
} else {
|
|
2403
|
+
try options.log.fail(.{ battle.active(player), .None });
|
|
2404
|
+
try options.log.laststill(.{});
|
|
2405
|
+
}
|
|
2262
2406
|
}
|
|
2263
2407
|
|
|
2264
|
-
fn thrashing(battle: anytype, player: Player) void {
|
|
2408
|
+
fn thrashing(battle: anytype, player: Player, options: anytype) void {
|
|
2265
2409
|
var volatiles = &battle.side(player).active.volatiles;
|
|
2266
2410
|
assert(!volatiles.Thrashing);
|
|
2267
2411
|
assert(!volatiles.Bide);
|
|
2268
2412
|
|
|
2269
2413
|
volatiles.Thrashing = true;
|
|
2270
|
-
volatiles.
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
else
|
|
2274
|
-
(battle.rng.next() & 1) + 2);
|
|
2414
|
+
volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
2415
|
+
|
|
2416
|
+
options.chance.observe(.attacking, player, .started);
|
|
2275
2417
|
}
|
|
2276
2418
|
|
|
2277
|
-
fn transform(battle: anytype, player: Player,
|
|
2419
|
+
fn transform(battle: anytype, player: Player, options: anytype) !void {
|
|
2278
2420
|
var side = battle.side(player);
|
|
2279
2421
|
const foe = battle.foe(player);
|
|
2280
2422
|
const foe_ident = battle.active(player.foe());
|
|
@@ -2300,10 +2442,10 @@ pub const Effects = struct {
|
|
|
2300
2442
|
side.active.moves[i].pp = if (m.id != .None) 5 else 0;
|
|
2301
2443
|
}
|
|
2302
2444
|
|
|
2303
|
-
try log.transform(battle.active(player), foe_ident);
|
|
2445
|
+
try options.log.transform(.{ battle.active(player), foe_ident });
|
|
2304
2446
|
}
|
|
2305
2447
|
|
|
2306
|
-
fn binding(battle: anytype, player: Player) void {
|
|
2448
|
+
fn binding(battle: anytype, player: Player, rewrap: bool, options: anytype) !void {
|
|
2307
2449
|
var side = battle.side(player);
|
|
2308
2450
|
var foe = battle.foe(player);
|
|
2309
2451
|
|
|
@@ -2311,12 +2453,17 @@ pub const Effects = struct {
|
|
|
2311
2453
|
side.active.volatiles.Binding = true;
|
|
2312
2454
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2313
2455
|
// (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
|
|
2314
|
-
if (!showdown)
|
|
2456
|
+
if (!showdown) {
|
|
2457
|
+
foe.active.volatiles.Recharging = false;
|
|
2458
|
+
} else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
|
|
2315
2459
|
|
|
2316
|
-
side.active.volatiles.attacks =
|
|
2460
|
+
side.active.volatiles.attacks =
|
|
2461
|
+
try Rolls.distribution(battle, .Binding, player, options) - 1;
|
|
2462
|
+
options.chance.observe(.binding, player, .started);
|
|
2317
2463
|
}
|
|
2318
2464
|
|
|
2319
|
-
fn boost(battle: anytype, player: Player, move: Move.Data,
|
|
2465
|
+
fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
2466
|
+
var log = options.log;
|
|
2320
2467
|
var side = battle.side(player);
|
|
2321
2468
|
const ident = battle.active(player);
|
|
2322
2469
|
|
|
@@ -2326,85 +2473,87 @@ pub const Effects = struct {
|
|
|
2326
2473
|
switch (move.effect) {
|
|
2327
2474
|
.AttackUp1, .AttackUp2, .Rage => {
|
|
2328
2475
|
assert(boosts.atk >= -6 and boosts.atk <= 6);
|
|
2329
|
-
if (boosts.atk == 6) return
|
|
2476
|
+
if (boosts.atk == 6) return log.fail(.{ ident, .None });
|
|
2330
2477
|
const n: u2 = if (move.effect == .AttackUp2) 2 else 1;
|
|
2331
|
-
boosts.atk = @
|
|
2332
|
-
const reason = if (move.effect == .Rage)
|
|
2478
|
+
boosts.atk = @as(i4, @intCast(@min(6, @as(i8, boosts.atk) + n)));
|
|
2479
|
+
const reason: Boost = if (move.effect == .Rage) .Rage else .Attack;
|
|
2333
2480
|
if (stats.atk == MAX_STAT_VALUE) {
|
|
2334
2481
|
boosts.atk -= 1;
|
|
2335
2482
|
if (showdown) {
|
|
2336
|
-
try log.boost(ident, reason, n);
|
|
2337
|
-
try log.boost(ident,
|
|
2483
|
+
try log.boost(.{ ident, reason, n });
|
|
2484
|
+
try log.boost(.{ ident, .Attack, -1 });
|
|
2338
2485
|
if (move.effect == .Rage) return;
|
|
2339
2486
|
}
|
|
2340
|
-
return
|
|
2487
|
+
return log.fail(.{ ident, .None });
|
|
2341
2488
|
}
|
|
2342
|
-
|
|
2343
|
-
const stat = unmodifiedStats(battle,
|
|
2489
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
|
|
2490
|
+
const stat = unmodifiedStats(battle, side).atk;
|
|
2344
2491
|
stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2345
|
-
try log.boost(ident, reason, n);
|
|
2492
|
+
try log.boost(.{ ident, reason, n });
|
|
2493
|
+
// Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
|
|
2494
|
+
if (showdown and move.effect == .Rage) return;
|
|
2346
2495
|
},
|
|
2347
2496
|
.DefenseUp1, .DefenseUp2 => {
|
|
2348
2497
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
2349
|
-
if (boosts.def == 6) return
|
|
2498
|
+
if (boosts.def == 6) return log.fail(.{ ident, .None });
|
|
2350
2499
|
const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
|
|
2351
|
-
boosts.def = @intCast(
|
|
2500
|
+
boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
|
|
2352
2501
|
if (stats.def == MAX_STAT_VALUE) {
|
|
2353
2502
|
boosts.def -= 1;
|
|
2354
2503
|
if (showdown) {
|
|
2355
|
-
try log.boost(ident, .Defense, n);
|
|
2356
|
-
try log.boost(ident, .Defense, -1);
|
|
2504
|
+
try log.boost(.{ ident, .Defense, n });
|
|
2505
|
+
try log.boost(.{ ident, .Defense, -1 });
|
|
2357
2506
|
}
|
|
2358
|
-
return
|
|
2507
|
+
return log.fail(.{ ident, .None });
|
|
2359
2508
|
}
|
|
2360
|
-
|
|
2361
|
-
const stat = unmodifiedStats(battle,
|
|
2509
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
|
|
2510
|
+
const stat = unmodifiedStats(battle, side).def;
|
|
2362
2511
|
stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2363
|
-
try log.boost(ident, .Defense, n);
|
|
2512
|
+
try log.boost(.{ ident, .Defense, n });
|
|
2364
2513
|
},
|
|
2365
2514
|
.SpeedUp2 => {
|
|
2366
2515
|
assert(boosts.spe >= -6 and boosts.spe <= 6);
|
|
2367
|
-
if (boosts.spe == 6) return
|
|
2368
|
-
boosts.spe = @intCast(
|
|
2516
|
+
if (boosts.spe == 6) return log.fail(.{ ident, .None });
|
|
2517
|
+
boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
|
|
2369
2518
|
if (stats.spe == MAX_STAT_VALUE) {
|
|
2370
2519
|
boosts.spe -= 1;
|
|
2371
2520
|
if (showdown) {
|
|
2372
|
-
try log.boost(ident, .Speed, 2);
|
|
2373
|
-
try log.boost(ident, .Speed, -1);
|
|
2521
|
+
try log.boost(.{ ident, .Speed, 2 });
|
|
2522
|
+
try log.boost(.{ ident, .Speed, -1 });
|
|
2374
2523
|
}
|
|
2375
|
-
return
|
|
2524
|
+
return log.fail(.{ ident, .None });
|
|
2376
2525
|
}
|
|
2377
|
-
|
|
2378
|
-
const stat = unmodifiedStats(battle,
|
|
2526
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
|
|
2527
|
+
const stat = unmodifiedStats(battle, side).spe;
|
|
2379
2528
|
stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2380
|
-
try log.boost(ident, .Speed, 2);
|
|
2529
|
+
try log.boost(.{ ident, .Speed, 2 });
|
|
2381
2530
|
},
|
|
2382
2531
|
.SpecialUp1, .SpecialUp2 => {
|
|
2383
2532
|
assert(boosts.spc >= -6 and boosts.spc <= 6);
|
|
2384
|
-
if (boosts.spc == 6) return
|
|
2533
|
+
if (boosts.spc == 6) return log.fail(.{ ident, .None });
|
|
2385
2534
|
const n: u2 = if (move.effect == .SpecialUp2) 2 else 1;
|
|
2386
|
-
boosts.spc = @intCast(
|
|
2535
|
+
boosts.spc = @intCast(@min(6, @as(i8, boosts.spc) + n));
|
|
2387
2536
|
if (stats.spc == MAX_STAT_VALUE) {
|
|
2388
2537
|
boosts.spc -= 1;
|
|
2389
2538
|
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);
|
|
2539
|
+
try log.boost(.{ ident, .SpecialAttack, n });
|
|
2540
|
+
try log.boost(.{ ident, .SpecialAttack, -1 });
|
|
2541
|
+
try log.boost(.{ ident, .SpecialDefense, n });
|
|
2542
|
+
try log.boost(.{ ident, .SpecialDefense, -1 });
|
|
2394
2543
|
}
|
|
2395
|
-
return
|
|
2544
|
+
return log.fail(.{ ident, .None });
|
|
2396
2545
|
}
|
|
2397
|
-
|
|
2398
|
-
const stat = unmodifiedStats(battle,
|
|
2546
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
|
|
2547
|
+
const stat = unmodifiedStats(battle, side).spc;
|
|
2399
2548
|
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);
|
|
2549
|
+
try log.boost(.{ ident, .SpecialAttack, n });
|
|
2550
|
+
try log.boost(.{ ident, .SpecialDefense, n });
|
|
2402
2551
|
},
|
|
2403
2552
|
.EvasionUp1 => {
|
|
2404
2553
|
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);
|
|
2554
|
+
if (boosts.evasion == 6) return log.fail(.{ ident, .None });
|
|
2555
|
+
boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
|
|
2556
|
+
try log.boost(.{ ident, .Evasion, 1 });
|
|
2408
2557
|
},
|
|
2409
2558
|
else => unreachable,
|
|
2410
2559
|
}
|
|
@@ -2413,106 +2562,105 @@ pub const Effects = struct {
|
|
|
2413
2562
|
statusModify(battle.foe(player).stored().status, &battle.foe(player).active.stats);
|
|
2414
2563
|
}
|
|
2415
2564
|
|
|
2416
|
-
fn unboost(battle: anytype, player: Player, move: Move.Data,
|
|
2565
|
+
fn unboost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
2566
|
+
var log = options.log;
|
|
2417
2567
|
var foe = battle.foe(player);
|
|
2418
2568
|
const foe_ident = battle.active(player.foe());
|
|
2569
|
+
const secondary = move.effect.isStatDownChance();
|
|
2419
2570
|
|
|
2420
|
-
if (foe.active.volatiles.Substitute) {
|
|
2421
|
-
return if (!move.effect.isStatDownChance()) log.fail(foe_ident, .None);
|
|
2422
|
-
}
|
|
2571
|
+
if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(.{ foe_ident, .None });
|
|
2423
2572
|
|
|
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
|
|
2573
|
+
if (secondary) {
|
|
2574
|
+
const proc = try Rolls.unboost(battle, player, options);
|
|
2575
|
+
if (!proc or foe.active.volatiles.Invulnerable) return;
|
|
2576
|
+
} else {
|
|
2577
|
+
// checkHit already checks for Invulnerable
|
|
2578
|
+
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2432
2579
|
}
|
|
2433
2580
|
|
|
2434
2581
|
var stats = &foe.active.stats;
|
|
2435
2582
|
var boosts = &foe.active.boosts;
|
|
2583
|
+
const fail = showdown or !secondary;
|
|
2436
2584
|
|
|
2437
2585
|
switch (move.effect) {
|
|
2438
2586
|
.AttackDown1, .AttackDownChance => {
|
|
2439
2587
|
assert(boosts.atk >= -6 and boosts.atk <= 6);
|
|
2440
|
-
if (boosts.atk == -6) return try log.fail(foe_ident, .None);
|
|
2441
|
-
boosts.atk = @intCast(
|
|
2588
|
+
if (boosts.atk == -6) return if (fail) try log.fail(.{ foe_ident, .None });
|
|
2589
|
+
boosts.atk = @intCast(@max(-6, @as(i8, boosts.atk) - 1));
|
|
2442
2590
|
if (stats.atk == 1) {
|
|
2443
2591
|
boosts.atk += 1;
|
|
2444
2592
|
if (showdown) {
|
|
2445
|
-
try log.boost(foe_ident, .Attack, -1);
|
|
2446
|
-
try log.boost(foe_ident, .Attack, 1);
|
|
2593
|
+
try log.boost(.{ foe_ident, .Attack, -1 });
|
|
2594
|
+
try log.boost(.{ foe_ident, .Attack, 1 });
|
|
2447
2595
|
}
|
|
2448
|
-
return
|
|
2596
|
+
return log.fail(.{ foe_ident, .None });
|
|
2449
2597
|
}
|
|
2450
|
-
|
|
2451
|
-
const stat = unmodifiedStats(battle,
|
|
2598
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
|
|
2599
|
+
const stat = unmodifiedStats(battle, foe).atk;
|
|
2452
2600
|
stats.atk = @max(1, stat * mod[0] / mod[1]);
|
|
2453
|
-
try log.boost(foe_ident, .Attack, -1);
|
|
2601
|
+
try log.boost(.{ foe_ident, .Attack, -1 });
|
|
2454
2602
|
},
|
|
2455
2603
|
.DefenseDown1, .DefenseDown2, .DefenseDownChance => {
|
|
2456
2604
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
2457
|
-
if (boosts.def == -6) return try log.fail(foe_ident, .None);
|
|
2605
|
+
if (boosts.def == -6) return if (fail) try log.fail(.{ foe_ident, .None });
|
|
2458
2606
|
const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
|
|
2459
|
-
boosts.def = @intCast(
|
|
2607
|
+
boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
|
|
2460
2608
|
if (stats.def == 1) {
|
|
2461
2609
|
boosts.def += 1;
|
|
2462
2610
|
if (showdown) {
|
|
2463
|
-
try log.boost(foe_ident, .Defense, -@as(i8, n));
|
|
2464
|
-
try log.boost(foe_ident, .Defense, 1);
|
|
2611
|
+
try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
|
|
2612
|
+
try log.boost(.{ foe_ident, .Defense, 1 });
|
|
2465
2613
|
}
|
|
2466
|
-
return
|
|
2614
|
+
return log.fail(.{ foe_ident, .None });
|
|
2467
2615
|
}
|
|
2468
|
-
|
|
2469
|
-
const stat = unmodifiedStats(battle,
|
|
2616
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
|
|
2617
|
+
const stat = unmodifiedStats(battle, foe).def;
|
|
2470
2618
|
stats.def = @max(1, stat * mod[0] / mod[1]);
|
|
2471
|
-
try log.boost(foe_ident, .Defense, -@as(i8, n));
|
|
2619
|
+
try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
|
|
2472
2620
|
},
|
|
2473
2621
|
.SpeedDown1, .SpeedDownChance => {
|
|
2474
2622
|
assert(boosts.spe >= -6 and boosts.spe <= 6);
|
|
2475
|
-
if (boosts.spe == -6) return try log.fail(foe_ident, .None);
|
|
2476
|
-
boosts.spe = @intCast(
|
|
2623
|
+
if (boosts.spe == -6) return if (fail) try log.fail(.{ foe_ident, .None });
|
|
2624
|
+
boosts.spe = @intCast(@max(-6, @as(i8, boosts.spe) - 1));
|
|
2477
2625
|
if (stats.spe == 1) {
|
|
2478
2626
|
boosts.spe += 1;
|
|
2479
2627
|
if (showdown) {
|
|
2480
|
-
try log.boost(foe_ident, .Speed, -1);
|
|
2481
|
-
try log.boost(foe_ident, .Speed, 1);
|
|
2628
|
+
try log.boost(.{ foe_ident, .Speed, -1 });
|
|
2629
|
+
try log.boost(.{ foe_ident, .Speed, 1 });
|
|
2482
2630
|
}
|
|
2483
|
-
return
|
|
2631
|
+
return log.fail(.{ foe_ident, .None });
|
|
2484
2632
|
}
|
|
2485
|
-
|
|
2486
|
-
const stat = unmodifiedStats(battle,
|
|
2633
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
|
|
2634
|
+
const stat = unmodifiedStats(battle, foe).spe;
|
|
2487
2635
|
stats.spe = @max(1, stat * mod[0] / mod[1]);
|
|
2488
|
-
try log.boost(foe_ident, .Speed, -1);
|
|
2636
|
+
try log.boost(.{ foe_ident, .Speed, -1 });
|
|
2489
2637
|
assert(boosts.spe >= -6);
|
|
2490
2638
|
},
|
|
2491
2639
|
.SpecialDownChance => {
|
|
2492
2640
|
assert(boosts.spc >= -6 and boosts.spc <= 6);
|
|
2493
|
-
if (boosts.spc == -6) return try log.fail(foe_ident, .None);
|
|
2494
|
-
boosts.spc = @intCast(
|
|
2641
|
+
if (boosts.spc == -6) return if (fail) try log.fail(.{ foe_ident, .None });
|
|
2642
|
+
boosts.spc = @intCast(@max(-6, @as(i8, boosts.spc) - 1));
|
|
2495
2643
|
if (stats.spc == 1) {
|
|
2496
2644
|
boosts.spc += 1;
|
|
2497
2645
|
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);
|
|
2646
|
+
try log.boost(.{ foe_ident, .SpecialAttack, -1 });
|
|
2647
|
+
try log.boost(.{ foe_ident, .SpecialAttack, 1 });
|
|
2648
|
+
try log.boost(.{ foe_ident, .SpecialDefense, -1 });
|
|
2649
|
+
try log.boost(.{ foe_ident, .SpecialDefense, 1 });
|
|
2502
2650
|
}
|
|
2503
|
-
return
|
|
2651
|
+
return log.fail(.{ foe_ident, .None });
|
|
2504
2652
|
}
|
|
2505
|
-
|
|
2506
|
-
const stat = unmodifiedStats(battle,
|
|
2653
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
|
|
2654
|
+
const stat = unmodifiedStats(battle, foe).spc;
|
|
2507
2655
|
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);
|
|
2656
|
+
try log.boost(.{ foe_ident, .SpecialAttack, -1 });
|
|
2657
|
+
try log.boost(.{ foe_ident, .SpecialDefense, -1 });
|
|
2510
2658
|
},
|
|
2511
2659
|
.AccuracyDown1 => {
|
|
2512
2660
|
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);
|
|
2661
|
+
if (boosts.accuracy == -6) return if (fail) try log.fail(.{ foe_ident, .None });
|
|
2662
|
+
boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
|
|
2663
|
+
try log.boost(.{ foe_ident, .Accuracy, -1 });
|
|
2516
2664
|
},
|
|
2517
2665
|
else => unreachable,
|
|
2518
2666
|
}
|
|
@@ -2522,8 +2670,7 @@ pub const Effects = struct {
|
|
|
2522
2670
|
}
|
|
2523
2671
|
};
|
|
2524
2672
|
|
|
2525
|
-
fn unmodifiedStats(battle: anytype,
|
|
2526
|
-
const side = battle.side(who);
|
|
2673
|
+
fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
|
|
2527
2674
|
if (!side.active.volatiles.Transform) return &side.stored().stats;
|
|
2528
2675
|
const id = ID.from(side.active.volatiles.transform);
|
|
2529
2676
|
return &battle.side(id.player).pokemon[id.id - 1].stats;
|
|
@@ -2537,37 +2684,40 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
|
|
|
2537
2684
|
}
|
|
2538
2685
|
}
|
|
2539
2686
|
|
|
2540
|
-
|
|
2687
|
+
fn isForced(active: anytype) bool {
|
|
2541
2688
|
return active.volatiles.Recharging or active.volatiles.Rage or
|
|
2542
2689
|
active.volatiles.Thrashing or active.volatiles.Charging;
|
|
2543
2690
|
}
|
|
2544
2691
|
|
|
2545
|
-
fn clearVolatiles(battle: anytype, who: Player,
|
|
2692
|
+
fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
|
|
2693
|
+
var log = options.log;
|
|
2546
2694
|
var side = battle.side(who);
|
|
2547
2695
|
var volatiles = &side.active.volatiles;
|
|
2548
2696
|
const ident = battle.active(who);
|
|
2549
2697
|
|
|
2550
|
-
if (volatiles.
|
|
2551
|
-
volatiles.
|
|
2552
|
-
volatiles.
|
|
2553
|
-
|
|
2698
|
+
if (volatiles.disable_move != 0) {
|
|
2699
|
+
volatiles.disable_move = 0;
|
|
2700
|
+
volatiles.disable_duration = 0;
|
|
2701
|
+
options.chance.observe(.disable, who, .None);
|
|
2702
|
+
try log.end(.{ ident, .DisableSilent });
|
|
2554
2703
|
}
|
|
2555
2704
|
if (volatiles.Confusion) {
|
|
2556
2705
|
// volatiles.confusion is left unchanged
|
|
2557
2706
|
volatiles.Confusion = false;
|
|
2558
|
-
|
|
2707
|
+
options.chance.observe(.confusion, who, .None);
|
|
2708
|
+
try log.end(.{ ident, .ConfusionSilent });
|
|
2559
2709
|
}
|
|
2560
2710
|
if (volatiles.Mist) {
|
|
2561
2711
|
volatiles.Mist = false;
|
|
2562
|
-
try log.end(ident, .
|
|
2712
|
+
try log.end(.{ ident, .MistSilent });
|
|
2563
2713
|
}
|
|
2564
2714
|
if (volatiles.FocusEnergy) {
|
|
2565
2715
|
volatiles.FocusEnergy = false;
|
|
2566
|
-
try log.end(ident, .
|
|
2716
|
+
try log.end(.{ ident, .FocusEnergySilent });
|
|
2567
2717
|
}
|
|
2568
2718
|
if (volatiles.LeechSeed) {
|
|
2569
2719
|
volatiles.LeechSeed = false;
|
|
2570
|
-
try log.end(ident, .
|
|
2720
|
+
try log.end(.{ ident, .LeechSeedSilent });
|
|
2571
2721
|
}
|
|
2572
2722
|
if (!showdown and volatiles.Toxic) {
|
|
2573
2723
|
volatiles.Toxic = false;
|
|
@@ -2575,102 +2725,388 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
|
|
|
2575
2725
|
}
|
|
2576
2726
|
if (volatiles.LightScreen) {
|
|
2577
2727
|
volatiles.LightScreen = false;
|
|
2578
|
-
try log.end(ident, .
|
|
2728
|
+
try log.end(.{ ident, .LightScreenSilent });
|
|
2579
2729
|
}
|
|
2580
2730
|
if (volatiles.Reflect) {
|
|
2581
2731
|
volatiles.Reflect = false;
|
|
2582
|
-
try log.end(ident, .
|
|
2732
|
+
try log.end(.{ ident, .ReflectSilent });
|
|
2583
2733
|
}
|
|
2584
2734
|
if (showdown and volatiles.Toxic) {
|
|
2585
2735
|
volatiles.Toxic = false;
|
|
2586
2736
|
// Pokémon Showdown erroneously clears the toxic counter
|
|
2587
2737
|
volatiles.toxic = 0;
|
|
2588
|
-
try log.end(ident, .
|
|
2738
|
+
try log.end(.{ ident, .ToxicSilent });
|
|
2589
2739
|
}
|
|
2590
2740
|
}
|
|
2591
2741
|
|
|
2592
|
-
fn
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2742
|
+
fn decrement(
|
|
2743
|
+
comptime field: chance.Action.Field,
|
|
2744
|
+
player: Player,
|
|
2745
|
+
options: anytype,
|
|
2746
|
+
n: anytype,
|
|
2747
|
+
) @TypeOf(n) {
|
|
2748
|
+
return if (options.calc.overridden(player, field)) |obs| switch (obs) {
|
|
2749
|
+
.started, .ended => 0,
|
|
2750
|
+
.continuing => if (n > 1) n - 1 else n,
|
|
2751
|
+
else => unreachable,
|
|
2752
|
+
} else n - 1;
|
|
2603
2753
|
}
|
|
2604
2754
|
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2755
|
+
pub const Rolls = struct {
|
|
2756
|
+
fn speedTie(battle: anytype, options: anytype) !bool {
|
|
2757
|
+
const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
|
|
2758
|
+
player == .P1
|
|
2759
|
+
else if (@hasDecl(@TypeOf(battle.rng), "speedTie"))
|
|
2760
|
+
battle.rng.speedTie()
|
|
2761
|
+
else if (showdown)
|
|
2762
|
+
battle.rng.range(u8, 0, 2) == 0
|
|
2763
|
+
else
|
|
2764
|
+
battle.rng.next() < Gen12.percent(50) + 1;
|
|
2765
|
+
|
|
2766
|
+
try options.chance.speedTie(p1);
|
|
2767
|
+
return p1;
|
|
2768
|
+
}
|
|
2769
|
+
|
|
2770
|
+
fn criticalHit(
|
|
2771
|
+
battle: anytype,
|
|
2772
|
+
player: Player,
|
|
2773
|
+
rate: u8,
|
|
2774
|
+
spurious: bool,
|
|
2775
|
+
options: anytype,
|
|
2776
|
+
) !bool {
|
|
2777
|
+
const crit = if (options.calc.overridden(player, .critical_hit)) |val|
|
|
2778
|
+
val == .true
|
|
2779
|
+
else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
|
|
2780
|
+
battle.rng.criticalHit(player, rate)
|
|
2781
|
+
else if (showdown)
|
|
2782
|
+
battle.rng.chance(u8, rate, 256)
|
|
2783
|
+
else
|
|
2784
|
+
std.math.rotl(u8, battle.rng.next(), 3) < rate;
|
|
2785
|
+
|
|
2786
|
+
if (!spurious) try options.chance.criticalHit(player, crit, rate);
|
|
2787
|
+
return crit;
|
|
2609
2788
|
}
|
|
2610
|
-
}
|
|
2611
2789
|
|
|
2612
|
-
fn
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2790
|
+
fn damage(battle: anytype, player: Player, options: anytype) !u8 {
|
|
2791
|
+
const roll = if (options.calc.overridden(player, .damage)) |val|
|
|
2792
|
+
val
|
|
2793
|
+
else if (@hasDecl(@TypeOf(battle.rng), "damage"))
|
|
2794
|
+
battle.rng.damage(player)
|
|
2795
|
+
else roll: {
|
|
2796
|
+
if (showdown) break :roll battle.rng.range(u8, 217, 256);
|
|
2797
|
+
while (true) {
|
|
2798
|
+
const r = std.math.rotr(u8, battle.rng.next(), 1);
|
|
2799
|
+
if (r >= 217) break :roll r;
|
|
2800
|
+
}
|
|
2801
|
+
};
|
|
2616
2802
|
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
else
|
|
2622
|
-
(battle.rng.next() & 3) + 2);
|
|
2803
|
+
assert(roll >= 217 and roll <= 255);
|
|
2804
|
+
try options.chance.damage(player, roll);
|
|
2805
|
+
return roll;
|
|
2806
|
+
}
|
|
2623
2807
|
|
|
2624
|
-
|
|
2625
|
-
|
|
2808
|
+
const uN = if (pkmn.options.miss) u8 else u9;
|
|
2809
|
+
fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) !bool {
|
|
2810
|
+
const acc: uN = if (pkmn.options.miss) accuracy else @as(uN, accuracy) + 1;
|
|
2811
|
+
const ok = if (options.calc.overridden(player, .hit)) |val|
|
|
2812
|
+
val == .true
|
|
2813
|
+
else if (@hasDecl(@TypeOf(battle.rng), "hit"))
|
|
2814
|
+
battle.rng.hit(player, acc)
|
|
2815
|
+
else if (showdown)
|
|
2816
|
+
battle.rng.chance(uN, acc, 256)
|
|
2817
|
+
else
|
|
2818
|
+
battle.rng.next() < acc;
|
|
2626
2819
|
|
|
2627
|
-
|
|
2820
|
+
try options.chance.hit(player, ok, acc);
|
|
2821
|
+
return ok;
|
|
2822
|
+
}
|
|
2628
2823
|
|
|
2629
|
-
fn
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2824
|
+
fn confused(battle: anytype, player: Player, options: anytype) !bool {
|
|
2825
|
+
const cfz = if (options.calc.overridden(player, .confused)) |val|
|
|
2826
|
+
val == .true
|
|
2827
|
+
else if (@hasDecl(@TypeOf(battle.rng), "confused"))
|
|
2828
|
+
battle.rng.confused(player)
|
|
2829
|
+
else if (showdown)
|
|
2830
|
+
!battle.rng.chance(u8, 128, 256)
|
|
2831
|
+
else
|
|
2832
|
+
battle.rng.next() >= Gen12.percent(50) + 1;
|
|
2634
2833
|
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2834
|
+
try options.chance.confused(player, cfz);
|
|
2835
|
+
return cfz;
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2838
|
+
fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
|
|
2839
|
+
const par = if (options.calc.overridden(player, .paralyzed)) |val|
|
|
2840
|
+
val == .true
|
|
2841
|
+
else if (@hasDecl(@TypeOf(battle.rng), "paralyzed"))
|
|
2842
|
+
battle.rng.paralyzed(player)
|
|
2843
|
+
else if (showdown)
|
|
2844
|
+
battle.rng.chance(u8, 63, 256)
|
|
2845
|
+
else
|
|
2846
|
+
battle.rng.next() < Gen12.percent(25);
|
|
2847
|
+
|
|
2848
|
+
try options.chance.paralyzed(player, par);
|
|
2849
|
+
return par;
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2852
|
+
fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
|
|
2853
|
+
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2854
|
+
val == .true
|
|
2855
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2856
|
+
battle.rng.secondaryChance(player, 25)
|
|
2857
|
+
else if (showdown)
|
|
2858
|
+
battle.rng.chance(u8, 25, 256)
|
|
2859
|
+
else
|
|
2860
|
+
battle.rng.next() < Gen12.percent(10);
|
|
2861
|
+
|
|
2862
|
+
try options.chance.secondaryChance(player, proc, 25);
|
|
2863
|
+
return proc;
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2866
|
+
fn secondaryChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
|
|
2867
|
+
const rate: u8 = if (low) 26 else 77;
|
|
2868
|
+
|
|
2869
|
+
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2870
|
+
val == .true
|
|
2871
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2872
|
+
battle.rng.secondaryChance(player, rate)
|
|
2873
|
+
else if (showdown)
|
|
2874
|
+
battle.rng.chance(u8, rate, 256)
|
|
2875
|
+
else
|
|
2876
|
+
battle.rng.next() < 1 + (if (low) Gen12.percent(10) else Gen12.percent(30));
|
|
2877
|
+
|
|
2878
|
+
try options.chance.secondaryChance(player, proc, rate);
|
|
2879
|
+
return proc;
|
|
2880
|
+
}
|
|
2881
|
+
|
|
2882
|
+
fn poisonChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
|
|
2883
|
+
const rate: u8 = if (low) 52 else 103;
|
|
2884
|
+
|
|
2885
|
+
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2886
|
+
val == .true
|
|
2887
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2888
|
+
battle.rng.secondaryChance(player, rate)
|
|
2889
|
+
else if (showdown)
|
|
2890
|
+
battle.rng.chance(u8, rate, 256)
|
|
2891
|
+
else
|
|
2892
|
+
battle.rng.next() < 1 + (if (low) Gen12.percent(20) else Gen12.percent(40));
|
|
2893
|
+
|
|
2894
|
+
try options.chance.secondaryChance(player, proc, rate);
|
|
2895
|
+
return proc;
|
|
2896
|
+
}
|
|
2897
|
+
|
|
2898
|
+
fn unboost(battle: anytype, player: Player, options: anytype) !bool {
|
|
2899
|
+
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2900
|
+
val == .true
|
|
2901
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2902
|
+
battle.rng.secondaryChance(player, 85)
|
|
2903
|
+
else if (showdown)
|
|
2904
|
+
battle.rng.chance(u8, 85, 256)
|
|
2905
|
+
else
|
|
2906
|
+
battle.rng.next() < Gen12.percent(33) + 1;
|
|
2907
|
+
|
|
2908
|
+
try options.chance.secondaryChance(player, proc, 85);
|
|
2909
|
+
return proc;
|
|
2910
|
+
}
|
|
2911
|
+
|
|
2912
|
+
fn metronome(battle: anytype, player: Player, options: anytype) !Move {
|
|
2913
|
+
const move: Move = if (options.calc.overridden(player, .metronome)) |val|
|
|
2914
|
+
val
|
|
2915
|
+
else if (@hasDecl(@TypeOf(battle.rng), "metronome"))
|
|
2916
|
+
battle.rng.metronome(player)
|
|
2917
|
+
else if (showdown)
|
|
2918
|
+
Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
|
|
2919
|
+
else move: {
|
|
2920
|
+
while (true) {
|
|
2921
|
+
const r = battle.rng.next();
|
|
2922
|
+
if (r == 0 or r == @intFromEnum(Move.Metronome)) continue;
|
|
2923
|
+
if (r >= @intFromEnum(Move.Struggle)) continue;
|
|
2924
|
+
break :move @enumFromInt(r);
|
|
2925
|
+
}
|
|
2926
|
+
};
|
|
2927
|
+
|
|
2928
|
+
try options.chance.metronome(player, move);
|
|
2929
|
+
return move;
|
|
2930
|
+
}
|
|
2931
|
+
|
|
2932
|
+
fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
|
|
2933
|
+
const power = if (options.calc.overridden(player, .psywave)) |val|
|
|
2934
|
+
val - 1
|
|
2935
|
+
else if (@hasDecl(@TypeOf(battle.rng), "psywave"))
|
|
2936
|
+
battle.rng.psywave(player)
|
|
2937
|
+
else if (showdown)
|
|
2938
|
+
battle.rng.range(u8, 0, max)
|
|
2939
|
+
else power: {
|
|
2940
|
+
while (true) {
|
|
2941
|
+
const r = battle.rng.next();
|
|
2942
|
+
if (r < max) break :power r;
|
|
2943
|
+
}
|
|
2944
|
+
};
|
|
2945
|
+
|
|
2946
|
+
assert(power < max);
|
|
2947
|
+
try options.chance.psywave(player, power, max);
|
|
2948
|
+
return power;
|
|
2949
|
+
}
|
|
2950
|
+
|
|
2951
|
+
fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2952
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2953
|
+
if (val >= 1 and val <= 7) @as(u3, @intCast(val)) else null
|
|
2954
|
+
else
|
|
2955
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
|
|
2956
|
+
battle.rng.sleepDuration(player)
|
|
2957
|
+
else if (showdown)
|
|
2958
|
+
battle.rng.range(u3, 1, 8)
|
|
2959
|
+
else duration: {
|
|
2960
|
+
while (true) {
|
|
2961
|
+
const r = battle.rng.next() & 7;
|
|
2962
|
+
if (r != 0) break :duration @intCast(r);
|
|
2642
2963
|
}
|
|
2964
|
+
};
|
|
2965
|
+
|
|
2966
|
+
assert(duration >= 1 and duration <= 7);
|
|
2967
|
+
options.chance.duration(player, duration);
|
|
2968
|
+
return duration;
|
|
2969
|
+
}
|
|
2970
|
+
|
|
2971
|
+
fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
|
|
2972
|
+
const duration: u4 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2973
|
+
if (val >= 1 and val <= 8) val else null
|
|
2974
|
+
else
|
|
2975
|
+
null) orelse 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
|
+
if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
|
|
2990
|
+
else
|
|
2991
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
|
|
2992
|
+
battle.rng.confusionDuration(player)
|
|
2993
|
+
else if (showdown)
|
|
2994
|
+
battle.rng.range(u3, 2, 6)
|
|
2995
|
+
else
|
|
2996
|
+
@intCast((battle.rng.next() & 3) + 2);
|
|
2997
|
+
|
|
2998
|
+
assert(duration >= 2 and duration <= 5);
|
|
2999
|
+
options.chance.duration(player, duration);
|
|
3000
|
+
return duration;
|
|
3001
|
+
}
|
|
3002
|
+
|
|
3003
|
+
fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
3004
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
3005
|
+
if (val >= 2 and val <= 3) @as(u3, @intCast(val)) else null
|
|
3006
|
+
else
|
|
3007
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
|
|
3008
|
+
battle.rng.attackingDuration(player)
|
|
3009
|
+
else if (showdown)
|
|
3010
|
+
battle.rng.range(u3, 2, 4)
|
|
3011
|
+
else
|
|
3012
|
+
@intCast((battle.rng.next() & 1) + 2);
|
|
3013
|
+
|
|
3014
|
+
assert(duration >= 2 and duration <= 3);
|
|
3015
|
+
options.chance.duration(player, duration);
|
|
3016
|
+
return duration;
|
|
3017
|
+
}
|
|
3018
|
+
|
|
3019
|
+
const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
|
|
3020
|
+
|
|
3021
|
+
fn distribution(
|
|
3022
|
+
battle: anytype,
|
|
3023
|
+
comptime effect: Move.Effect,
|
|
3024
|
+
player: Player,
|
|
3025
|
+
options: anytype,
|
|
3026
|
+
) !u3 {
|
|
3027
|
+
const roll = if (effect == .Binding) .duration else .multi_hit;
|
|
3028
|
+
const n: u3 = (if (options.calc.overridden(player, roll)) |val|
|
|
3029
|
+
if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
|
|
3030
|
+
else
|
|
3031
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "distribution"))
|
|
3032
|
+
battle.rng.distribution(player)
|
|
3033
|
+
else if (showdown)
|
|
3034
|
+
DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
|
|
3035
|
+
else n: {
|
|
3036
|
+
const r = (battle.rng.next() & 3);
|
|
3037
|
+
break :n @intCast((if (r < 2) r else battle.rng.next() & 3) + 2);
|
|
3038
|
+
};
|
|
3039
|
+
|
|
3040
|
+
assert(n >= 2 and n <= 5);
|
|
3041
|
+
if (effect == .Binding) {
|
|
3042
|
+
options.chance.duration(player, n);
|
|
2643
3043
|
} else {
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
3044
|
+
try options.chance.multiHit(player, n);
|
|
3045
|
+
}
|
|
3046
|
+
return n;
|
|
3047
|
+
}
|
|
3048
|
+
|
|
3049
|
+
fn moveSlot(
|
|
3050
|
+
battle: anytype,
|
|
3051
|
+
player: Player,
|
|
3052
|
+
moves: []MoveSlot,
|
|
3053
|
+
check_pp: u4,
|
|
3054
|
+
options: anytype,
|
|
3055
|
+
) !u4 {
|
|
3056
|
+
// TODO: consider throwing error instead of rerolling?
|
|
3057
|
+
const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
|
|
3058
|
+
if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
|
|
3059
|
+
val
|
|
3060
|
+
else
|
|
3061
|
+
null
|
|
3062
|
+
else
|
|
3063
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
|
|
3064
|
+
battle.rng.moveSlot(player, moves, check_pp)
|
|
3065
|
+
else slot: {
|
|
3066
|
+
if (showdown) {
|
|
3067
|
+
if (check_pp == 0) {
|
|
3068
|
+
var i: usize = moves.len;
|
|
3069
|
+
while (i > 0) {
|
|
3070
|
+
i -= 1;
|
|
3071
|
+
if (moves[i].id != .None) {
|
|
3072
|
+
break :slot battle.rng.range(u4, 0, @as(u4, @intCast(i + 1))) + 1;
|
|
3073
|
+
}
|
|
3074
|
+
}
|
|
3075
|
+
} else {
|
|
3076
|
+
var r = battle.rng.range(u4, 0, check_pp) + 1;
|
|
3077
|
+
var i: usize = 0;
|
|
3078
|
+
while (i < moves.len and r > 0) : (i += 1) {
|
|
3079
|
+
if (moves[i].pp > 0) {
|
|
3080
|
+
r -= 1;
|
|
3081
|
+
if (r == 0) break :slot @intCast(i + 1);
|
|
3082
|
+
}
|
|
3083
|
+
}
|
|
2650
3084
|
}
|
|
2651
3085
|
}
|
|
2652
|
-
return @intCast(u4, i + 1);
|
|
2653
|
-
}
|
|
2654
|
-
}
|
|
2655
3086
|
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
3087
|
+
while (true) {
|
|
3088
|
+
const r: u4 = @intCast(battle.rng.next() & 3);
|
|
3089
|
+
if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) break :slot r + 1;
|
|
3090
|
+
}
|
|
3091
|
+
};
|
|
3092
|
+
|
|
3093
|
+
try options.chance.moveSlot(player, slot, moves, check_pp);
|
|
3094
|
+
return slot;
|
|
2659
3095
|
}
|
|
2660
|
-
}
|
|
3096
|
+
};
|
|
2661
3097
|
|
|
2662
3098
|
test "RNG agreement" {
|
|
2663
3099
|
if (!showdown) return;
|
|
2664
3100
|
var expected: [256]u32 = undefined;
|
|
2665
3101
|
for (0..expected.len) |i| {
|
|
2666
|
-
expected[i] = @intCast(
|
|
3102
|
+
expected[i] = @intCast(i * 0x1000000);
|
|
2667
3103
|
}
|
|
2668
3104
|
|
|
2669
|
-
var spe
|
|
2670
|
-
var cfz
|
|
2671
|
-
var par
|
|
2672
|
-
var brn
|
|
2673
|
-
var eff
|
|
3105
|
+
var spe: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
|
|
3106
|
+
var cfz: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
|
|
3107
|
+
var par: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
|
|
3108
|
+
var brn: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
|
|
3109
|
+
var eff: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
|
|
2674
3110
|
|
|
2675
3111
|
for (0..expected.len) |i| {
|
|
2676
3112
|
try expectEqual(spe.range(u8, 0, 2) == 0, i < Gen12.percent(50) + 1);
|
|
@@ -2706,11 +3142,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
2706
3142
|
const side = battle.side(player);
|
|
2707
3143
|
const foe = battle.foe(player);
|
|
2708
3144
|
|
|
2709
|
-
|
|
3145
|
+
const active = &side.active;
|
|
2710
3146
|
const stored = side.stored();
|
|
2711
3147
|
|
|
2712
3148
|
// 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
|
|
3149
|
+
// Pokémon Showdown instead produces a list with a single move that must be chosen
|
|
2714
3150
|
//
|
|
2715
3151
|
// Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
|
|
2716
3152
|
// here when not in compatibility mode, but the engine needs to be able to differentiate
|
|
@@ -2718,9 +3154,9 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
2718
3154
|
// passing due to being forced into using a move. Instead of introducing another option
|
|
2719
3155
|
// we simply repurpose Move with no move slot, even though pedantically this is not
|
|
2720
3156
|
// strictly correct as the player would not have been presented the option to move or
|
|
2721
|
-
// switch at all
|
|
3157
|
+
// switch at all
|
|
2722
3158
|
if (isForced(active)) {
|
|
2723
|
-
out[n] = .{ .type = .Move, .data = @
|
|
3159
|
+
out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
|
|
2724
3160
|
n += 1;
|
|
2725
3161
|
return n;
|
|
2726
3162
|
}
|
|
@@ -2737,7 +3173,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
2737
3173
|
// On the cartridge, all of these happen after "FIGHT" (indicating you are not
|
|
2738
3174
|
// switching) but before you are allowed to select a move. Pokémon Showdown instead
|
|
2739
3175
|
// either disables all other moves in the case of limited or requires you to select a
|
|
2740
|
-
// move normally if sleeping/frozen/bound
|
|
3176
|
+
// move normally if sleeping/frozen/bound
|
|
2741
3177
|
if (!showdown and (limited or foe.active.volatiles.Binding or
|
|
2742
3178
|
Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
|
|
2743
3179
|
{
|
|
@@ -2750,17 +3186,17 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
2750
3186
|
// Pokémon Showdown handles Bide and Binding moves by checking if the move in question
|
|
2751
3187
|
// is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
|
|
2752
3188
|
// 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
|
|
3189
|
+
// proc-ed move in their moveset) and disabling all other moves
|
|
2754
3190
|
if (limited) {
|
|
2755
3191
|
assert(showdown);
|
|
2756
3192
|
assert(side.last_selected_move != .None);
|
|
2757
3193
|
while (slot <= 4) : (slot += 1) {
|
|
2758
3194
|
const m = active.moves[slot - 1];
|
|
2759
3195
|
if (m.id == .None) break;
|
|
2760
|
-
if (m.id == side.last_selected_move) {
|
|
3196
|
+
if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
|
|
2761
3197
|
// Pokémon Showdown displays Struggle if limited to Bide but unable to pick
|
|
2762
3198
|
const struggle =
|
|
2763
|
-
m.id == .Bide and (m.pp == 0 or active.volatiles.
|
|
3199
|
+
m.id == .Bide and (m.pp == 0 or active.volatiles.disable_move == slot);
|
|
2764
3200
|
const s = if (struggle) 0 else slot;
|
|
2765
3201
|
out[n] = .{ .type = .Move, .data = s };
|
|
2766
3202
|
n += 1;
|
|
@@ -2775,7 +3211,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
2775
3211
|
const m = active.moves[slot - 1];
|
|
2776
3212
|
if (m.id == .None) break;
|
|
2777
3213
|
if (m.pp == 0) continue;
|
|
2778
|
-
if (active.volatiles.
|
|
3214
|
+
if (active.volatiles.disable_move == slot) continue;
|
|
2779
3215
|
out[n] = .{ .type = .Move, .data = slot };
|
|
2780
3216
|
n += 1;
|
|
2781
3217
|
}
|