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