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