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