@pkmn/engine 0.1.0-dev.b9fb6eab → 0.1.0-dev.bcb16a6c
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 +98 -59
- 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/ids.json +12 -12
- package/build/pkg/data/layout.json +25 -3
- package/build/pkg/data/protocol.json +69 -12
- package/build/pkg/data.d.ts +3 -4
- 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 +15 -12
- package/build/pkg/index.js.map +1 -1
- package/build/pkg/protocol.d.ts +4 -2
- package/build/pkg/protocol.js +60 -39
- 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 +98 -79
- package/package.json +65 -36
- package/src/bin/install-pkmn-engine +11 -6
- 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 +21 -11
- package/src/lib/common/js.zig +118 -0
- package/src/lib/common/optional.zig +69 -0
- package/src/lib/common/options.zig +12 -3
- package/src/lib/common/protocol.zig +752 -237
- 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 +268 -154
- 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 +5 -5
- package/src/lib/gen1/data.zig +78 -64
- package/src/lib/gen1/helpers.zig +592 -32
- package/src/lib/gen1/mechanics.zig +1020 -658
- package/src/lib/gen2/README.md +106 -85
- package/src/lib/gen2/data/items.zig +12 -18
- package/src/lib/gen2/data/moves.zig +640 -563
- package/src/lib/gen2/data/species.zig +1 -1
- package/src/lib/gen2/data/types.zig +130 -28
- package/src/lib/gen2/data.zig +131 -87
- package/src/lib/gen2/helpers.zig +16 -22
- package/src/lib/gen2/mechanics.zig +1532 -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,34 +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
|
+
|
|
350
371
|
residual = true;
|
|
351
372
|
replace = battle.side(foe_player).stored().hp == 0;
|
|
352
|
-
|
|
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;
|
|
353
384
|
if (!replace) {
|
|
354
|
-
if (foe_choice.type != .Switch) {
|
|
355
|
-
if (try checkFaint(battle, player,
|
|
385
|
+
if (!calc and foe_choice.type != .Switch) {
|
|
386
|
+
if (try checkFaint(battle, player, options)) |r| return r;
|
|
356
387
|
}
|
|
357
|
-
if (residual) try handleResidual(battle, foe_player,
|
|
358
|
-
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;
|
|
359
390
|
}
|
|
360
391
|
|
|
361
392
|
// Flinch is bugged on Pokémon Showdown because it gets implemented with a duration which causes
|
|
@@ -372,29 +403,32 @@ fn executeMove(
|
|
|
372
403
|
battle: anytype,
|
|
373
404
|
player: Player,
|
|
374
405
|
choice: Choice,
|
|
406
|
+
rewrap: bool,
|
|
407
|
+
skip: bool,
|
|
375
408
|
from: ?Move,
|
|
376
409
|
residual: *bool,
|
|
377
|
-
|
|
410
|
+
options: anytype,
|
|
378
411
|
) !?Result {
|
|
379
412
|
var side = battle.side(player);
|
|
380
413
|
|
|
381
414
|
if (choice.type == .Switch) {
|
|
382
|
-
try switchIn(battle, player,
|
|
415
|
+
try switchIn(battle, player, choice.data, false, options);
|
|
383
416
|
return null;
|
|
384
417
|
}
|
|
385
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
|
|
386
422
|
if (side.last_selected_move == .SKIP_TURN) {
|
|
387
|
-
// Pokémon Showdown overwrites the SKIP_TURN sentinel with its botched move select,
|
|
388
|
-
// Binding instead gets handled in beforeMove after sleep and freeze
|
|
389
423
|
assert(!showdown);
|
|
390
424
|
if (battle.foe(player).active.volatiles.Binding) {
|
|
391
|
-
try log.cant(battle.active(player), .Bound);
|
|
425
|
+
try options.log.cant(battle.active(player), .Bound);
|
|
392
426
|
}
|
|
393
427
|
return null;
|
|
394
428
|
}
|
|
395
429
|
|
|
396
430
|
assert(choice.type == .Move);
|
|
397
|
-
var mslot = @intCast(
|
|
431
|
+
var mslot: u4 = @intCast(choice.data);
|
|
398
432
|
// Sadly, we can't even check `Move.get(side.last_selected_move).effect == .Binding` here
|
|
399
433
|
// because Pokémon Showdown's Mirror Move implementation clobbers side.last_selected_move
|
|
400
434
|
var auto = showdown and side.last_selected_move != .None;
|
|
@@ -403,10 +437,7 @@ fn executeMove(
|
|
|
403
437
|
if (mslot == 0 and side.last_selected_move != .None and side.last_selected_move != .Struggle) {
|
|
404
438
|
// choice.data == 0 only happens with Struggle on Pokémon Showdown
|
|
405
439
|
assert(!showdown);
|
|
406
|
-
mslot = @intCast(
|
|
407
|
-
battle.last_selected_indexes.p1
|
|
408
|
-
else
|
|
409
|
-
battle.last_selected_indexes.p2);
|
|
440
|
+
mslot = @intCast(battle.lastMove(player).index);
|
|
410
441
|
const stored = side.stored();
|
|
411
442
|
// GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
|
|
412
443
|
auto = side.last_selected_move == .HyperBeam or side.active.volatiles.Bide or
|
|
@@ -427,15 +458,12 @@ fn executeMove(
|
|
|
427
458
|
} else if (showdown and side.active.volatiles.Charging) {
|
|
428
459
|
// Incorrect mslot with Pokémon Showdown choice semantics so we need to recover from index
|
|
429
460
|
assert(mslot == 1);
|
|
430
|
-
mslot = @intCast(
|
|
431
|
-
battle.last_selected_indexes.p1
|
|
432
|
-
else
|
|
433
|
-
battle.last_selected_indexes.p2);
|
|
461
|
+
mslot = @intCast(battle.lastMove(player).index);
|
|
434
462
|
}
|
|
435
463
|
|
|
436
464
|
var skip_can = false;
|
|
437
465
|
var skip_pp = false;
|
|
438
|
-
switch (try beforeMove(battle, player, from, residual,
|
|
466
|
+
switch (try beforeMove(battle, player, skip, from, residual, options)) {
|
|
439
467
|
.done => return null,
|
|
440
468
|
.skip_can => skip_can = true,
|
|
441
469
|
.skip_pp => skip_pp = true,
|
|
@@ -443,22 +471,24 @@ fn executeMove(
|
|
|
443
471
|
.err => return @as(?Result, Result.Error),
|
|
444
472
|
}
|
|
445
473
|
|
|
446
|
-
if (!skip_can and !try canMove(battle, player, mslot, auto, skip_pp, from, residual,
|
|
474
|
+
if (!skip_can and !try canMove(battle, player, mslot, auto, skip_pp, from, residual, options)) {
|
|
447
475
|
return null;
|
|
448
476
|
}
|
|
449
477
|
|
|
450
|
-
return doMove(battle, player, mslot, auto, residual,
|
|
478
|
+
return doMove(battle, player, mslot, rewrap, auto, residual, options);
|
|
451
479
|
}
|
|
452
480
|
|
|
453
|
-
const BeforeMove =
|
|
481
|
+
const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
|
|
454
482
|
|
|
455
483
|
fn beforeMove(
|
|
456
484
|
battle: anytype,
|
|
457
485
|
player: Player,
|
|
486
|
+
skip: bool,
|
|
458
487
|
from: ?Move,
|
|
459
488
|
residual: *bool,
|
|
460
|
-
|
|
489
|
+
options: anytype,
|
|
461
490
|
) !BeforeMove {
|
|
491
|
+
var log = options.log;
|
|
462
492
|
var side = battle.side(player);
|
|
463
493
|
const foe = battle.foe(player);
|
|
464
494
|
var active = &side.active;
|
|
@@ -467,11 +497,21 @@ fn beforeMove(
|
|
|
467
497
|
var volatiles = &active.volatiles;
|
|
468
498
|
|
|
469
499
|
if (Status.is(stored.status, .SLP)) {
|
|
470
|
-
const
|
|
500
|
+
const before = stored.status;
|
|
471
501
|
// Even if the EXT bit is set this will still correctly modify the sleep duration
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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);
|
|
475
515
|
stored.status = 0; // clears EXT if present
|
|
476
516
|
} else {
|
|
477
517
|
try log.cant(ident, .Sleep);
|
|
@@ -486,13 +526,15 @@ fn beforeMove(
|
|
|
486
526
|
return .done;
|
|
487
527
|
}
|
|
488
528
|
|
|
489
|
-
if (foe.active.volatiles.Binding) {
|
|
529
|
+
if (skip or foe.active.volatiles.Binding) {
|
|
490
530
|
try log.cant(ident, .Bound);
|
|
491
531
|
return .done;
|
|
492
532
|
}
|
|
493
533
|
|
|
494
|
-
if (
|
|
495
|
-
|
|
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;
|
|
496
538
|
try log.cant(ident, .Flinch);
|
|
497
539
|
return .done;
|
|
498
540
|
}
|
|
@@ -504,21 +546,20 @@ fn beforeMove(
|
|
|
504
546
|
}
|
|
505
547
|
|
|
506
548
|
if (volatiles.disabled_duration > 0) {
|
|
507
|
-
|
|
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
|
+
|
|
508
556
|
if (volatiles.disabled_duration == 0) {
|
|
509
557
|
volatiles.disabled_move = 0;
|
|
510
558
|
try log.end(ident, .Disable);
|
|
511
559
|
}
|
|
512
560
|
}
|
|
513
561
|
// Pokémon Showdown's disable condition has a single onBeforeMove handler
|
|
514
|
-
if (showdown and try disabled(side, ident,
|
|
515
|
-
|
|
516
|
-
// Pokémon Showdown checks for Flinch *after* instead of before
|
|
517
|
-
if (showdown and volatiles.Flinch) {
|
|
518
|
-
volatiles.Flinch = false;
|
|
519
|
-
try log.cant(ident, .Flinch);
|
|
520
|
-
return .done;
|
|
521
|
-
}
|
|
562
|
+
if (showdown and try disabled(side, ident, options)) return .done;
|
|
522
563
|
|
|
523
564
|
// This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
|
|
524
565
|
// before the side had a selected a move - we simply need to assume this leads to a desync
|
|
@@ -530,19 +571,20 @@ fn beforeMove(
|
|
|
530
571
|
if (volatiles.Confusion) {
|
|
531
572
|
assert(volatiles.confusion > 0);
|
|
532
573
|
|
|
533
|
-
|
|
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
|
+
|
|
534
581
|
if (volatiles.confusion == 0) {
|
|
535
582
|
volatiles.Confusion = false;
|
|
536
583
|
try log.end(ident, .Confusion);
|
|
537
584
|
} else {
|
|
538
585
|
try log.activate(ident, .Confusion);
|
|
539
586
|
|
|
540
|
-
|
|
541
|
-
!battle.rng.chance(u8, 128, 256)
|
|
542
|
-
else
|
|
543
|
-
battle.rng.next() >= Gen12.percent(50) + 1;
|
|
544
|
-
|
|
545
|
-
if (confused) {
|
|
587
|
+
if (try Rolls.confused(battle, player, options)) {
|
|
546
588
|
assert(!volatiles.MultiHit);
|
|
547
589
|
if (!volatiles.Rage) volatiles.state = 0;
|
|
548
590
|
volatiles.Bide = false;
|
|
@@ -552,6 +594,7 @@ fn beforeMove(
|
|
|
552
594
|
volatiles.Charging = false;
|
|
553
595
|
volatiles.Binding = false;
|
|
554
596
|
volatiles.Invulnerable = false;
|
|
597
|
+
options.chance.clearDurations(player, null);
|
|
555
598
|
{
|
|
556
599
|
// This feels (and is) disgusting but the cartridge literally just overwrites
|
|
557
600
|
// the opponent's defense with the user's defense and resets it after. As a
|
|
@@ -568,42 +611,53 @@ fn beforeMove(
|
|
|
568
611
|
player
|
|
569
612
|
else
|
|
570
613
|
player.foe();
|
|
614
|
+
|
|
615
|
+
const uncapped = battle.last_damage;
|
|
571
616
|
// Skipping adjustDamage / randomizeDamage / checkHit
|
|
572
|
-
_ = try applyDamage(battle, player, target, .Confusion,
|
|
617
|
+
_ = try applyDamage(battle, player, target, .Confusion, options);
|
|
618
|
+
// Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
|
|
619
|
+
if (showdown) battle.last_damage = uncapped;
|
|
573
620
|
|
|
574
621
|
return .done;
|
|
575
622
|
}
|
|
576
623
|
}
|
|
577
624
|
}
|
|
578
625
|
|
|
579
|
-
if (!showdown and try disabled(side, ident,
|
|
580
|
-
|
|
581
|
-
if (Status.is(stored.status, .PAR)) {
|
|
582
|
-
const paralyzed = if (showdown)
|
|
583
|
-
battle.rng.chance(u8, 63, 256)
|
|
584
|
-
else
|
|
585
|
-
battle.rng.next() < Gen12.percent(25);
|
|
626
|
+
if (!showdown and try disabled(side, ident, options)) return .done;
|
|
586
627
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
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;
|
|
597
638
|
}
|
|
598
639
|
|
|
599
640
|
if (volatiles.Bide) {
|
|
600
641
|
assert(!volatiles.Thrashing and !volatiles.Rage);
|
|
601
642
|
|
|
602
|
-
|
|
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
|
+
}
|
|
603
651
|
|
|
604
652
|
assert(volatiles.attacks > 0);
|
|
605
653
|
|
|
606
|
-
|
|
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
|
+
|
|
607
661
|
if (volatiles.attacks != 0) {
|
|
608
662
|
try log.activate(ident, .Bide);
|
|
609
663
|
return .done;
|
|
@@ -620,15 +674,16 @@ fn beforeMove(
|
|
|
620
674
|
return .done;
|
|
621
675
|
}
|
|
622
676
|
|
|
623
|
-
|
|
624
|
-
|
|
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);
|
|
625
680
|
|
|
626
681
|
// For reasons passing understanding, Pokémon Showdown still inflicts residual damage to
|
|
627
682
|
// Bide's user even if the above damage has caused the foe to faint. It's simpler to always
|
|
628
683
|
// run residual here regardless of whether the foe fainted and opt-out of the default flow
|
|
629
684
|
if (showdown) {
|
|
630
685
|
residual.* = false;
|
|
631
|
-
try handleResidual(battle, player,
|
|
686
|
+
try handleResidual(battle, player, options);
|
|
632
687
|
}
|
|
633
688
|
|
|
634
689
|
return .done;
|
|
@@ -637,20 +692,39 @@ fn beforeMove(
|
|
|
637
692
|
if (volatiles.Thrashing) {
|
|
638
693
|
assert(volatiles.attacks > 0);
|
|
639
694
|
assert(from != null);
|
|
640
|
-
|
|
641
|
-
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)) {
|
|
642
704
|
try log.start(battle.active(player), .ConfusionSilent);
|
|
643
705
|
}
|
|
644
706
|
try log.move(ident, side.last_selected_move, battle.active(player.foe()), from);
|
|
645
|
-
if (showdown
|
|
646
|
-
|
|
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
|
+
}
|
|
647
714
|
}
|
|
648
715
|
return .skip_can;
|
|
649
716
|
}
|
|
650
717
|
|
|
651
718
|
if (volatiles.Binding) {
|
|
652
719
|
assert(volatiles.attacks > 0);
|
|
653
|
-
|
|
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
|
+
|
|
654
728
|
try log.move(
|
|
655
729
|
battle.active(player),
|
|
656
730
|
side.last_selected_move,
|
|
@@ -658,7 +732,11 @@ fn beforeMove(
|
|
|
658
732
|
side.last_selected_move,
|
|
659
733
|
);
|
|
660
734
|
if (showdown or battle.last_damage != 0) {
|
|
661
|
-
|
|
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
|
+
}
|
|
662
740
|
}
|
|
663
741
|
return .done;
|
|
664
742
|
}
|
|
@@ -674,7 +752,7 @@ fn canMove(
|
|
|
674
752
|
skip_pp: bool,
|
|
675
753
|
from: ?Move,
|
|
676
754
|
residual: *bool,
|
|
677
|
-
|
|
755
|
+
options: anytype,
|
|
678
756
|
) !bool {
|
|
679
757
|
var side = battle.side(player);
|
|
680
758
|
const player_ident = battle.active(player);
|
|
@@ -689,8 +767,9 @@ fn canMove(
|
|
|
689
767
|
side.active.volatiles.Charging = false;
|
|
690
768
|
side.active.volatiles.Invulnerable = false;
|
|
691
769
|
} else if (move.effect == .Charge) {
|
|
692
|
-
try log.move(player_ident, side.last_selected_move, .{}, from);
|
|
693
|
-
|
|
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);
|
|
694
773
|
return false;
|
|
695
774
|
}
|
|
696
775
|
|
|
@@ -705,23 +784,35 @@ fn canMove(
|
|
|
705
784
|
// relevant for control flow, but we don't have anything useful to put in the log. This is
|
|
706
785
|
// regrettable, but this information is more "nice to have" than required
|
|
707
786
|
const f = if (from != null and from.? == .None) null else from;
|
|
708
|
-
try log.move(player_ident, side.last_selected_move, battle.active(target), f);
|
|
787
|
+
try options.log.move(player_ident, side.last_selected_move, battle.active(target), f);
|
|
788
|
+
setCounterable(battle, player, side, move);
|
|
709
789
|
|
|
710
790
|
if (move.effect.onBegin()) {
|
|
711
|
-
try onBegin(battle, player, move, mslot, residual,
|
|
791
|
+
try onBegin(battle, player, move, mslot, residual, options);
|
|
712
792
|
return false;
|
|
713
793
|
}
|
|
714
794
|
|
|
715
795
|
if (move.effect == .Thrashing) {
|
|
716
|
-
Effects.thrashing(battle, player);
|
|
796
|
+
Effects.thrashing(battle, player, options);
|
|
717
797
|
} else if (!showdown and move.effect == .Binding) {
|
|
718
798
|
// Pokémon Showdown handles this after hit/miss checks and damage calculation
|
|
719
|
-
Effects.binding(battle, player);
|
|
799
|
+
try Effects.binding(battle, player, false, options);
|
|
720
800
|
}
|
|
721
801
|
|
|
722
802
|
return true;
|
|
723
803
|
}
|
|
724
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
|
+
|
|
725
816
|
fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
|
|
726
817
|
if (side.last_selected_move == .Struggle) return;
|
|
727
818
|
|
|
@@ -732,11 +823,11 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
|
|
|
732
823
|
if (volatiles.Bide) return;
|
|
733
824
|
|
|
734
825
|
assert(active.move(mslot).pp > 0 or auto);
|
|
735
|
-
active.move(mslot).pp = @
|
|
826
|
+
active.move(mslot).pp = @as(u6, @intCast(active.move(mslot).pp)) -% 1;
|
|
736
827
|
if (volatiles.Transform) return;
|
|
737
828
|
|
|
738
829
|
assert(side.stored().move(mslot).pp > 0 or auto);
|
|
739
|
-
side.stored().move(mslot).pp = @
|
|
830
|
+
side.stored().move(mslot).pp = @as(u6, @intCast(side.stored().move(mslot).pp)) -% 1;
|
|
740
831
|
assert(active.move(mslot).pp == side.stored().move(mslot).pp);
|
|
741
832
|
}
|
|
742
833
|
|
|
@@ -744,12 +835,12 @@ fn incrementPP(side: *Side, mslot: u4) void {
|
|
|
744
835
|
var active = &side.active;
|
|
745
836
|
const volatiles = &active.volatiles;
|
|
746
837
|
|
|
747
|
-
active.move(mslot).pp = @
|
|
838
|
+
active.move(mslot).pp = @as(u6, @intCast(active.move(mslot).pp)) +% 1;
|
|
748
839
|
// GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
|
|
749
840
|
if (showdown and volatiles.Transform) return;
|
|
750
841
|
|
|
751
842
|
assert(mslot > 0 and mslot <= 4);
|
|
752
|
-
side.stored().moves[mslot - 1].pp = @
|
|
843
|
+
side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% 1;
|
|
753
844
|
}
|
|
754
845
|
|
|
755
846
|
// Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
|
|
@@ -757,10 +848,12 @@ fn doMove(
|
|
|
757
848
|
battle: anytype,
|
|
758
849
|
player: Player,
|
|
759
850
|
mslot: u4,
|
|
851
|
+
rewrap: bool,
|
|
760
852
|
auto: bool,
|
|
761
853
|
residual: *bool,
|
|
762
|
-
|
|
854
|
+
options: anytype,
|
|
763
855
|
) !?Result {
|
|
856
|
+
var log = options.log;
|
|
764
857
|
var side = battle.side(player);
|
|
765
858
|
const foe = battle.foe(player);
|
|
766
859
|
|
|
@@ -775,37 +868,44 @@ fn doMove(
|
|
|
775
868
|
var hits: u4 = 1;
|
|
776
869
|
var effectiveness = Effectiveness.neutral;
|
|
777
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
|
+
|
|
778
874
|
// The cartridge handles set damage moves in applyDamage but we short circuit to simplify things
|
|
779
875
|
if (move.effect == .SuperFang or move.effect == .SpecialDamage) {
|
|
780
|
-
return specialDamage(battle, player, move,
|
|
876
|
+
return specialDamage(battle, player, move, options);
|
|
781
877
|
}
|
|
782
878
|
|
|
783
879
|
// Pokémon Showdown runs invulnerability / immunity checks before checking accuracy - simply
|
|
784
880
|
// calling moveHit early covers most of that but we also need to check type immunity first
|
|
785
881
|
var miss = showdown and miss: {
|
|
786
882
|
immune = move.target != .Self and !status and !counter and
|
|
787
|
-
(@
|
|
788
|
-
@
|
|
883
|
+
(@intFromEnum(move.type.effectiveness(foe.active.types.type1)) == 0 or
|
|
884
|
+
@intFromEnum(move.type.effectiveness(foe.active.types.type2)) == 0);
|
|
789
885
|
if (immune and move.effect != .Binding) break :miss true;
|
|
790
|
-
if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe)
|
|
791
|
-
|
|
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));
|
|
792
892
|
};
|
|
793
893
|
assert(!immune or miss or (showdown and move.effect == .Binding));
|
|
794
894
|
|
|
795
|
-
var late = showdown;
|
|
895
|
+
var late = showdown and move.effect != .Explode;
|
|
796
896
|
const skip = status or immune;
|
|
797
897
|
if ((!showdown or (!skip or counter)) and !miss) blk: {
|
|
798
898
|
if (showdown and move.effect.isMulti()) {
|
|
799
|
-
Effects.multiHit(battle, player, move);
|
|
899
|
+
try Effects.multiHit(battle, player, move, options);
|
|
800
900
|
hits = side.active.volatiles.attacks;
|
|
801
901
|
late = false;
|
|
802
902
|
}
|
|
803
903
|
|
|
804
904
|
// Cartridge rolls for crit even for moves that can't crit (Counter/Metronome/status/OHKO)
|
|
805
905
|
const check = !showdown or (!counter and move.effect != .OHKO);
|
|
806
|
-
if (check) crit = checkCriticalHit(battle, player, move);
|
|
906
|
+
if (check) crit = try checkCriticalHit(battle, player, move, options);
|
|
807
907
|
|
|
808
|
-
if (counter) return counterDamage(battle, player, move,
|
|
908
|
+
if (counter) return counterDamage(battle, player, move, options);
|
|
809
909
|
|
|
810
910
|
battle.last_damage = 0;
|
|
811
911
|
|
|
@@ -827,17 +927,15 @@ fn doMove(
|
|
|
827
927
|
effectiveness = adjustDamage(battle, player);
|
|
828
928
|
immune = effectiveness == 0;
|
|
829
929
|
}
|
|
830
|
-
randomizeDamage(battle);
|
|
930
|
+
try randomizeDamage(battle, player, options);
|
|
831
931
|
}
|
|
832
932
|
}
|
|
833
933
|
|
|
834
|
-
|
|
835
|
-
if (showdown and skip) battle.last_damage = 0;
|
|
836
|
-
|
|
934
|
+
var zero = battle.last_damage == 0;
|
|
837
935
|
miss = if (showdown or skip)
|
|
838
936
|
miss
|
|
839
937
|
else
|
|
840
|
-
(!moveHit(battle, player, move, &immune, &mist) or
|
|
938
|
+
(!moveHit(battle, player, move, &immune, &mist, options) or zero);
|
|
841
939
|
|
|
842
940
|
assert(showdown or miss or battle.last_damage > 0 or skip);
|
|
843
941
|
assert((!showdown and miss) or !(ohko and immune));
|
|
@@ -845,11 +943,11 @@ fn doMove(
|
|
|
845
943
|
|
|
846
944
|
if (!showdown or !miss) {
|
|
847
945
|
if (move.effect == .MirrorMove) {
|
|
848
|
-
return mirrorMove(battle, player, mslot, auto, residual,
|
|
946
|
+
return mirrorMove(battle, player, mslot, rewrap, auto, residual, options);
|
|
849
947
|
} else if (move.effect == .Metronome) {
|
|
850
|
-
return metronome(battle, player, mslot, auto, residual,
|
|
948
|
+
return metronome(battle, player, mslot, rewrap, auto, residual, options);
|
|
851
949
|
} else if (move.effect.onEnd()) {
|
|
852
|
-
try onEnd(battle, player, move,
|
|
950
|
+
try onEnd(battle, player, move, options);
|
|
853
951
|
return null;
|
|
854
952
|
}
|
|
855
953
|
}
|
|
@@ -862,38 +960,46 @@ fn doMove(
|
|
|
862
960
|
move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
|
|
863
961
|
if (ohko) {
|
|
864
962
|
try log.immune(foe_ident, .OHKO);
|
|
865
|
-
} else if (immune and !invulnerable) {
|
|
963
|
+
} else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
|
|
866
964
|
try log.immune(foe_ident, .None);
|
|
867
965
|
} else if (mist) {
|
|
868
966
|
if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
|
|
869
967
|
try log.fail(foe_ident, .None);
|
|
870
968
|
} else {
|
|
969
|
+
if (showdown or !zero) try options.chance.commit(player, .miss);
|
|
871
970
|
try log.lastmiss();
|
|
872
971
|
try log.miss(battle.active(player));
|
|
873
972
|
}
|
|
874
973
|
if (move.effect == .JumpKick) {
|
|
875
974
|
// Recoil is supposed to be damage/8 but damage will always be 0 here
|
|
876
|
-
assert(battle.last_damage == 0);
|
|
975
|
+
assert(showdown or battle.last_damage == 0);
|
|
877
976
|
battle.last_damage = 1;
|
|
878
|
-
_ = try applyDamage(battle, player, player.foe(), .None,
|
|
977
|
+
_ = try applyDamage(battle, player, player.foe(), .None, options);
|
|
879
978
|
if (showdown and side.stored().hp == 0) residual.* = false;
|
|
880
979
|
} else if (move.effect == .Explode) {
|
|
881
980
|
try Effects.explode(battle, player);
|
|
882
|
-
try buildRage(battle, player.foe(),
|
|
981
|
+
try buildRage(battle, player.foe(), options);
|
|
883
982
|
} else if (showdown and move.effect == .Disable) {
|
|
884
|
-
try buildRage(battle, player.foe(),
|
|
983
|
+
try buildRage(battle, player.foe(), options);
|
|
984
|
+
} else if (!showdown and immune and move.effect == .Binding) {
|
|
985
|
+
try options.chance.commit(player, .binding);
|
|
885
986
|
}
|
|
886
987
|
return null;
|
|
988
|
+
} else {
|
|
989
|
+
try options.chance.commit(player, .hit);
|
|
887
990
|
}
|
|
888
991
|
|
|
889
992
|
// On the cartridge MultiHit doesn't get set up until after damage has been applied for the
|
|
890
993
|
// first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
|
|
891
994
|
// it up above before damage calculation).
|
|
892
995
|
if (!showdown and move.effect.isMulti()) {
|
|
893
|
-
Effects.multiHit(battle, player, move);
|
|
996
|
+
try Effects.multiHit(battle, player, move, options);
|
|
894
997
|
hits = side.active.volatiles.attacks;
|
|
895
998
|
}
|
|
896
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
|
+
|
|
897
1003
|
var nullified = false;
|
|
898
1004
|
var hit: u4 = 0;
|
|
899
1005
|
while (hit < hits) {
|
|
@@ -905,11 +1011,13 @@ fn doMove(
|
|
|
905
1011
|
try log.resisted(battle.active(player.foe()));
|
|
906
1012
|
}
|
|
907
1013
|
}
|
|
908
|
-
if (!skip) nullified = try applyDamage(battle, player.foe(), player.foe(), .None,
|
|
1014
|
+
if (!skip) nullified = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
909
1015
|
if (hit == 0 and ohko) try log.ohko();
|
|
910
1016
|
hit += 1;
|
|
911
1017
|
if (foe.stored().hp == 0) break;
|
|
912
|
-
if (!late
|
|
1018
|
+
if (!late and (!sub or move.effect == .Explode)) {
|
|
1019
|
+
try buildRage(battle, player.foe(), options);
|
|
1020
|
+
}
|
|
913
1021
|
// If the substitute breaks during a multi-hit attack, the attack ends
|
|
914
1022
|
if (nullified) break;
|
|
915
1023
|
}
|
|
@@ -919,17 +1027,23 @@ fn doMove(
|
|
|
919
1027
|
assert(nullified or foe.stored().hp == 0 or side.active.volatiles.attacks - hit == 0);
|
|
920
1028
|
side.active.volatiles.attacks = 0;
|
|
921
1029
|
if (showdown and move.effect == .Twineedle and !nullified and foe.stored().hp > 0) {
|
|
922
|
-
try Effects.poison(battle, player, Move.get(.PoisonSting),
|
|
1030
|
+
try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
923
1031
|
}
|
|
924
1032
|
try log.hitcount(battle.active(player.foe()), hit);
|
|
925
1033
|
} else if (showdown) {
|
|
926
1034
|
// This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
|
|
927
1035
|
if (move.effect == .Binding) {
|
|
928
|
-
Effects.binding(battle, player);
|
|
1036
|
+
try Effects.binding(battle, player, rewrap, options);
|
|
929
1037
|
if (immune) {
|
|
930
1038
|
battle.last_damage = 0;
|
|
1039
|
+
assert(foe.stored().hp > 0);
|
|
931
1040
|
// Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
|
|
932
|
-
|
|
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
|
+
}
|
|
933
1047
|
return null;
|
|
934
1048
|
}
|
|
935
1049
|
}
|
|
@@ -942,12 +1056,12 @@ fn doMove(
|
|
|
942
1056
|
// On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
|
|
943
1057
|
// but this is only done to setup the MultiHit looping in the first place. Moving the MultiHit
|
|
944
1058
|
// setup before the loop means we can avoid having to waste time doing no-op handler searches.
|
|
945
|
-
if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual,
|
|
1059
|
+
if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
|
|
946
1060
|
|
|
947
1061
|
if (foe.stored().hp == 0) return null;
|
|
948
1062
|
|
|
949
|
-
// Pokémon Showdown builds Rage at the wrong time for non-MultiHit
|
|
950
|
-
if (late and move.effect != .Disable) try buildRage(battle, player.foe(),
|
|
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);
|
|
951
1065
|
|
|
952
1066
|
if (!move.effect.isSpecial()) {
|
|
953
1067
|
// On the cartridge Rage is not considered to be "special" and thus gets executed for a
|
|
@@ -958,30 +1072,29 @@ fn doMove(
|
|
|
958
1072
|
// Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
|
|
959
1073
|
// puts the |-status| message before |-hitcount| instead of after.
|
|
960
1074
|
if (move.effect == .Twineedle) {
|
|
961
|
-
if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting),
|
|
1075
|
+
if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
962
1076
|
} else if (move.effect == .Disable) {
|
|
963
|
-
const result = try Effects.disable(battle, player, move,
|
|
964
|
-
if (showdown) try buildRage(battle, player.foe(),
|
|
1077
|
+
const result = try Effects.disable(battle, player, move, options);
|
|
1078
|
+
if (showdown) try buildRage(battle, player.foe(), options);
|
|
965
1079
|
return result;
|
|
966
1080
|
} else {
|
|
967
|
-
try moveEffect(battle, player, move,
|
|
1081
|
+
try moveEffect(battle, player, move, options);
|
|
968
1082
|
}
|
|
969
1083
|
}
|
|
970
1084
|
|
|
971
1085
|
return null;
|
|
972
1086
|
}
|
|
973
1087
|
|
|
974
|
-
fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
|
|
1088
|
+
fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !bool {
|
|
975
1089
|
const side = battle.side(player);
|
|
976
1090
|
|
|
977
1091
|
// Base speed is used for the critical hit calculation, even when Transform-ed
|
|
978
|
-
var
|
|
979
|
-
// GLITCH: Focus Energy reduces critical hit
|
|
980
|
-
|
|
981
|
-
|
|
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;
|
|
982
1096
|
|
|
983
|
-
|
|
984
|
-
return std.math.rotl(u8, battle.rng.next(), 3) < chance;
|
|
1097
|
+
return Rolls.criticalHit(battle, player, @intCast(rate), options);
|
|
985
1098
|
}
|
|
986
1099
|
|
|
987
1100
|
fn calcDamage(
|
|
@@ -1047,7 +1160,7 @@ fn calcDamage(
|
|
|
1047
1160
|
d = @min(997, d);
|
|
1048
1161
|
d += 2;
|
|
1049
1162
|
|
|
1050
|
-
battle.last_damage = @intCast(
|
|
1163
|
+
battle.last_damage = @intCast(d);
|
|
1051
1164
|
|
|
1052
1165
|
return true;
|
|
1053
1166
|
}
|
|
@@ -1061,9 +1174,9 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
|
1061
1174
|
var d = battle.last_damage;
|
|
1062
1175
|
if (side.active.types.includes(move.type)) d +%= d / 2;
|
|
1063
1176
|
|
|
1064
|
-
const neutral = @
|
|
1065
|
-
const eff1: u16 = @
|
|
1066
|
-
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));
|
|
1067
1180
|
|
|
1068
1181
|
// Type effectiveness matchup precedence only matters with (NVE, SE)
|
|
1069
1182
|
if (!showdown and (eff1 + eff2) == Effectiveness.mismatch and
|
|
@@ -1083,101 +1196,87 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
|
1083
1196
|
return if (types.type1 == types.type2) eff1 * neutral else eff1 * eff2;
|
|
1084
1197
|
}
|
|
1085
1198
|
|
|
1086
|
-
fn randomizeDamage(battle: anytype) void {
|
|
1199
|
+
fn randomizeDamage(battle: anytype, player: Player, options: anytype) !void {
|
|
1087
1200
|
if (battle.last_damage <= 1) return;
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
while (true) {
|
|
1093
|
-
const r = std.math.rotr(u8, battle.rng.next(), 1);
|
|
1094
|
-
if (r >= 217) break :loop r;
|
|
1095
|
-
}
|
|
1096
|
-
};
|
|
1097
|
-
|
|
1098
|
-
battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
|
|
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);
|
|
1099
1205
|
}
|
|
1100
1206
|
|
|
1101
|
-
fn specialDamage(battle: anytype, player: Player, move: Move.Data,
|
|
1207
|
+
fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
|
|
1102
1208
|
const side = battle.side(player);
|
|
1209
|
+
const foe = battle.foe(player);
|
|
1103
1210
|
|
|
1104
|
-
if (!try checkHit(battle, player, move,
|
|
1211
|
+
if (!try checkHit(battle, player, move, options)) return null;
|
|
1212
|
+
try options.chance.commit(player, .hit);
|
|
1105
1213
|
|
|
1106
1214
|
battle.last_damage = switch (side.last_selected_move) {
|
|
1107
|
-
.SuperFang => @max(
|
|
1215
|
+
.SuperFang => @max(foe.stored().hp / 2, 1),
|
|
1108
1216
|
.SeismicToss, .NightShade => side.stored().level,
|
|
1109
1217
|
.SonicBoom => 20,
|
|
1110
1218
|
.DragonRage => 40,
|
|
1111
1219
|
// GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
|
|
1112
1220
|
.Psywave => power: {
|
|
1113
|
-
const max = @intCast(
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
// GLITCH: Psywave infinite glitch loop
|
|
1118
|
-
if (max <= 1) return Result.Error;
|
|
1119
|
-
while (true) {
|
|
1120
|
-
const r = battle.rng.next();
|
|
1121
|
-
if (r < max) break :power r;
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
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);
|
|
1124
1225
|
},
|
|
1125
1226
|
else => unreachable,
|
|
1126
1227
|
};
|
|
1127
1228
|
|
|
1128
1229
|
if (battle.last_damage == 0) return if (showdown) null else Result.Error;
|
|
1129
1230
|
|
|
1130
|
-
|
|
1131
|
-
|
|
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);
|
|
1132
1234
|
|
|
1133
1235
|
return null;
|
|
1134
1236
|
}
|
|
1135
1237
|
|
|
1136
|
-
fn counterDamage(battle: anytype, player: Player, move: Move.Data,
|
|
1238
|
+
fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
|
|
1137
1239
|
const foe = battle.foe(player);
|
|
1138
1240
|
|
|
1139
1241
|
if (battle.last_damage == 0) {
|
|
1140
|
-
try log.fail(battle.active(player), .None);
|
|
1242
|
+
try options.log.fail(battle.active(player), .None);
|
|
1141
1243
|
return null;
|
|
1142
1244
|
}
|
|
1143
1245
|
|
|
1144
|
-
// Pretend
|
|
1145
|
-
const foe_last_used_move =
|
|
1146
|
-
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
|
|
1147
1247
|
const foe_last_selected_move =
|
|
1148
1248
|
Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
|
|
1149
|
-
.
|
|
1249
|
+
.Splash
|
|
1150
1250
|
else
|
|
1151
1251
|
foe.last_selected_move);
|
|
1152
1252
|
|
|
1153
|
-
const used =
|
|
1154
|
-
foe.last_used_move != .Counter and
|
|
1155
|
-
(foe_last_used_move.type == .Normal or
|
|
1156
|
-
foe_last_used_move.type == .Fighting);
|
|
1157
|
-
|
|
1253
|
+
const used = battle.lastMove(player.foe()).counterable != 0;
|
|
1158
1254
|
const selected = foe_last_selected_move.bp > 0 and
|
|
1159
1255
|
foe.last_selected_move != .Counter and
|
|
1160
1256
|
(foe_last_selected_move.type == .Normal or
|
|
1161
1257
|
foe_last_selected_move.type == .Fighting);
|
|
1162
1258
|
|
|
1163
1259
|
if (!used and !selected) {
|
|
1164
|
-
try log.fail(battle.active(player), .None);
|
|
1260
|
+
try options.log.fail(battle.active(player), .None);
|
|
1165
1261
|
return null;
|
|
1166
1262
|
}
|
|
1167
1263
|
|
|
1168
1264
|
if (!used or !selected) {
|
|
1169
1265
|
// GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
|
|
1170
1266
|
if (!showdown) return Result.Error;
|
|
1171
|
-
try log.fail(battle.active(player), .None);
|
|
1267
|
+
try options.log.fail(battle.active(player), .None);
|
|
1172
1268
|
return null;
|
|
1173
1269
|
}
|
|
1174
1270
|
|
|
1175
|
-
battle.last_damage
|
|
1271
|
+
battle.last_damage *|= 2;
|
|
1176
1272
|
|
|
1177
1273
|
// Pokémon Showdown calls checkHit before Counter
|
|
1178
|
-
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);
|
|
1179
1276
|
|
|
1180
|
-
|
|
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);
|
|
1181
1280
|
return null;
|
|
1182
1281
|
}
|
|
1183
1282
|
|
|
@@ -1186,7 +1285,7 @@ fn applyDamage(
|
|
|
1186
1285
|
target_player: Player,
|
|
1187
1286
|
sub_player: Player,
|
|
1188
1287
|
reason: Damage,
|
|
1189
|
-
|
|
1288
|
+
options: anytype,
|
|
1190
1289
|
) !bool {
|
|
1191
1290
|
assert(showdown or battle.last_damage != 0);
|
|
1192
1291
|
|
|
@@ -1201,19 +1300,20 @@ fn applyDamage(
|
|
|
1201
1300
|
subbed.active.volatiles.substitute = 0;
|
|
1202
1301
|
subbed.active.volatiles.Substitute = false;
|
|
1203
1302
|
// battle.last_damage is not updated with the amount of HP the Substitute had
|
|
1204
|
-
try log.end(battle.active(sub_player), .Substitute);
|
|
1303
|
+
try options.log.end(battle.active(sub_player), .Substitute);
|
|
1304
|
+
options.calc.capped(target_player);
|
|
1205
1305
|
return true;
|
|
1206
1306
|
} else {
|
|
1207
1307
|
// Safe to truncate since less than subbed.volatiles.substitute which is a u8
|
|
1208
|
-
subbed.active.volatiles.substitute -= @intCast(
|
|
1209
|
-
try log.activate(battle.active(sub_player), .Substitute);
|
|
1308
|
+
subbed.active.volatiles.substitute -= @intCast(battle.last_damage);
|
|
1309
|
+
try options.log.activate(battle.active(sub_player), .Substitute);
|
|
1210
1310
|
return false;
|
|
1211
1311
|
}
|
|
1212
1312
|
}
|
|
1213
1313
|
|
|
1214
1314
|
if (battle.last_damage > target.stored().hp) battle.last_damage = target.stored().hp;
|
|
1215
1315
|
target.stored().hp -= battle.last_damage;
|
|
1216
|
-
try log.damage(battle.active(target_player), target.stored(), reason);
|
|
1316
|
+
try options.log.damage(battle.active(target_player), target.stored(), reason);
|
|
1217
1317
|
return false;
|
|
1218
1318
|
}
|
|
1219
1319
|
|
|
@@ -1221,73 +1321,78 @@ fn mirrorMove(
|
|
|
1221
1321
|
battle: anytype,
|
|
1222
1322
|
player: Player,
|
|
1223
1323
|
mslot: u4,
|
|
1324
|
+
rewrap: bool,
|
|
1224
1325
|
auto: bool,
|
|
1225
1326
|
residual: *bool,
|
|
1226
|
-
|
|
1227
|
-
) !?Result {
|
|
1327
|
+
options: anytype,
|
|
1328
|
+
) anyerror!?Result {
|
|
1228
1329
|
var side = battle.side(player);
|
|
1229
1330
|
const foe = battle.foe(player);
|
|
1230
1331
|
|
|
1231
1332
|
side.last_selected_move = foe.last_used_move;
|
|
1232
1333
|
|
|
1233
1334
|
if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
|
|
1234
|
-
try log.fail(battle.active(player), .None);
|
|
1335
|
+
try options.log.fail(battle.active(player), .None);
|
|
1235
1336
|
return null;
|
|
1337
|
+
} else if (!showdown or foe.last_used_move != .Struggle) {
|
|
1338
|
+
incrementPP(side, mslot);
|
|
1236
1339
|
}
|
|
1237
1340
|
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
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);
|
|
1242
1345
|
}
|
|
1243
1346
|
|
|
1244
1347
|
fn metronome(
|
|
1245
1348
|
battle: anytype,
|
|
1246
1349
|
player: Player,
|
|
1247
1350
|
mslot: u4,
|
|
1351
|
+
rewrap: bool,
|
|
1248
1352
|
auto: bool,
|
|
1249
1353
|
residual: *bool,
|
|
1250
|
-
|
|
1251
|
-
) !?Result {
|
|
1354
|
+
options: anytype,
|
|
1355
|
+
) anyerror!?Result {
|
|
1252
1356
|
var side = battle.side(player);
|
|
1253
1357
|
|
|
1254
|
-
side.last_selected_move =
|
|
1255
|
-
const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
|
|
1256
|
-
const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
|
|
1257
|
-
break :blk @intToEnum(Move, r + mod);
|
|
1258
|
-
} else loop: {
|
|
1259
|
-
while (true) {
|
|
1260
|
-
const r = battle.rng.next();
|
|
1261
|
-
if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
|
|
1262
|
-
if (r >= @enumToInt(Move.Struggle)) continue;
|
|
1263
|
-
break :loop @intToEnum(Move, r);
|
|
1264
|
-
}
|
|
1265
|
-
};
|
|
1266
|
-
|
|
1358
|
+
side.last_selected_move = try Rolls.metronome(battle, player, options);
|
|
1267
1359
|
incrementPP(side, mslot);
|
|
1268
1360
|
|
|
1269
|
-
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual,
|
|
1270
|
-
|
|
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);
|
|
1271
1365
|
}
|
|
1272
1366
|
|
|
1273
|
-
fn checkHit(battle: anytype, player: Player, move: Move.Data,
|
|
1367
|
+
fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !bool {
|
|
1274
1368
|
var immune = false;
|
|
1275
1369
|
var mist = false;
|
|
1276
|
-
|
|
1370
|
+
|
|
1371
|
+
if (moveHit(battle, player, move, &immune, &mist, options)) return true;
|
|
1372
|
+
|
|
1277
1373
|
assert(!immune);
|
|
1278
1374
|
if (mist) {
|
|
1279
1375
|
assert(!showdown);
|
|
1280
1376
|
const foe_ident = battle.active(player.foe());
|
|
1281
|
-
try log.activate(foe_ident, .Mist);
|
|
1282
|
-
try log.fail(foe_ident, .None);
|
|
1377
|
+
try options.log.activate(foe_ident, .Mist);
|
|
1378
|
+
try options.log.fail(foe_ident, .None);
|
|
1283
1379
|
} else {
|
|
1284
|
-
try
|
|
1285
|
-
try log.
|
|
1380
|
+
try options.chance.commit(player, .miss);
|
|
1381
|
+
try options.log.lastmiss();
|
|
1382
|
+
try options.log.miss(battle.active(player));
|
|
1286
1383
|
}
|
|
1384
|
+
|
|
1287
1385
|
return false;
|
|
1288
1386
|
}
|
|
1289
1387
|
|
|
1290
|
-
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 {
|
|
1291
1396
|
var side = battle.side(player);
|
|
1292
1397
|
const foe = battle.foe(player);
|
|
1293
1398
|
|
|
@@ -1321,23 +1426,24 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
|
|
|
1321
1426
|
}
|
|
1322
1427
|
|
|
1323
1428
|
// GLITCH: Thrash / Petal Dance / Rage get their accuracy overwritten on subsequent hits
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
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))];
|
|
1329
1436
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1330
|
-
boost = BOOSTS[@
|
|
1437
|
+
boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
|
|
1331
1438
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1332
1439
|
accuracy = @min(255, @max(1, accuracy));
|
|
1333
1440
|
|
|
1334
|
-
|
|
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;
|
|
1335
1444
|
|
|
1336
1445
|
// GLITCH: max accuracy is 255 so 1/256 chance of miss
|
|
1337
|
-
break :miss
|
|
1338
|
-
!battle.rng.chance(u8, @intCast(u8, accuracy), 256)
|
|
1339
|
-
else
|
|
1340
|
-
battle.rng.next() >= accuracy;
|
|
1446
|
+
break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
|
|
1341
1447
|
};
|
|
1342
1448
|
|
|
1343
1449
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1347,7 +1453,7 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
|
|
|
1347
1453
|
}
|
|
1348
1454
|
|
|
1349
1455
|
if (!miss) return true;
|
|
1350
|
-
battle.last_damage = 0;
|
|
1456
|
+
if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
|
|
1351
1457
|
side.active.volatiles.Binding = false;
|
|
1352
1458
|
return false;
|
|
1353
1459
|
}
|
|
@@ -1355,8 +1461,8 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
|
|
|
1355
1461
|
fn checkFaint(
|
|
1356
1462
|
battle: anytype,
|
|
1357
1463
|
player: Player,
|
|
1358
|
-
|
|
1359
|
-
) @TypeOf(log).Error!?Result {
|
|
1464
|
+
options: anytype,
|
|
1465
|
+
) @TypeOf(options.log).Error!?Result {
|
|
1360
1466
|
const side = battle.side(player);
|
|
1361
1467
|
if (side.stored().hp > 0) return null;
|
|
1362
1468
|
|
|
@@ -1368,29 +1474,29 @@ fn checkFaint(
|
|
|
1368
1474
|
const tie = player_out and foe_out;
|
|
1369
1475
|
const more = tie or player_out or foe_out;
|
|
1370
1476
|
|
|
1371
|
-
if (try faint(battle, player,
|
|
1372
|
-
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;
|
|
1373
1479
|
|
|
1374
1480
|
assert(!side.active.volatiles.MultiHit);
|
|
1375
1481
|
assert(!foe.active.volatiles.MultiHit);
|
|
1376
1482
|
|
|
1377
1483
|
if (tie) {
|
|
1378
|
-
try log.tie();
|
|
1484
|
+
try options.log.tie();
|
|
1379
1485
|
return Result.Tie;
|
|
1380
1486
|
} else if (player_out) {
|
|
1381
|
-
try log.win(player.foe());
|
|
1487
|
+
try options.log.win(player.foe());
|
|
1382
1488
|
return if (player == .P1) Result.Lose else Result.Win;
|
|
1383
1489
|
} else if (foe_out) {
|
|
1384
|
-
try log.win(player);
|
|
1490
|
+
try options.log.win(player);
|
|
1385
1491
|
return if (player == .P1) Result.Win else Result.Lose;
|
|
1386
1492
|
}
|
|
1387
1493
|
|
|
1388
1494
|
const foe_choice: Choice.Type = if (foe_fainted) .Switch else .Pass;
|
|
1389
|
-
if (player == .P1) return
|
|
1390
|
-
return
|
|
1495
|
+
if (player == .P1) return .{ .p1 = .Switch, .p2 = foe_choice };
|
|
1496
|
+
return .{ .p1 = foe_choice, .p2 = .Switch };
|
|
1391
1497
|
}
|
|
1392
1498
|
|
|
1393
|
-
fn faint(battle: anytype, player: Player,
|
|
1499
|
+
fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
|
|
1394
1500
|
var side = battle.side(player);
|
|
1395
1501
|
var foe = battle.foe(player);
|
|
1396
1502
|
assert(side.stored().hp == 0);
|
|
@@ -1423,32 +1529,31 @@ fn faint(battle: anytype, player: Player, log: anytype, done: bool) !?Result {
|
|
|
1423
1529
|
side.stored().stats.spe;
|
|
1424
1530
|
}
|
|
1425
1531
|
|
|
1426
|
-
try log.faint(battle.active(player), done);
|
|
1532
|
+
try options.log.faint(battle.active(player), done);
|
|
1533
|
+
options.calc.capped(player);
|
|
1427
1534
|
return null;
|
|
1428
1535
|
}
|
|
1429
1536
|
|
|
1430
|
-
fn handleResidual(battle: anytype, player: Player,
|
|
1537
|
+
fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
|
|
1431
1538
|
var side = battle.side(player);
|
|
1432
1539
|
var stored = side.stored();
|
|
1433
1540
|
const ident = battle.active(player);
|
|
1434
1541
|
var volatiles = &side.active.volatiles;
|
|
1435
1542
|
|
|
1436
1543
|
const brn = Status.is(stored.status, .BRN);
|
|
1437
|
-
if (brn or Status.is(stored.status, .PSN))
|
|
1544
|
+
if (brn or Status.is(stored.status, .PSN)) {
|
|
1438
1545
|
var damage = @max(stored.stats.hp / 16, 1);
|
|
1439
|
-
|
|
1440
1546
|
if (volatiles.Toxic) {
|
|
1441
1547
|
volatiles.toxic += 1;
|
|
1442
1548
|
damage *= volatiles.toxic;
|
|
1443
1549
|
}
|
|
1550
|
+
damage = @min(damage, stored.hp);
|
|
1444
1551
|
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
// Pokémon Showdown uses damageOf here but its not relevant in Generation I
|
|
1451
|
-
try log.damage(ident, stored, if (brn) Damage.Burn else Damage.Poison);
|
|
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
|
+
}
|
|
1452
1557
|
}
|
|
1453
1558
|
|
|
1454
1559
|
if (volatiles.LeechSeed) {
|
|
@@ -1469,49 +1574,46 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
|
|
|
1469
1574
|
damage *= volatiles.toxic;
|
|
1470
1575
|
}
|
|
1471
1576
|
|
|
1472
|
-
// Pokémon Showdown erroneously updates last damage with uncapped Leech Seed damage
|
|
1473
|
-
if (showdown) battle.last_damage = damage;
|
|
1474
|
-
|
|
1475
1577
|
const amount = @min(damage, stored.hp);
|
|
1476
1578
|
stored.hp -= amount;
|
|
1477
1579
|
|
|
1478
1580
|
// As above, Pokémon Showdown uses damageOf but its not relevant
|
|
1479
|
-
if (amount > 0) try log.damage(ident, stored, .LeechSeed);
|
|
1581
|
+
if (amount > 0) try options.log.damage(ident, stored, .LeechSeed);
|
|
1480
1582
|
|
|
1481
1583
|
const before = foe_stored.hp;
|
|
1482
1584
|
// Uncapped damage is added back to the foe
|
|
1483
1585
|
foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
|
|
1484
1586
|
// Pokémon Showdown uses the less specific heal here instead of drain... because reasons?
|
|
1485
|
-
if (foe_stored.hp > before) try log.heal(foe_ident, foe_stored, .Silent);
|
|
1587
|
+
if (foe_stored.hp > before) try options.log.heal(foe_ident, foe_stored, .Silent);
|
|
1486
1588
|
}
|
|
1487
1589
|
}
|
|
1488
1590
|
|
|
1489
|
-
fn endTurn(battle: anytype,
|
|
1591
|
+
fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result {
|
|
1490
1592
|
assert(!battle.side(.P1).active.volatiles.MultiHit);
|
|
1491
1593
|
assert(!battle.side(.P2).active.volatiles.MultiHit);
|
|
1492
1594
|
|
|
1493
|
-
|
|
1494
|
-
|
|
1595
|
+
battle.turn += 1;
|
|
1596
|
+
|
|
1597
|
+
if (showdown and pkmn.options.ebc and checkEBC(battle)) {
|
|
1598
|
+
try options.log.tie();
|
|
1495
1599
|
return Result.Tie;
|
|
1496
1600
|
}
|
|
1497
1601
|
|
|
1498
|
-
battle.turn += 1;
|
|
1499
|
-
|
|
1500
1602
|
if (showdown and battle.turn >= 1000) {
|
|
1501
|
-
try log.tie();
|
|
1603
|
+
try options.log.tie();
|
|
1502
1604
|
return Result.Tie;
|
|
1503
1605
|
} else if (battle.turn >= 65535) {
|
|
1504
1606
|
return Result.Error;
|
|
1505
1607
|
}
|
|
1506
1608
|
|
|
1507
|
-
try log.turn(battle.turn);
|
|
1609
|
+
try options.log.turn(battle.turn);
|
|
1508
1610
|
|
|
1509
1611
|
return Result.Default;
|
|
1510
1612
|
}
|
|
1511
1613
|
|
|
1512
1614
|
fn checkEBC(battle: anytype) bool {
|
|
1513
1615
|
for (battle.sides, 0..) |side, i| {
|
|
1514
|
-
const foe = battle.sides[~@
|
|
1616
|
+
const foe = battle.sides[~@as(u1, @intCast(i))];
|
|
1515
1617
|
|
|
1516
1618
|
var foe_all_ghosts = true;
|
|
1517
1619
|
var foe_all_transform = true;
|
|
@@ -1563,48 +1665,84 @@ fn checkEBC(battle: anytype) bool {
|
|
|
1563
1665
|
return false;
|
|
1564
1666
|
}
|
|
1565
1667
|
|
|
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
|
+
|
|
1566
1704
|
inline fn onBegin(
|
|
1567
1705
|
battle: anytype,
|
|
1568
1706
|
player: Player,
|
|
1569
1707
|
move: Move.Data,
|
|
1570
1708
|
mslot: u8,
|
|
1571
1709
|
residual: *bool,
|
|
1572
|
-
|
|
1710
|
+
options: anytype,
|
|
1573
1711
|
) !void {
|
|
1574
1712
|
assert(move.effect.onBegin());
|
|
1575
1713
|
return switch (move.effect) {
|
|
1576
|
-
.Confusion => Effects.confusion(battle, player, move,
|
|
1577
|
-
.Conversion => Effects.conversion(battle, player,
|
|
1578
|
-
.FocusEnergy => Effects.focusEnergy(battle, player,
|
|
1579
|
-
.Haze => Effects.haze(battle, player,
|
|
1580
|
-
.Heal => Effects.heal(battle, player,
|
|
1581
|
-
.LeechSeed => Effects.leechSeed(battle, player, move,
|
|
1582
|
-
.LightScreen => Effects.lightScreen(battle, player,
|
|
1583
|
-
.Mimic => Effects.mimic(battle, player, move, mslot,
|
|
1584
|
-
.Mist => Effects.mist(battle, player,
|
|
1585
|
-
.Paralyze => Effects.paralyze(battle, player, move,
|
|
1586
|
-
.Poison => Effects.poison(battle, player, move,
|
|
1587
|
-
.Reflect => Effects.reflect(battle, player,
|
|
1588
|
-
.Splash => Effects.splash(battle, player,
|
|
1589
|
-
.Substitute => Effects.substitute(battle, player, residual,
|
|
1590
|
-
.SwitchAndTeleport => Effects.switchAndTeleport(battle, player, move,
|
|
1591
|
-
.Transform => Effects.transform(battle, player,
|
|
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),
|
|
1592
1730
|
else => unreachable,
|
|
1593
1731
|
};
|
|
1594
1732
|
}
|
|
1595
1733
|
|
|
1596
|
-
inline fn onEnd(battle: anytype, player: Player, move: Move.Data,
|
|
1734
|
+
inline fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1597
1735
|
assert(move.effect.onEnd());
|
|
1598
1736
|
return switch (move.effect) {
|
|
1599
|
-
.Bide => Effects.bide(battle, player,
|
|
1737
|
+
.Bide => Effects.bide(battle, player, options),
|
|
1600
1738
|
// zig fmt: off
|
|
1601
1739
|
.AttackUp1, .AttackUp2, .DefenseUp1, .DefenseUp2,
|
|
1602
1740
|
.EvasionUp1, .SpecialUp1, .SpecialUp2, .SpeedUp2 =>
|
|
1603
|
-
Effects.boost(battle, player, move,
|
|
1741
|
+
Effects.boost(battle, player, move, options),
|
|
1604
1742
|
.AccuracyDown1, .AttackDown1, .DefenseDown1, .DefenseDown2, .SpeedDown1 =>
|
|
1605
|
-
Effects.unboost(battle, player, move,
|
|
1743
|
+
Effects.unboost(battle, player, move, options),
|
|
1606
1744
|
// zig fmt: on
|
|
1607
|
-
.Sleep => Effects.sleep(battle, player, move,
|
|
1745
|
+
.Sleep => Effects.sleep(battle, player, move, options),
|
|
1608
1746
|
else => unreachable,
|
|
1609
1747
|
};
|
|
1610
1748
|
}
|
|
@@ -1614,54 +1752,51 @@ inline fn alwaysHappens(
|
|
|
1614
1752
|
player: Player,
|
|
1615
1753
|
move: Move.Data,
|
|
1616
1754
|
residual: *bool,
|
|
1617
|
-
|
|
1755
|
+
options: anytype,
|
|
1618
1756
|
) !void {
|
|
1619
1757
|
assert(move.effect.alwaysHappens());
|
|
1620
1758
|
return switch (move.effect) {
|
|
1621
|
-
.DrainHP, .DreamEater => Effects.drainHP(battle, player,
|
|
1759
|
+
.DrainHP, .DreamEater => Effects.drainHP(battle, player, options),
|
|
1622
1760
|
.Explode => Effects.explode(battle, player),
|
|
1623
|
-
.PayDay => Effects.payDay(battle, player,
|
|
1761
|
+
.PayDay => Effects.payDay(battle, player, options),
|
|
1624
1762
|
.Rage => Effects.rage(battle, player),
|
|
1625
|
-
.Recoil => Effects.recoil(battle, player, residual,
|
|
1763
|
+
.Recoil => Effects.recoil(battle, player, residual, options),
|
|
1626
1764
|
.JumpKick, .Binding => {},
|
|
1627
1765
|
else => unreachable,
|
|
1628
1766
|
};
|
|
1629
1767
|
}
|
|
1630
1768
|
|
|
1631
|
-
inline fn moveEffect(battle: anytype, player: Player, move: Move.Data,
|
|
1769
|
+
inline fn moveEffect(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1632
1770
|
return switch (move.effect) {
|
|
1633
|
-
.BurnChance1, .BurnChance2 => Effects.burnChance(battle, player, move,
|
|
1634
|
-
.ConfusionChance => Effects.confusion(battle, player, move,
|
|
1635
|
-
.FlinchChance1, .FlinchChance2 => Effects.flinchChance(battle, player, move),
|
|
1636
|
-
.FreezeChance => Effects.freezeChance(battle, player, move,
|
|
1637
|
-
.HyperBeam => Effects.hyperBeam(battle, player,
|
|
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),
|
|
1638
1776
|
.MultiHit, .DoubleHit, .Twineedle => unreachable,
|
|
1639
|
-
.ParalyzeChance1, .ParalyzeChance2 => Effects.paralyzeChance(battle, player, move,
|
|
1640
|
-
.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),
|
|
1641
1779
|
// zig fmt: off
|
|
1642
1780
|
.AttackDownChance, .DefenseDownChance, .SpecialDownChance, .SpeedDownChance =>
|
|
1643
|
-
Effects.unboost(battle, player, move,
|
|
1781
|
+
Effects.unboost(battle, player, move, options),
|
|
1644
1782
|
// zig fmt: on
|
|
1645
1783
|
else => {},
|
|
1646
1784
|
};
|
|
1647
1785
|
}
|
|
1648
1786
|
|
|
1649
1787
|
pub const Effects = struct {
|
|
1650
|
-
fn bide(battle: anytype, player: Player,
|
|
1788
|
+
fn bide(battle: anytype, player: Player, options: anytype) !void {
|
|
1651
1789
|
var side = battle.side(player);
|
|
1652
1790
|
|
|
1653
1791
|
side.active.volatiles.Bide = true;
|
|
1654
1792
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1655
1793
|
side.active.volatiles.state = 0;
|
|
1656
|
-
side.active.volatiles.attacks =
|
|
1657
|
-
battle.rng.range(u4, 2, 4)
|
|
1658
|
-
else
|
|
1659
|
-
(battle.rng.next() & 1) + 2);
|
|
1794
|
+
side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
1660
1795
|
|
|
1661
|
-
try log.start(battle.active(player), .Bide);
|
|
1796
|
+
try options.log.start(battle.active(player), .Bide);
|
|
1662
1797
|
}
|
|
1663
1798
|
|
|
1664
|
-
fn burnChance(battle: anytype, player: Player, move: Move.Data,
|
|
1799
|
+
fn burnChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1665
1800
|
var foe = battle.foe(player);
|
|
1666
1801
|
var foe_stored = foe.stored();
|
|
1667
1802
|
|
|
@@ -1672,96 +1807,97 @@ pub const Effects = struct {
|
|
|
1672
1807
|
// GLITCH: Freeze top move selection desync can occur if thawed Pokémon is slower
|
|
1673
1808
|
if (Status.is(foe_stored.status, .FRZ)) {
|
|
1674
1809
|
assert(move.type == .Fire);
|
|
1675
|
-
try log.curestatus(
|
|
1810
|
+
try options.log.curestatus(
|
|
1811
|
+
battle.active(player.foe()),
|
|
1812
|
+
foe_stored.status,
|
|
1813
|
+
.Message,
|
|
1814
|
+
);
|
|
1676
1815
|
foe_stored.status = 0;
|
|
1677
1816
|
}
|
|
1678
1817
|
return;
|
|
1679
1818
|
}
|
|
1680
1819
|
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
Gen12.percent(10)
|
|
1686
|
-
else
|
|
1687
|
-
Gen12.percent(30));
|
|
1688
|
-
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
|
+
}
|
|
1689
1824
|
|
|
1690
1825
|
foe_stored.status = Status.init(.BRN);
|
|
1691
1826
|
foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
|
|
1692
1827
|
|
|
1693
|
-
try log.status(battle.active(player.foe()), foe_stored.status, .None);
|
|
1828
|
+
try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
|
|
1694
1829
|
}
|
|
1695
1830
|
|
|
1696
|
-
fn charge(battle: anytype, player: Player,
|
|
1831
|
+
fn charge(battle: anytype, player: Player, options: anytype) !void {
|
|
1697
1832
|
var side = battle.side(player);
|
|
1698
1833
|
var volatiles = &side.active.volatiles;
|
|
1699
1834
|
|
|
1700
1835
|
volatiles.Charging = true;
|
|
1701
1836
|
const move = side.last_selected_move;
|
|
1702
1837
|
if (move == .Fly or move == .Dig) volatiles.Invulnerable = true;
|
|
1703
|
-
try log.laststill();
|
|
1704
|
-
try log.prepare(battle.active(player), move);
|
|
1838
|
+
try options.log.laststill();
|
|
1839
|
+
try options.log.prepare(battle.active(player), move);
|
|
1705
1840
|
}
|
|
1706
1841
|
|
|
1707
|
-
fn confusion(battle: anytype, player: Player, move: Move.Data,
|
|
1842
|
+
fn confusion(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1708
1843
|
var foe = battle.foe(player);
|
|
1709
1844
|
const sub = foe.active.volatiles.Substitute;
|
|
1710
1845
|
|
|
1711
1846
|
if (move.effect == .ConfusionChance) {
|
|
1712
|
-
|
|
1713
|
-
battle.rng.chance(u8, 25, 256)
|
|
1714
|
-
else
|
|
1715
|
-
battle.rng.next() < Gen12.percent(10);
|
|
1716
|
-
if (!chance) return;
|
|
1847
|
+
if (!try Rolls.confusionChance(battle, player, options)) return;
|
|
1717
1848
|
} else {
|
|
1718
1849
|
if (showdown) {
|
|
1719
|
-
if (!try checkHit(battle, player, move,
|
|
1850
|
+
if (!try checkHit(battle, player, move, options)) {
|
|
1720
1851
|
return;
|
|
1721
1852
|
} else if (sub) {
|
|
1722
|
-
return log.fail(battle.active(player.foe()), .None);
|
|
1853
|
+
return options.log.fail(battle.active(player.foe()), .None);
|
|
1723
1854
|
}
|
|
1724
1855
|
} else {
|
|
1725
1856
|
if (sub) {
|
|
1726
|
-
return log.fail(battle.active(player.foe()), .None);
|
|
1727
|
-
} 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)) {
|
|
1728
1859
|
return;
|
|
1729
1860
|
}
|
|
1730
1861
|
}
|
|
1862
|
+
try options.chance.commit(player, .hit);
|
|
1731
1863
|
}
|
|
1732
1864
|
|
|
1733
1865
|
if (foe.active.volatiles.Confusion) return;
|
|
1734
1866
|
foe.active.volatiles.Confusion = true;
|
|
1735
|
-
foe.active.volatiles.confusion =
|
|
1736
|
-
battle.rng.range(u8, 2, 6)
|
|
1737
|
-
else
|
|
1738
|
-
(battle.rng.next() & 3) + 2);
|
|
1867
|
+
foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
|
|
1739
1868
|
|
|
1740
|
-
try log.start(battle.active(player.foe()), .Confusion);
|
|
1869
|
+
try options.log.start(battle.active(player.foe()), .Confusion);
|
|
1741
1870
|
}
|
|
1742
1871
|
|
|
1743
|
-
fn conversion(battle: anytype, player: Player,
|
|
1872
|
+
fn conversion(battle: anytype, player: Player, options: anytype) !void {
|
|
1744
1873
|
const foe = battle.foe(player);
|
|
1745
1874
|
|
|
1746
1875
|
if (foe.active.volatiles.Invulnerable) {
|
|
1747
|
-
try log.lastmiss();
|
|
1748
|
-
return log.miss(battle.active(player));
|
|
1876
|
+
try options.log.lastmiss();
|
|
1877
|
+
return options.log.miss(battle.active(player));
|
|
1749
1878
|
}
|
|
1750
1879
|
|
|
1751
1880
|
battle.side(player).active.types = foe.active.types;
|
|
1752
|
-
return log.typechange(
|
|
1881
|
+
return options.log.typechange(
|
|
1882
|
+
battle.active(player),
|
|
1883
|
+
foe.active.types,
|
|
1884
|
+
battle.active(player.foe()),
|
|
1885
|
+
);
|
|
1753
1886
|
}
|
|
1754
1887
|
|
|
1755
|
-
fn disable(battle: anytype, player: Player, move: Move.Data,
|
|
1888
|
+
fn disable(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
|
|
1756
1889
|
var foe = battle.foe(player);
|
|
1757
1890
|
var volatiles = &foe.active.volatiles;
|
|
1758
1891
|
const foe_ident = battle.active(player.foe());
|
|
1759
1892
|
|
|
1760
1893
|
// Pokémon Showdown handles hit/miss earlier in doMove
|
|
1761
|
-
if (!showdown
|
|
1894
|
+
if (!showdown) {
|
|
1895
|
+
if (!try checkHit(battle, player, move, options)) return null;
|
|
1896
|
+
try options.chance.commit(player, .hit);
|
|
1897
|
+
}
|
|
1762
1898
|
|
|
1763
1899
|
if (volatiles.disabled_move != 0) {
|
|
1764
|
-
try log.fail(foe_ident, .None);
|
|
1900
|
+
try options.log.fail(foe_ident, .None);
|
|
1765
1901
|
return null;
|
|
1766
1902
|
}
|
|
1767
1903
|
|
|
@@ -1777,7 +1913,7 @@ pub const Effects = struct {
|
|
|
1777
1913
|
// Technically this is still considered simply a "miss" on the cartridge,
|
|
1778
1914
|
// but diverging from Pokémon Showdown here would mostly just be pedantic
|
|
1779
1915
|
if (n == 0) {
|
|
1780
|
-
try log.fail(foe_ident, .None);
|
|
1916
|
+
try options.log.fail(foe_ident, .None);
|
|
1781
1917
|
return null;
|
|
1782
1918
|
} else if (err) {
|
|
1783
1919
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
@@ -1785,23 +1921,22 @@ pub const Effects = struct {
|
|
|
1785
1921
|
return Result.Error;
|
|
1786
1922
|
}
|
|
1787
1923
|
|
|
1788
|
-
volatiles.disabled_move =
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
else
|
|
1792
|
-
(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);
|
|
1793
1927
|
|
|
1794
|
-
|
|
1928
|
+
const id = foe.active.move(volatiles.disabled_move).id;
|
|
1929
|
+
try options.log.startEffect(foe_ident, .Disable, id);
|
|
1795
1930
|
return null;
|
|
1796
1931
|
}
|
|
1797
1932
|
|
|
1798
|
-
fn drainHP(battle: anytype, player: Player,
|
|
1933
|
+
fn drainHP(battle: anytype, player: Player, options: anytype) !void {
|
|
1799
1934
|
var side = battle.side(player);
|
|
1800
1935
|
var stored = side.stored();
|
|
1801
1936
|
|
|
1802
1937
|
if (battle.last_damage == 0) {
|
|
1803
1938
|
assert(showdown);
|
|
1804
|
-
return;
|
|
1939
|
+
if (!battle.foe(player).active.volatiles.Substitute) return;
|
|
1805
1940
|
}
|
|
1806
1941
|
|
|
1807
1942
|
const drain = @max(battle.last_damage / 2, 1);
|
|
@@ -1810,7 +1945,7 @@ pub const Effects = struct {
|
|
|
1810
1945
|
if (stored.hp == stored.stats.hp) return;
|
|
1811
1946
|
stored.hp = @min(stored.stats.hp, stored.hp + drain);
|
|
1812
1947
|
|
|
1813
|
-
try log.drain(battle.active(player), stored, battle.active(player.foe()));
|
|
1948
|
+
try options.log.drain(battle.active(player), stored, battle.active(player.foe()));
|
|
1814
1949
|
}
|
|
1815
1950
|
|
|
1816
1951
|
fn explode(battle: anytype, player: Player) !void {
|
|
@@ -1824,34 +1959,28 @@ pub const Effects = struct {
|
|
|
1824
1959
|
side.active.volatiles.LeechSeed = false;
|
|
1825
1960
|
}
|
|
1826
1961
|
|
|
1827
|
-
fn flinchChance(battle: anytype, player: Player, move: Move.Data) void {
|
|
1962
|
+
fn flinchChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1828
1963
|
var volatiles = &battle.foe(player).active.volatiles;
|
|
1829
1964
|
|
|
1830
1965
|
if (volatiles.Substitute) return;
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
else
|
|
1835
|
-
battle.rng.next() < 1 + (if (move.effect == .FlinchChance1)
|
|
1836
|
-
Gen12.percent(10)
|
|
1837
|
-
else
|
|
1838
|
-
Gen12.percent(30));
|
|
1839
|
-
if (!chance) return;
|
|
1966
|
+
if (!try Rolls.secondaryChance(battle, player, move.effect == .FlinchChance1, options)) {
|
|
1967
|
+
return;
|
|
1968
|
+
}
|
|
1840
1969
|
|
|
1841
1970
|
volatiles.Flinch = true;
|
|
1842
1971
|
volatiles.Recharging = false;
|
|
1843
1972
|
}
|
|
1844
1973
|
|
|
1845
|
-
fn focusEnergy(battle: anytype, player: Player,
|
|
1974
|
+
fn focusEnergy(battle: anytype, player: Player, options: anytype) !void {
|
|
1846
1975
|
var side = battle.side(player);
|
|
1847
1976
|
|
|
1848
1977
|
if (side.active.volatiles.FocusEnergy) return;
|
|
1849
1978
|
side.active.volatiles.FocusEnergy = true;
|
|
1850
1979
|
|
|
1851
|
-
try log.start(battle.active(player), .FocusEnergy);
|
|
1980
|
+
try options.log.start(battle.active(player), .FocusEnergy);
|
|
1852
1981
|
}
|
|
1853
1982
|
|
|
1854
|
-
fn freezeChance(battle: anytype, player: Player, move: Move.Data,
|
|
1983
|
+
fn freezeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1855
1984
|
var foe = battle.foe(player);
|
|
1856
1985
|
var foe_stored = foe.stored();
|
|
1857
1986
|
const foe_ident = battle.active(player.foe());
|
|
@@ -1862,22 +1991,19 @@ pub const Effects = struct {
|
|
|
1862
1991
|
return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
|
|
1863
1992
|
}
|
|
1864
1993
|
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
else
|
|
1868
|
-
battle.rng.next() < 1 + Gen12.percent(10);
|
|
1869
|
-
if (!chance) return;
|
|
1870
|
-
|
|
1994
|
+
if (foe.active.types.includes(move.type)) return;
|
|
1995
|
+
if (!try Rolls.secondaryChance(battle, player, true, options)) return;
|
|
1871
1996
|
// Freeze Clause Mod
|
|
1872
1997
|
if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
1873
1998
|
|
|
1874
1999
|
foe_stored.status = Status.init(.FRZ);
|
|
1875
2000
|
// GLITCH: Hyper Beam recharging status is not cleared
|
|
1876
2001
|
|
|
1877
|
-
try log.status(foe_ident, foe_stored.status, .None);
|
|
2002
|
+
try options.log.status(foe_ident, foe_stored.status, .None);
|
|
1878
2003
|
}
|
|
1879
2004
|
|
|
1880
|
-
fn haze(battle: anytype, player: Player,
|
|
2005
|
+
fn haze(battle: anytype, player: Player, options: anytype) !void {
|
|
2006
|
+
var log = options.log;
|
|
1881
2007
|
var side = battle.side(player);
|
|
1882
2008
|
var foe = battle.foe(player);
|
|
1883
2009
|
|
|
@@ -1899,7 +2025,7 @@ pub const Effects = struct {
|
|
|
1899
2025
|
// Pokémon Showdown clears P1 then P2 instead of status -> side -> foe
|
|
1900
2026
|
if (showdown) {
|
|
1901
2027
|
for (&battle.sides, 0..) |*s, i| {
|
|
1902
|
-
const p = @
|
|
2028
|
+
const p: Player = @enumFromInt(i);
|
|
1903
2029
|
// Pokémon Showdown incorrectly does not prevent sleep/freeze from moving
|
|
1904
2030
|
if (p != player and Status.any(s.stored().status)) {
|
|
1905
2031
|
try log.curestatus(foe_ident, foe_stored.status, .Silent);
|
|
@@ -1908,7 +2034,7 @@ pub const Effects = struct {
|
|
|
1908
2034
|
s.stored().status = Status.init(.PSN);
|
|
1909
2035
|
try log.status(battle.active(p), s.stored().status, .None);
|
|
1910
2036
|
}
|
|
1911
|
-
try clearVolatiles(battle, p,
|
|
2037
|
+
try clearVolatiles(battle, p, p != player, options);
|
|
1912
2038
|
}
|
|
1913
2039
|
} else {
|
|
1914
2040
|
if (Status.any(foe_stored.status)) {
|
|
@@ -1918,71 +2044,76 @@ pub const Effects = struct {
|
|
|
1918
2044
|
try log.curestatus(foe_ident, foe_stored.status, .Silent);
|
|
1919
2045
|
foe_stored.status = 0;
|
|
1920
2046
|
}
|
|
1921
|
-
try clearVolatiles(battle, player,
|
|
1922
|
-
try clearVolatiles(battle, player.foe(),
|
|
2047
|
+
try clearVolatiles(battle, player, false, options);
|
|
2048
|
+
try clearVolatiles(battle, player.foe(), true, options);
|
|
1923
2049
|
}
|
|
1924
2050
|
}
|
|
1925
2051
|
|
|
1926
|
-
fn heal(battle: anytype, player: Player,
|
|
2052
|
+
fn heal(battle: anytype, player: Player, options: anytype) !void {
|
|
1927
2053
|
var side = battle.side(player);
|
|
1928
2054
|
var stored = side.stored();
|
|
1929
2055
|
const ident = battle.active(player);
|
|
1930
2056
|
|
|
1931
2057
|
// GLITCH: HP recovery move failure glitches
|
|
1932
2058
|
const delta = stored.stats.hp - stored.hp;
|
|
1933
|
-
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
|
+
}
|
|
1934
2062
|
|
|
1935
2063
|
const rest = side.last_selected_move == .Rest;
|
|
1936
2064
|
if (rest) {
|
|
1937
2065
|
// Adding the sleep status runs the sleep condition handler to roll duration
|
|
1938
2066
|
if (showdown) battle.rng.advance(1);
|
|
1939
2067
|
stored.status = Status.slf(2);
|
|
1940
|
-
try log.statusFrom(ident, stored.status, Move.Rest);
|
|
2068
|
+
try options.log.statusFrom(ident, stored.status, Move.Rest);
|
|
1941
2069
|
stored.hp = stored.stats.hp;
|
|
1942
2070
|
} else {
|
|
1943
2071
|
stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
|
|
1944
2072
|
}
|
|
1945
|
-
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);
|
|
1946
2074
|
}
|
|
1947
2075
|
|
|
1948
|
-
fn hyperBeam(battle: anytype, player: Player,
|
|
2076
|
+
fn hyperBeam(battle: anytype, player: Player, options: anytype) !void {
|
|
1949
2077
|
battle.side(player).active.volatiles.Recharging = true;
|
|
1950
|
-
try log.mustrecharge(battle.active(player));
|
|
2078
|
+
try options.log.mustrecharge(battle.active(player));
|
|
1951
2079
|
}
|
|
1952
2080
|
|
|
1953
|
-
fn leechSeed(battle: anytype, player: Player, move: Move.Data,
|
|
2081
|
+
fn leechSeed(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1954
2082
|
var foe = battle.foe(player);
|
|
1955
2083
|
|
|
1956
2084
|
if (showdown) {
|
|
1957
2085
|
// Invulnerability trumps type immunity on Pokémon Showdown
|
|
1958
2086
|
if (!foe.active.volatiles.Invulnerable and foe.active.types.includes(.Grass)) {
|
|
1959
|
-
return log.immune(battle.active(player.foe()), .None);
|
|
2087
|
+
return options.log.immune(battle.active(player.foe()), .None);
|
|
1960
2088
|
}
|
|
1961
|
-
if (!try checkHit(battle, player, move,
|
|
2089
|
+
if (!try checkHit(battle, player, move, options)) return;
|
|
1962
2090
|
if (foe.active.volatiles.LeechSeed) return;
|
|
1963
2091
|
} else {
|
|
1964
|
-
if (!try checkHit(battle, player, move,
|
|
2092
|
+
if (!try checkHit(battle, player, move, options)) return;
|
|
1965
2093
|
if (foe.active.types.includes(.Grass) or foe.active.volatiles.LeechSeed) {
|
|
1966
|
-
try log.lastmiss();
|
|
1967
|
-
return log.miss(battle.active(player));
|
|
2094
|
+
try options.log.lastmiss();
|
|
2095
|
+
return options.log.miss(battle.active(player));
|
|
1968
2096
|
}
|
|
1969
2097
|
}
|
|
1970
2098
|
|
|
2099
|
+
try options.chance.commit(player, .hit);
|
|
1971
2100
|
foe.active.volatiles.LeechSeed = true;
|
|
1972
2101
|
|
|
1973
|
-
try log.start(battle.active(player.foe()), .LeechSeed);
|
|
2102
|
+
try options.log.start(battle.active(player.foe()), .LeechSeed);
|
|
1974
2103
|
}
|
|
1975
2104
|
|
|
1976
|
-
fn lightScreen(battle: anytype, player: Player,
|
|
2105
|
+
fn lightScreen(battle: anytype, player: Player, options: anytype) !void {
|
|
1977
2106
|
var side = battle.side(player);
|
|
1978
2107
|
|
|
1979
|
-
if (side.active.volatiles.LightScreen)
|
|
2108
|
+
if (side.active.volatiles.LightScreen) {
|
|
2109
|
+
return options.log.fail(battle.active(player), .None);
|
|
2110
|
+
}
|
|
1980
2111
|
side.active.volatiles.LightScreen = true;
|
|
1981
2112
|
|
|
1982
|
-
try log.start(battle.active(player), .LightScreen);
|
|
2113
|
+
try options.log.start(battle.active(player), .LightScreen);
|
|
1983
2114
|
}
|
|
1984
2115
|
|
|
1985
|
-
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 {
|
|
1986
2117
|
var side = battle.side(player);
|
|
1987
2118
|
var foe = battle.foe(player);
|
|
1988
2119
|
|
|
@@ -1998,45 +2129,51 @@ pub const Effects = struct {
|
|
|
1998
2129
|
const has_mimic = has_mimic: {
|
|
1999
2130
|
for (side.active.moves, 0..) |m, i| {
|
|
2000
2131
|
if (m.id == .Mimic) {
|
|
2001
|
-
oslot = @intCast(
|
|
2132
|
+
oslot = @intCast(i + 1);
|
|
2002
2133
|
break :has_mimic true;
|
|
2003
2134
|
}
|
|
2004
2135
|
}
|
|
2005
2136
|
break :has_mimic false;
|
|
2006
2137
|
};
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
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);
|
|
2011
2143
|
}
|
|
2012
2144
|
}
|
|
2013
|
-
if (!try checkHit(battle, player, move,
|
|
2145
|
+
if (!try checkHit(battle, player, move, options)) return;
|
|
2146
|
+
try options.chance.commit(player, .hit);
|
|
2014
2147
|
|
|
2015
|
-
const rslot =
|
|
2148
|
+
const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
|
|
2016
2149
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
2017
2150
|
|
|
2018
|
-
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);
|
|
2019
2152
|
}
|
|
2020
2153
|
|
|
2021
|
-
fn mist(battle: anytype, player: Player,
|
|
2154
|
+
fn mist(battle: anytype, player: Player, options: anytype) !void {
|
|
2022
2155
|
var side = battle.side(player);
|
|
2023
2156
|
|
|
2024
2157
|
if (side.active.volatiles.Mist) return;
|
|
2025
2158
|
side.active.volatiles.Mist = true;
|
|
2026
2159
|
|
|
2027
|
-
try log.start(battle.active(player), .Mist);
|
|
2160
|
+
try options.log.start(battle.active(player), .Mist);
|
|
2028
2161
|
}
|
|
2029
2162
|
|
|
2030
|
-
fn multiHit(battle: anytype, player: Player, move: Move.Data) void {
|
|
2163
|
+
fn multiHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
2031
2164
|
var side = battle.side(player);
|
|
2032
2165
|
|
|
2033
2166
|
assert(!side.active.volatiles.MultiHit);
|
|
2034
2167
|
side.active.volatiles.MultiHit = true;
|
|
2035
2168
|
|
|
2036
|
-
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;
|
|
2037
2173
|
}
|
|
2038
2174
|
|
|
2039
|
-
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;
|
|
2040
2177
|
var foe = battle.foe(player);
|
|
2041
2178
|
var foe_stored = foe.stored();
|
|
2042
2179
|
const foe_ident = battle.active(player.foe());
|
|
@@ -2047,7 +2184,7 @@ pub const Effects = struct {
|
|
|
2047
2184
|
if (showdown) {
|
|
2048
2185
|
// Invulnerability trumps type immunity on Pokémon Showdown
|
|
2049
2186
|
if (immune and !foe.active.volatiles.Invulnerable) return log.immune(foe_ident, .None);
|
|
2050
|
-
if (!try checkHit(battle, player, move,
|
|
2187
|
+
if (!try checkHit(battle, player, move, options)) return;
|
|
2051
2188
|
}
|
|
2052
2189
|
if (Status.any(foe_stored.status)) {
|
|
2053
2190
|
return log.fail(
|
|
@@ -2057,8 +2194,9 @@ pub const Effects = struct {
|
|
|
2057
2194
|
}
|
|
2058
2195
|
if (!showdown) {
|
|
2059
2196
|
if (immune) return log.immune(foe_ident, .None);
|
|
2060
|
-
if (!try checkHit(battle, player, move,
|
|
2197
|
+
if (!try checkHit(battle, player, move, options)) return;
|
|
2061
2198
|
}
|
|
2199
|
+
try options.chance.commit(player, .hit);
|
|
2062
2200
|
|
|
2063
2201
|
foe_stored.status = Status.init(.PAR);
|
|
2064
2202
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
@@ -2066,7 +2204,7 @@ pub const Effects = struct {
|
|
|
2066
2204
|
try log.status(foe_ident, foe_stored.status, .None);
|
|
2067
2205
|
}
|
|
2068
2206
|
|
|
2069
|
-
fn paralyzeChance(battle: anytype, player: Player, move: Move.Data,
|
|
2207
|
+
fn paralyzeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
2070
2208
|
var foe = battle.foe(player);
|
|
2071
2209
|
var foe_stored = foe.stored();
|
|
2072
2210
|
|
|
@@ -2077,32 +2215,31 @@ pub const Effects = struct {
|
|
|
2077
2215
|
}
|
|
2078
2216
|
|
|
2079
2217
|
// Body Slam can't paralyze a Normal type Pokémon
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
Gen12.percent(10)
|
|
2085
|
-
else
|
|
2086
|
-
Gen12.percent(30));
|
|
2087
|
-
if (!chance) return;
|
|
2218
|
+
if (foe.active.types.includes(move.type)) return;
|
|
2219
|
+
if (!try Rolls.secondaryChance(battle, player, move.effect == .ParalyzeChance1, options)) {
|
|
2220
|
+
return;
|
|
2221
|
+
}
|
|
2088
2222
|
|
|
2089
2223
|
foe_stored.status = Status.init(.PAR);
|
|
2090
2224
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
2091
2225
|
|
|
2092
|
-
try log.status(battle.active(player.foe()), foe_stored.status, .None);
|
|
2226
|
+
try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
|
|
2093
2227
|
}
|
|
2094
2228
|
|
|
2095
|
-
fn payDay(battle: anytype, player: Player,
|
|
2096
|
-
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
|
+
}
|
|
2097
2233
|
}
|
|
2098
2234
|
|
|
2099
|
-
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;
|
|
2100
2237
|
var foe = battle.foe(player);
|
|
2101
2238
|
var foe_stored = foe.stored();
|
|
2102
2239
|
const foe_ident = battle.active(player.foe());
|
|
2103
2240
|
const toxic = battle.side(player).last_selected_move == .Toxic;
|
|
2104
2241
|
|
|
2105
|
-
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)) {
|
|
2106
2243
|
return;
|
|
2107
2244
|
} else if (foe.active.volatiles.Substitute) {
|
|
2108
2245
|
if (move.effect != .Poison) return;
|
|
@@ -2126,16 +2263,12 @@ pub const Effects = struct {
|
|
|
2126
2263
|
}
|
|
2127
2264
|
|
|
2128
2265
|
if (move.effect == .Poison) {
|
|
2129
|
-
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);
|
|
2130
2268
|
} else {
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
battle.rng.next() < 1 + (if (move.effect == .PoisonChance1)
|
|
2135
|
-
Gen12.percent(20)
|
|
2136
|
-
else
|
|
2137
|
-
Gen12.percent(40));
|
|
2138
|
-
if (!chance) return;
|
|
2269
|
+
if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
|
|
2270
|
+
return;
|
|
2271
|
+
}
|
|
2139
2272
|
}
|
|
2140
2273
|
|
|
2141
2274
|
foe_stored.status = Status.init(.PSN);
|
|
@@ -2150,34 +2283,40 @@ pub const Effects = struct {
|
|
|
2150
2283
|
|
|
2151
2284
|
fn rage(battle: anytype, player: Player) !void {
|
|
2152
2285
|
var volatiles = &battle.side(player).active.volatiles;
|
|
2153
|
-
if (volatiles.Rage) return;
|
|
2154
|
-
volatiles.Rage = true;
|
|
2155
2286
|
assert(!volatiles.Bide);
|
|
2156
|
-
volatiles.
|
|
2287
|
+
volatiles.Rage = true;
|
|
2157
2288
|
}
|
|
2158
2289
|
|
|
2159
|
-
fn recoil(battle: anytype, player: Player, residual: *bool,
|
|
2290
|
+
fn recoil(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
|
|
2160
2291
|
var side = battle.side(player);
|
|
2161
2292
|
var stored = side.stored();
|
|
2162
2293
|
|
|
2163
|
-
|
|
2294
|
+
assert(showdown or battle.last_damage > 0);
|
|
2295
|
+
if (showdown and battle.last_damage == 0) return;
|
|
2296
|
+
|
|
2297
|
+
const damage: i16 = @intCast(@max(battle.last_damage /
|
|
2164
2298
|
@as(u8, if (side.last_selected_move == .Struggle) 2 else 4), 1));
|
|
2165
|
-
stored.hp = @intCast(
|
|
2299
|
+
stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
|
|
2166
2300
|
|
|
2167
|
-
try log.damageOf(
|
|
2301
|
+
try options.log.damageOf(
|
|
2302
|
+
battle.active(player),
|
|
2303
|
+
stored,
|
|
2304
|
+
.RecoilOf,
|
|
2305
|
+
battle.active(player.foe()),
|
|
2306
|
+
);
|
|
2168
2307
|
if (showdown and stored.hp == 0) residual.* = false;
|
|
2169
2308
|
}
|
|
2170
2309
|
|
|
2171
|
-
fn reflect(battle: anytype, player: Player,
|
|
2310
|
+
fn reflect(battle: anytype, player: Player, options: anytype) !void {
|
|
2172
2311
|
var side = battle.side(player);
|
|
2173
2312
|
|
|
2174
|
-
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);
|
|
2175
2314
|
side.active.volatiles.Reflect = true;
|
|
2176
2315
|
|
|
2177
|
-
try log.start(battle.active(player), .Reflect);
|
|
2316
|
+
try options.log.start(battle.active(player), .Reflect);
|
|
2178
2317
|
}
|
|
2179
2318
|
|
|
2180
|
-
fn sleep(battle: anytype, player: Player, move: Move.Data,
|
|
2319
|
+
fn sleep(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
2181
2320
|
var foe = battle.foe(player);
|
|
2182
2321
|
var foe_stored = foe.stored();
|
|
2183
2322
|
const foe_ident = battle.active(player.foe());
|
|
@@ -2188,13 +2327,14 @@ pub const Effects = struct {
|
|
|
2188
2327
|
// the Sleep Clause Mod might activate, causing us to not actually bypass
|
|
2189
2328
|
} else {
|
|
2190
2329
|
if (Status.any(foe_stored.status)) {
|
|
2191
|
-
return log.fail(
|
|
2330
|
+
return options.log.fail(
|
|
2192
2331
|
foe_ident,
|
|
2193
2332
|
if (Status.is(foe_stored.status, .SLP)) .Sleep else .None,
|
|
2194
2333
|
);
|
|
2195
2334
|
}
|
|
2196
2335
|
// If checkHit in doMove didn't return true Pokémon Showdown wouldn't be in here
|
|
2197
|
-
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);
|
|
2198
2338
|
}
|
|
2199
2339
|
|
|
2200
2340
|
// Sleep Clause Mod
|
|
@@ -2205,40 +2345,32 @@ pub const Effects = struct {
|
|
|
2205
2345
|
}
|
|
2206
2346
|
foe.active.volatiles.Recharging = false;
|
|
2207
2347
|
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
while (true) {
|
|
2212
|
-
const r = battle.rng.next() & 7;
|
|
2213
|
-
if (r != 0) break :loop r;
|
|
2214
|
-
}
|
|
2215
|
-
});
|
|
2216
|
-
|
|
2217
|
-
foe_stored.status = Status.slp(duration);
|
|
2218
|
-
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);
|
|
2219
2351
|
}
|
|
2220
2352
|
|
|
2221
|
-
fn splash(battle: anytype, player: Player,
|
|
2222
|
-
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);
|
|
2223
2355
|
}
|
|
2224
2356
|
|
|
2225
|
-
fn substitute(battle: anytype, player: Player, residual: *bool,
|
|
2357
|
+
fn substitute(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
|
|
2226
2358
|
var side = battle.side(player);
|
|
2227
2359
|
if (side.active.volatiles.Substitute) {
|
|
2228
|
-
try log.fail(battle.active(player), .Substitute);
|
|
2360
|
+
try options.log.fail(battle.active(player), .Substitute);
|
|
2229
2361
|
return;
|
|
2230
2362
|
}
|
|
2231
2363
|
|
|
2232
2364
|
assert(side.stored().stats.hp <= 1023);
|
|
2233
2365
|
// Will be 0 if HP is <= 3 meaning that the user gets a 1 HP Substitute for "free"
|
|
2234
|
-
const hp = @intCast(
|
|
2366
|
+
const hp: u8 = @intCast(side.stored().stats.hp / 4);
|
|
2235
2367
|
// Pokénon Showdown incorrectly checks for 1/4 HP based on `target.maxhp / 4` which returns
|
|
2236
2368
|
// a floating point value and thus only correctly implements the Substitute 1/4 glitch when
|
|
2237
2369
|
// the target's HP is exactly divisible by 4 (here we're using an inlined divCeil routine to
|
|
2238
2370
|
// avoid having to convert to floating point)
|
|
2239
2371
|
const required_hp = if (showdown) @divFloor(side.stored().stats.hp - 1, 4) + 1 else hp;
|
|
2240
2372
|
if (side.stored().hp < required_hp) {
|
|
2241
|
-
try log.fail(battle.active(player), .Weak);
|
|
2373
|
+
try options.log.fail(battle.active(player), .Weak);
|
|
2242
2374
|
return;
|
|
2243
2375
|
}
|
|
2244
2376
|
|
|
@@ -2246,35 +2378,37 @@ pub const Effects = struct {
|
|
|
2246
2378
|
side.stored().hp -= hp;
|
|
2247
2379
|
side.active.volatiles.substitute = hp + 1;
|
|
2248
2380
|
side.active.volatiles.Substitute = true;
|
|
2249
|
-
try log.start(battle.active(player), .Substitute);
|
|
2381
|
+
try options.log.start(battle.active(player), .Substitute);
|
|
2250
2382
|
if (hp > 0) {
|
|
2251
|
-
try log.damage(battle.active(player), side.stored(), .None);
|
|
2383
|
+
try options.log.damage(battle.active(player), side.stored(), .None);
|
|
2252
2384
|
if (showdown and side.stored().hp == 0) residual.* = false;
|
|
2253
2385
|
}
|
|
2254
2386
|
}
|
|
2255
2387
|
|
|
2256
|
-
fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data,
|
|
2257
|
-
if (!showdown or battle.side(player).last_selected_move == .Teleport) return;
|
|
2258
|
-
|
|
2388
|
+
fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
2259
2389
|
// Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
|
|
2260
|
-
|
|
2261
|
-
|
|
2390
|
+
if (showdown) {
|
|
2391
|
+
if (battle.side(player).last_selected_move == .Teleport) return;
|
|
2392
|
+
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
|
+
}
|
|
2262
2400
|
}
|
|
2263
2401
|
|
|
2264
|
-
fn thrashing(battle: anytype, player: Player) void {
|
|
2402
|
+
fn thrashing(battle: anytype, player: Player, options: anytype) void {
|
|
2265
2403
|
var volatiles = &battle.side(player).active.volatiles;
|
|
2266
2404
|
assert(!volatiles.Thrashing);
|
|
2267
2405
|
assert(!volatiles.Bide);
|
|
2268
2406
|
|
|
2269
2407
|
volatiles.Thrashing = true;
|
|
2270
|
-
volatiles.
|
|
2271
|
-
volatiles.attacks = @intCast(u3, if (showdown)
|
|
2272
|
-
battle.rng.range(u8, 2, 4)
|
|
2273
|
-
else
|
|
2274
|
-
(battle.rng.next() & 1) + 2);
|
|
2408
|
+
volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
2275
2409
|
}
|
|
2276
2410
|
|
|
2277
|
-
fn transform(battle: anytype, player: Player,
|
|
2411
|
+
fn transform(battle: anytype, player: Player, options: anytype) !void {
|
|
2278
2412
|
var side = battle.side(player);
|
|
2279
2413
|
const foe = battle.foe(player);
|
|
2280
2414
|
const foe_ident = battle.active(player.foe());
|
|
@@ -2300,10 +2434,10 @@ pub const Effects = struct {
|
|
|
2300
2434
|
side.active.moves[i].pp = if (m.id != .None) 5 else 0;
|
|
2301
2435
|
}
|
|
2302
2436
|
|
|
2303
|
-
try log.transform(battle.active(player), foe_ident);
|
|
2437
|
+
try options.log.transform(battle.active(player), foe_ident);
|
|
2304
2438
|
}
|
|
2305
2439
|
|
|
2306
|
-
fn binding(battle: anytype, player: Player) void {
|
|
2440
|
+
fn binding(battle: anytype, player: Player, rewrap: bool, options: anytype) !void {
|
|
2307
2441
|
var side = battle.side(player);
|
|
2308
2442
|
var foe = battle.foe(player);
|
|
2309
2443
|
|
|
@@ -2311,12 +2445,16 @@ pub const Effects = struct {
|
|
|
2311
2445
|
side.active.volatiles.Binding = true;
|
|
2312
2446
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2313
2447
|
// (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
|
|
2314
|
-
if (!showdown)
|
|
2448
|
+
if (!showdown) {
|
|
2449
|
+
foe.active.volatiles.Recharging = false;
|
|
2450
|
+
} else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
|
|
2315
2451
|
|
|
2316
|
-
side.active.volatiles.attacks =
|
|
2452
|
+
side.active.volatiles.attacks =
|
|
2453
|
+
try Rolls.distribution(battle, .Binding, player, options) - 1;
|
|
2317
2454
|
}
|
|
2318
2455
|
|
|
2319
|
-
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;
|
|
2320
2458
|
var side = battle.side(player);
|
|
2321
2459
|
const ident = battle.active(player);
|
|
2322
2460
|
|
|
@@ -2328,7 +2466,7 @@ pub const Effects = struct {
|
|
|
2328
2466
|
assert(boosts.atk >= -6 and boosts.atk <= 6);
|
|
2329
2467
|
if (boosts.atk == 6) return try log.fail(ident, .None);
|
|
2330
2468
|
const n: u2 = if (move.effect == .AttackUp2) 2 else 1;
|
|
2331
|
-
boosts.atk = @
|
|
2469
|
+
boosts.atk = @as(i4, @intCast(@min(6, @as(i8, boosts.atk) + n)));
|
|
2332
2470
|
const reason = if (move.effect == .Rage) Boost.Rage else Boost.Attack;
|
|
2333
2471
|
if (stats.atk == MAX_STAT_VALUE) {
|
|
2334
2472
|
boosts.atk -= 1;
|
|
@@ -2339,8 +2477,8 @@ pub const Effects = struct {
|
|
|
2339
2477
|
}
|
|
2340
2478
|
return try log.fail(ident, .None);
|
|
2341
2479
|
}
|
|
2342
|
-
var mod = BOOSTS[@
|
|
2343
|
-
const stat = unmodifiedStats(battle,
|
|
2480
|
+
var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
|
|
2481
|
+
const stat = unmodifiedStats(battle, side).atk;
|
|
2344
2482
|
stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2345
2483
|
try log.boost(ident, reason, n);
|
|
2346
2484
|
// Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
|
|
@@ -2350,7 +2488,7 @@ pub const Effects = struct {
|
|
|
2350
2488
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
2351
2489
|
if (boosts.def == 6) return try log.fail(ident, .None);
|
|
2352
2490
|
const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
|
|
2353
|
-
boosts.def = @intCast(
|
|
2491
|
+
boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
|
|
2354
2492
|
if (stats.def == MAX_STAT_VALUE) {
|
|
2355
2493
|
boosts.def -= 1;
|
|
2356
2494
|
if (showdown) {
|
|
@@ -2359,15 +2497,15 @@ pub const Effects = struct {
|
|
|
2359
2497
|
}
|
|
2360
2498
|
return try log.fail(ident, .None);
|
|
2361
2499
|
}
|
|
2362
|
-
var mod = BOOSTS[@
|
|
2363
|
-
const stat = unmodifiedStats(battle,
|
|
2500
|
+
var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
|
|
2501
|
+
const stat = unmodifiedStats(battle, side).def;
|
|
2364
2502
|
stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2365
2503
|
try log.boost(ident, .Defense, n);
|
|
2366
2504
|
},
|
|
2367
2505
|
.SpeedUp2 => {
|
|
2368
2506
|
assert(boosts.spe >= -6 and boosts.spe <= 6);
|
|
2369
2507
|
if (boosts.spe == 6) return try log.fail(ident, .None);
|
|
2370
|
-
boosts.spe = @intCast(
|
|
2508
|
+
boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
|
|
2371
2509
|
if (stats.spe == MAX_STAT_VALUE) {
|
|
2372
2510
|
boosts.spe -= 1;
|
|
2373
2511
|
if (showdown) {
|
|
@@ -2376,8 +2514,8 @@ pub const Effects = struct {
|
|
|
2376
2514
|
}
|
|
2377
2515
|
return try log.fail(ident, .None);
|
|
2378
2516
|
}
|
|
2379
|
-
var mod = BOOSTS[@
|
|
2380
|
-
const stat = unmodifiedStats(battle,
|
|
2517
|
+
var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
|
|
2518
|
+
const stat = unmodifiedStats(battle, side).spe;
|
|
2381
2519
|
stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2382
2520
|
try log.boost(ident, .Speed, 2);
|
|
2383
2521
|
},
|
|
@@ -2385,7 +2523,7 @@ pub const Effects = struct {
|
|
|
2385
2523
|
assert(boosts.spc >= -6 and boosts.spc <= 6);
|
|
2386
2524
|
if (boosts.spc == 6) return try log.fail(ident, .None);
|
|
2387
2525
|
const n: u2 = if (move.effect == .SpecialUp2) 2 else 1;
|
|
2388
|
-
boosts.spc = @intCast(
|
|
2526
|
+
boosts.spc = @intCast(@min(6, @as(i8, boosts.spc) + n));
|
|
2389
2527
|
if (stats.spc == MAX_STAT_VALUE) {
|
|
2390
2528
|
boosts.spc -= 1;
|
|
2391
2529
|
if (showdown) {
|
|
@@ -2396,8 +2534,8 @@ pub const Effects = struct {
|
|
|
2396
2534
|
}
|
|
2397
2535
|
return try log.fail(ident, .None);
|
|
2398
2536
|
}
|
|
2399
|
-
var mod = BOOSTS[@
|
|
2400
|
-
const stat = unmodifiedStats(battle,
|
|
2537
|
+
var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
|
|
2538
|
+
const stat = unmodifiedStats(battle, side).spc;
|
|
2401
2539
|
stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2402
2540
|
try log.boost(ident, .SpecialAttack, n);
|
|
2403
2541
|
try log.boost(ident, .SpecialDefense, n);
|
|
@@ -2405,7 +2543,7 @@ pub const Effects = struct {
|
|
|
2405
2543
|
.EvasionUp1 => {
|
|
2406
2544
|
assert(boosts.evasion >= -6 and boosts.evasion <= 6);
|
|
2407
2545
|
if (boosts.evasion == 6) return try log.fail(ident, .None);
|
|
2408
|
-
boosts.evasion = @intCast(
|
|
2546
|
+
boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
|
|
2409
2547
|
try log.boost(ident, .Evasion, 1);
|
|
2410
2548
|
},
|
|
2411
2549
|
else => unreachable,
|
|
@@ -2415,32 +2553,32 @@ pub const Effects = struct {
|
|
|
2415
2553
|
statusModify(battle.foe(player).stored().status, &battle.foe(player).active.stats);
|
|
2416
2554
|
}
|
|
2417
2555
|
|
|
2418
|
-
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;
|
|
2419
2558
|
var foe = battle.foe(player);
|
|
2420
2559
|
const foe_ident = battle.active(player.foe());
|
|
2560
|
+
const secondary = move.effect.isStatDownChance();
|
|
2421
2561
|
|
|
2422
|
-
if (foe.active.volatiles.Substitute)
|
|
2423
|
-
return if (!move.effect.isStatDownChance()) log.fail(foe_ident, .None);
|
|
2424
|
-
}
|
|
2562
|
+
if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(foe_ident, .None);
|
|
2425
2563
|
|
|
2426
|
-
if (
|
|
2427
|
-
const
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
if (!
|
|
2432
|
-
|
|
2433
|
-
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);
|
|
2434
2571
|
}
|
|
2435
2572
|
|
|
2436
2573
|
var stats = &foe.active.stats;
|
|
2437
2574
|
var boosts = &foe.active.boosts;
|
|
2575
|
+
const fail = showdown or !secondary;
|
|
2438
2576
|
|
|
2439
2577
|
switch (move.effect) {
|
|
2440
2578
|
.AttackDown1, .AttackDownChance => {
|
|
2441
2579
|
assert(boosts.atk >= -6 and boosts.atk <= 6);
|
|
2442
|
-
if (boosts.atk == -6) return try log.fail(foe_ident, .None);
|
|
2443
|
-
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));
|
|
2444
2582
|
if (stats.atk == 1) {
|
|
2445
2583
|
boosts.atk += 1;
|
|
2446
2584
|
if (showdown) {
|
|
@@ -2449,16 +2587,16 @@ pub const Effects = struct {
|
|
|
2449
2587
|
}
|
|
2450
2588
|
return try log.fail(foe_ident, .None);
|
|
2451
2589
|
}
|
|
2452
|
-
var mod = BOOSTS[@
|
|
2453
|
-
const stat = unmodifiedStats(battle,
|
|
2590
|
+
var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
|
|
2591
|
+
const stat = unmodifiedStats(battle, foe).atk;
|
|
2454
2592
|
stats.atk = @max(1, stat * mod[0] / mod[1]);
|
|
2455
2593
|
try log.boost(foe_ident, .Attack, -1);
|
|
2456
2594
|
},
|
|
2457
2595
|
.DefenseDown1, .DefenseDown2, .DefenseDownChance => {
|
|
2458
2596
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
2459
|
-
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);
|
|
2460
2598
|
const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
|
|
2461
|
-
boosts.def = @intCast(
|
|
2599
|
+
boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
|
|
2462
2600
|
if (stats.def == 1) {
|
|
2463
2601
|
boosts.def += 1;
|
|
2464
2602
|
if (showdown) {
|
|
@@ -2467,15 +2605,15 @@ pub const Effects = struct {
|
|
|
2467
2605
|
}
|
|
2468
2606
|
return try log.fail(foe_ident, .None);
|
|
2469
2607
|
}
|
|
2470
|
-
var mod = BOOSTS[@
|
|
2471
|
-
const stat = unmodifiedStats(battle,
|
|
2608
|
+
var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
|
|
2609
|
+
const stat = unmodifiedStats(battle, foe).def;
|
|
2472
2610
|
stats.def = @max(1, stat * mod[0] / mod[1]);
|
|
2473
2611
|
try log.boost(foe_ident, .Defense, -@as(i8, n));
|
|
2474
2612
|
},
|
|
2475
2613
|
.SpeedDown1, .SpeedDownChance => {
|
|
2476
2614
|
assert(boosts.spe >= -6 and boosts.spe <= 6);
|
|
2477
|
-
if (boosts.spe == -6) return try log.fail(foe_ident, .None);
|
|
2478
|
-
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));
|
|
2479
2617
|
if (stats.spe == 1) {
|
|
2480
2618
|
boosts.spe += 1;
|
|
2481
2619
|
if (showdown) {
|
|
@@ -2484,16 +2622,16 @@ pub const Effects = struct {
|
|
|
2484
2622
|
}
|
|
2485
2623
|
return try log.fail(foe_ident, .None);
|
|
2486
2624
|
}
|
|
2487
|
-
var mod = BOOSTS[@
|
|
2488
|
-
const stat = unmodifiedStats(battle,
|
|
2625
|
+
var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
|
|
2626
|
+
const stat = unmodifiedStats(battle, foe).spe;
|
|
2489
2627
|
stats.spe = @max(1, stat * mod[0] / mod[1]);
|
|
2490
2628
|
try log.boost(foe_ident, .Speed, -1);
|
|
2491
2629
|
assert(boosts.spe >= -6);
|
|
2492
2630
|
},
|
|
2493
2631
|
.SpecialDownChance => {
|
|
2494
2632
|
assert(boosts.spc >= -6 and boosts.spc <= 6);
|
|
2495
|
-
if (boosts.spc == -6) return try log.fail(foe_ident, .None);
|
|
2496
|
-
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));
|
|
2497
2635
|
if (stats.spc == 1) {
|
|
2498
2636
|
boosts.spc += 1;
|
|
2499
2637
|
if (showdown) {
|
|
@@ -2504,16 +2642,16 @@ pub const Effects = struct {
|
|
|
2504
2642
|
}
|
|
2505
2643
|
return try log.fail(foe_ident, .None);
|
|
2506
2644
|
}
|
|
2507
|
-
var mod = BOOSTS[@
|
|
2508
|
-
const stat = unmodifiedStats(battle,
|
|
2645
|
+
var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
|
|
2646
|
+
const stat = unmodifiedStats(battle, foe).spc;
|
|
2509
2647
|
stats.spc = @max(1, stat * mod[0] / mod[1]);
|
|
2510
2648
|
try log.boost(foe_ident, .SpecialAttack, -1);
|
|
2511
2649
|
try log.boost(foe_ident, .SpecialDefense, -1);
|
|
2512
2650
|
},
|
|
2513
2651
|
.AccuracyDown1 => {
|
|
2514
2652
|
assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
|
|
2515
|
-
if (boosts.accuracy == -6) return try log.fail(foe_ident, .None);
|
|
2516
|
-
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));
|
|
2517
2655
|
try log.boost(foe_ident, .Accuracy, -1);
|
|
2518
2656
|
},
|
|
2519
2657
|
else => unreachable,
|
|
@@ -2524,8 +2662,7 @@ pub const Effects = struct {
|
|
|
2524
2662
|
}
|
|
2525
2663
|
};
|
|
2526
2664
|
|
|
2527
|
-
fn unmodifiedStats(battle: anytype,
|
|
2528
|
-
const side = battle.side(who);
|
|
2665
|
+
fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
|
|
2529
2666
|
if (!side.active.volatiles.Transform) return &side.stored().stats;
|
|
2530
2667
|
const id = ID.from(side.active.volatiles.transform);
|
|
2531
2668
|
return &battle.side(id.player).pokemon[id.id - 1].stats;
|
|
@@ -2539,16 +2676,19 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
|
|
|
2539
2676
|
}
|
|
2540
2677
|
}
|
|
2541
2678
|
|
|
2542
|
-
inline fn isForced(active:
|
|
2679
|
+
inline fn isForced(active: anytype) bool {
|
|
2543
2680
|
return active.volatiles.Recharging or active.volatiles.Rage or
|
|
2544
2681
|
active.volatiles.Thrashing or active.volatiles.Charging;
|
|
2545
2682
|
}
|
|
2546
2683
|
|
|
2547
|
-
fn clearVolatiles(battle: anytype, who: Player,
|
|
2684
|
+
fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !void {
|
|
2685
|
+
var log = options.log;
|
|
2548
2686
|
var side = battle.side(who);
|
|
2549
2687
|
var volatiles = &side.active.volatiles;
|
|
2550
2688
|
const ident = battle.active(who);
|
|
2551
2689
|
|
|
2690
|
+
options.chance.clearDurations(who, clear);
|
|
2691
|
+
|
|
2552
2692
|
if (volatiles.disabled_move != 0) {
|
|
2553
2693
|
volatiles.disabled_move = 0;
|
|
2554
2694
|
volatiles.disabled_duration = 0;
|
|
@@ -2561,15 +2701,15 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
|
|
|
2561
2701
|
}
|
|
2562
2702
|
if (volatiles.Mist) {
|
|
2563
2703
|
volatiles.Mist = false;
|
|
2564
|
-
try log.end(ident, .
|
|
2704
|
+
try log.end(ident, .MistSilent);
|
|
2565
2705
|
}
|
|
2566
2706
|
if (volatiles.FocusEnergy) {
|
|
2567
2707
|
volatiles.FocusEnergy = false;
|
|
2568
|
-
try log.end(ident, .
|
|
2708
|
+
try log.end(ident, .FocusEnergySilent);
|
|
2569
2709
|
}
|
|
2570
2710
|
if (volatiles.LeechSeed) {
|
|
2571
2711
|
volatiles.LeechSeed = false;
|
|
2572
|
-
try log.end(ident, .
|
|
2712
|
+
try log.end(ident, .LeechSeedSilent);
|
|
2573
2713
|
}
|
|
2574
2714
|
if (!showdown and volatiles.Toxic) {
|
|
2575
2715
|
volatiles.Toxic = false;
|
|
@@ -2577,107 +2717,329 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
|
|
|
2577
2717
|
}
|
|
2578
2718
|
if (volatiles.LightScreen) {
|
|
2579
2719
|
volatiles.LightScreen = false;
|
|
2580
|
-
try log.end(ident, .
|
|
2720
|
+
try log.end(ident, .LightScreenSilent);
|
|
2581
2721
|
}
|
|
2582
2722
|
if (volatiles.Reflect) {
|
|
2583
2723
|
volatiles.Reflect = false;
|
|
2584
|
-
try log.end(ident, .
|
|
2724
|
+
try log.end(ident, .ReflectSilent);
|
|
2585
2725
|
}
|
|
2586
2726
|
if (showdown and volatiles.Toxic) {
|
|
2587
2727
|
volatiles.Toxic = false;
|
|
2588
2728
|
// Pokémon Showdown erroneously clears the toxic counter
|
|
2589
2729
|
volatiles.toxic = 0;
|
|
2590
|
-
try log.end(ident, .
|
|
2730
|
+
try log.end(ident, .ToxicSilent);
|
|
2591
2731
|
}
|
|
2592
2732
|
}
|
|
2593
2733
|
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
m
|
|
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
|
|
2601
2740
|
else
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
return true;
|
|
2607
|
-
}
|
|
2741
|
+
battle.rng.next() < Gen12.percent(50) + 1;
|
|
2742
|
+
|
|
2743
|
+
try options.chance.speedTie(p1);
|
|
2744
|
+
return p1;
|
|
2608
2745
|
}
|
|
2609
|
-
return false;
|
|
2610
|
-
}
|
|
2611
2746
|
|
|
2612
|
-
inline fn
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
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;
|
|
2754
|
+
|
|
2755
|
+
try options.chance.criticalHit(player, crit, rate);
|
|
2756
|
+
return crit;
|
|
2616
2757
|
}
|
|
2617
|
-
}
|
|
2618
2758
|
|
|
2619
|
-
fn
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
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
|
+
};
|
|
2623
2769
|
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
else
|
|
2629
|
-
(battle.rng.next() & 3) + 2);
|
|
2770
|
+
assert(roll >= 217 and roll <= 255);
|
|
2771
|
+
try options.chance.damage(player, roll);
|
|
2772
|
+
return roll;
|
|
2773
|
+
}
|
|
2630
2774
|
|
|
2631
|
-
|
|
2632
|
-
|
|
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;
|
|
2633
2782
|
|
|
2634
|
-
|
|
2783
|
+
options.chance.hit(ok, accuracy);
|
|
2784
|
+
return ok;
|
|
2785
|
+
}
|
|
2635
2786
|
|
|
2636
|
-
fn
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
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;
|
|
2641
2794
|
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
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);
|
|
2649
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;
|
|
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);
|
|
2650
2975
|
} else {
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
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);
|
|
2657
3017
|
}
|
|
2658
3018
|
}
|
|
2659
|
-
return @intCast(u4, i + 1);
|
|
2660
|
-
}
|
|
2661
|
-
}
|
|
2662
3019
|
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
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;
|
|
2666
3028
|
}
|
|
2667
|
-
}
|
|
3029
|
+
};
|
|
2668
3030
|
|
|
2669
3031
|
test "RNG agreement" {
|
|
2670
3032
|
if (!showdown) return;
|
|
2671
3033
|
var expected: [256]u32 = undefined;
|
|
2672
3034
|
for (0..expected.len) |i| {
|
|
2673
|
-
expected[i] = @intCast(
|
|
3035
|
+
expected[i] = @intCast(i * 0x1000000);
|
|
2674
3036
|
}
|
|
2675
3037
|
|
|
2676
|
-
var spe
|
|
2677
|
-
var cfz
|
|
2678
|
-
var par
|
|
2679
|
-
var brn
|
|
2680
|
-
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 };
|
|
2681
3043
|
|
|
2682
3044
|
for (0..expected.len) |i| {
|
|
2683
3045
|
try expectEqual(spe.range(u8, 0, 2) == 0, i < Gen12.percent(50) + 1);
|
|
@@ -2713,7 +3075,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
2713
3075
|
const side = battle.side(player);
|
|
2714
3076
|
const foe = battle.foe(player);
|
|
2715
3077
|
|
|
2716
|
-
var active = side.active;
|
|
3078
|
+
var active = &side.active;
|
|
2717
3079
|
const stored = side.stored();
|
|
2718
3080
|
|
|
2719
3081
|
// While players are not given any input options on the cartridge in these cases,
|
|
@@ -2727,7 +3089,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
2727
3089
|
// strictly correct as the player would not have been presented the option to move or
|
|
2728
3090
|
// switch at all.
|
|
2729
3091
|
if (isForced(active)) {
|
|
2730
|
-
out[n] = .{ .type = .Move, .data = @
|
|
3092
|
+
out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
|
|
2731
3093
|
n += 1;
|
|
2732
3094
|
return n;
|
|
2733
3095
|
}
|
|
@@ -2764,7 +3126,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
2764
3126
|
while (slot <= 4) : (slot += 1) {
|
|
2765
3127
|
const m = active.moves[slot - 1];
|
|
2766
3128
|
if (m.id == .None) break;
|
|
2767
|
-
if (m.id == side.last_selected_move) {
|
|
3129
|
+
if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
|
|
2768
3130
|
// Pokémon Showdown displays Struggle if limited to Bide but unable to pick
|
|
2769
3131
|
const struggle =
|
|
2770
3132
|
m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);
|