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