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