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