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