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