@pkmn/engine 0.1.0-dev.5ec64ea5 → 0.1.0-dev.6158d5ad
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -60
- package/build/pkg/addon.d.ts +2 -2
- package/build/pkg/addon.js +8 -7
- package/build/pkg/addon.js.map +1 -1
- package/build/pkg/data/layout.json +25 -3
- package/build/pkg/gen1.d.ts +5 -4
- package/build/pkg/gen1.js +47 -31
- package/build/pkg/gen1.js.map +1 -1
- package/build/pkg/index.d.ts +15 -12
- package/build/pkg/index.js.map +1 -1
- package/build/pkg/protocol.d.ts +4 -2
- package/build/pkg/protocol.js +4 -2
- package/build/pkg/protocol.js.map +1 -1
- package/build/test/display.js +31 -13
- package/build/test/display.js.map +1 -1
- package/build.zig +95 -76
- package/package.json +26 -16
- package/src/bin/install-pkmn-engine +11 -6
- package/src/bin/pkmn-debug +2 -3
- 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 +262 -154
- 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 -32
- package/src/lib/gen1/mechanics.zig +983 -626
- 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
|
@@ -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,34 +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
|
+
|
|
350
378
|
residual = true;
|
|
351
379
|
replace = battle.side(foe_player).stored().hp == 0;
|
|
352
|
-
|
|
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;
|
|
353
391
|
if (!replace) {
|
|
354
|
-
if (foe_choice.type != .Switch) {
|
|
355
|
-
if (try checkFaint(battle, player,
|
|
392
|
+
if (!calc and foe_choice.type != .Switch) {
|
|
393
|
+
if (try checkFaint(battle, player, options)) |r| return r;
|
|
356
394
|
}
|
|
357
|
-
if (residual) try handleResidual(battle, foe_player,
|
|
358
|
-
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;
|
|
359
397
|
}
|
|
360
398
|
|
|
361
399
|
// Flinch is bugged on Pokémon Showdown because it gets implemented with a duration which causes
|
|
@@ -372,29 +410,32 @@ fn executeMove(
|
|
|
372
410
|
battle: anytype,
|
|
373
411
|
player: Player,
|
|
374
412
|
choice: Choice,
|
|
413
|
+
rewrap: bool,
|
|
414
|
+
skip: bool,
|
|
375
415
|
from: ?Move,
|
|
376
416
|
residual: *bool,
|
|
377
|
-
|
|
417
|
+
options: anytype,
|
|
378
418
|
) !?Result {
|
|
379
419
|
var side = battle.side(player);
|
|
380
420
|
|
|
381
421
|
if (choice.type == .Switch) {
|
|
382
|
-
try switchIn(battle, player,
|
|
422
|
+
try switchIn(battle, player, choice.data, false, options);
|
|
383
423
|
return null;
|
|
384
424
|
}
|
|
385
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
|
|
386
429
|
if (side.last_selected_move == .SKIP_TURN) {
|
|
387
|
-
// Pokémon Showdown overwrites the SKIP_TURN sentinel with its botched move select,
|
|
388
|
-
// Binding instead gets handled in beforeMove after sleep and freeze
|
|
389
430
|
assert(!showdown);
|
|
390
431
|
if (battle.foe(player).active.volatiles.Binding) {
|
|
391
|
-
try log.cant(battle.active(player), .Bound);
|
|
432
|
+
try options.log.cant(battle.active(player), .Bound);
|
|
392
433
|
}
|
|
393
434
|
return null;
|
|
394
435
|
}
|
|
395
436
|
|
|
396
437
|
assert(choice.type == .Move);
|
|
397
|
-
var mslot = @intCast(
|
|
438
|
+
var mslot: u4 = @intCast(choice.data);
|
|
398
439
|
// Sadly, we can't even check `Move.get(side.last_selected_move).effect == .Binding` here
|
|
399
440
|
// because Pokémon Showdown's Mirror Move implementation clobbers side.last_selected_move
|
|
400
441
|
var auto = showdown and side.last_selected_move != .None;
|
|
@@ -403,10 +444,7 @@ fn executeMove(
|
|
|
403
444
|
if (mslot == 0 and side.last_selected_move != .None and side.last_selected_move != .Struggle) {
|
|
404
445
|
// choice.data == 0 only happens with Struggle on Pokémon Showdown
|
|
405
446
|
assert(!showdown);
|
|
406
|
-
mslot = @intCast(
|
|
407
|
-
battle.last_selected_indexes.p1
|
|
408
|
-
else
|
|
409
|
-
battle.last_selected_indexes.p2);
|
|
447
|
+
mslot = @intCast(battle.lastMove(player).index);
|
|
410
448
|
const stored = side.stored();
|
|
411
449
|
// GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
|
|
412
450
|
auto = side.last_selected_move == .HyperBeam or side.active.volatiles.Bide or
|
|
@@ -427,15 +465,12 @@ fn executeMove(
|
|
|
427
465
|
} else if (showdown and side.active.volatiles.Charging) {
|
|
428
466
|
// Incorrect mslot with Pokémon Showdown choice semantics so we need to recover from index
|
|
429
467
|
assert(mslot == 1);
|
|
430
|
-
mslot = @intCast(
|
|
431
|
-
battle.last_selected_indexes.p1
|
|
432
|
-
else
|
|
433
|
-
battle.last_selected_indexes.p2);
|
|
468
|
+
mslot = @intCast(battle.lastMove(player).index);
|
|
434
469
|
}
|
|
435
470
|
|
|
436
471
|
var skip_can = false;
|
|
437
472
|
var skip_pp = false;
|
|
438
|
-
switch (try beforeMove(battle, player, from, residual,
|
|
473
|
+
switch (try beforeMove(battle, player, skip, from, residual, options)) {
|
|
439
474
|
.done => return null,
|
|
440
475
|
.skip_can => skip_can = true,
|
|
441
476
|
.skip_pp => skip_pp = true,
|
|
@@ -443,22 +478,24 @@ fn executeMove(
|
|
|
443
478
|
.err => return @as(?Result, Result.Error),
|
|
444
479
|
}
|
|
445
480
|
|
|
446
|
-
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)) {
|
|
447
482
|
return null;
|
|
448
483
|
}
|
|
449
484
|
|
|
450
|
-
return doMove(battle, player, mslot, auto, residual,
|
|
485
|
+
return doMove(battle, player, mslot, rewrap, auto, residual, options);
|
|
451
486
|
}
|
|
452
487
|
|
|
453
|
-
const BeforeMove =
|
|
488
|
+
const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
|
|
454
489
|
|
|
455
490
|
fn beforeMove(
|
|
456
491
|
battle: anytype,
|
|
457
492
|
player: Player,
|
|
493
|
+
skip: bool,
|
|
458
494
|
from: ?Move,
|
|
459
495
|
residual: *bool,
|
|
460
|
-
|
|
496
|
+
options: anytype,
|
|
461
497
|
) !BeforeMove {
|
|
498
|
+
var log = options.log;
|
|
462
499
|
var side = battle.side(player);
|
|
463
500
|
const foe = battle.foe(player);
|
|
464
501
|
var active = &side.active;
|
|
@@ -467,11 +504,21 @@ fn beforeMove(
|
|
|
467
504
|
var volatiles = &active.volatiles;
|
|
468
505
|
|
|
469
506
|
if (Status.is(stored.status, .SLP)) {
|
|
470
|
-
const
|
|
507
|
+
const before = stored.status;
|
|
471
508
|
// Even if the EXT bit is set this will still correctly modify the sleep duration
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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);
|
|
475
522
|
stored.status = 0; // clears EXT if present
|
|
476
523
|
} else {
|
|
477
524
|
try log.cant(ident, .Sleep);
|
|
@@ -486,13 +533,15 @@ fn beforeMove(
|
|
|
486
533
|
return .done;
|
|
487
534
|
}
|
|
488
535
|
|
|
489
|
-
if (foe.active.volatiles.Binding) {
|
|
536
|
+
if (skip or foe.active.volatiles.Binding) {
|
|
490
537
|
try log.cant(ident, .Bound);
|
|
491
538
|
return .done;
|
|
492
539
|
}
|
|
493
540
|
|
|
494
|
-
if (
|
|
495
|
-
|
|
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;
|
|
496
545
|
try log.cant(ident, .Flinch);
|
|
497
546
|
return .done;
|
|
498
547
|
}
|
|
@@ -504,21 +553,20 @@ fn beforeMove(
|
|
|
504
553
|
}
|
|
505
554
|
|
|
506
555
|
if (volatiles.disabled_duration > 0) {
|
|
507
|
-
|
|
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
|
+
|
|
508
563
|
if (volatiles.disabled_duration == 0) {
|
|
509
564
|
volatiles.disabled_move = 0;
|
|
510
565
|
try log.end(ident, .Disable);
|
|
511
566
|
}
|
|
512
567
|
}
|
|
513
568
|
// Pokémon Showdown's disable condition has a single onBeforeMove handler
|
|
514
|
-
if (showdown and try disabled(side, ident,
|
|
515
|
-
|
|
516
|
-
// Pokémon Showdown checks for Flinch *after* instead of before
|
|
517
|
-
if (showdown and volatiles.Flinch) {
|
|
518
|
-
volatiles.Flinch = false;
|
|
519
|
-
try log.cant(ident, .Flinch);
|
|
520
|
-
return .done;
|
|
521
|
-
}
|
|
569
|
+
if (showdown and try disabled(side, ident, options)) return .done;
|
|
522
570
|
|
|
523
571
|
// This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
|
|
524
572
|
// before the side had a selected a move - we simply need to assume this leads to a desync
|
|
@@ -530,19 +578,20 @@ fn beforeMove(
|
|
|
530
578
|
if (volatiles.Confusion) {
|
|
531
579
|
assert(volatiles.confusion > 0);
|
|
532
580
|
|
|
533
|
-
|
|
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
|
+
|
|
534
588
|
if (volatiles.confusion == 0) {
|
|
535
589
|
volatiles.Confusion = false;
|
|
536
590
|
try log.end(ident, .Confusion);
|
|
537
591
|
} else {
|
|
538
592
|
try log.activate(ident, .Confusion);
|
|
539
593
|
|
|
540
|
-
|
|
541
|
-
!battle.rng.chance(u8, 128, 256)
|
|
542
|
-
else
|
|
543
|
-
battle.rng.next() >= Gen12.percent(50) + 1;
|
|
544
|
-
|
|
545
|
-
if (confused) {
|
|
594
|
+
if (try Rolls.confused(battle, player, options)) {
|
|
546
595
|
assert(!volatiles.MultiHit);
|
|
547
596
|
if (!volatiles.Rage) volatiles.state = 0;
|
|
548
597
|
volatiles.Bide = false;
|
|
@@ -552,6 +601,7 @@ fn beforeMove(
|
|
|
552
601
|
volatiles.Charging = false;
|
|
553
602
|
volatiles.Binding = false;
|
|
554
603
|
volatiles.Invulnerable = false;
|
|
604
|
+
options.chance.clearDurations(player, null);
|
|
555
605
|
{
|
|
556
606
|
// This feels (and is) disgusting but the cartridge literally just overwrites
|
|
557
607
|
// the opponent's defense with the user's defense and resets it after. As a
|
|
@@ -568,42 +618,53 @@ fn beforeMove(
|
|
|
568
618
|
player
|
|
569
619
|
else
|
|
570
620
|
player.foe();
|
|
621
|
+
|
|
622
|
+
const uncapped = battle.last_damage;
|
|
571
623
|
// Skipping adjustDamage / randomizeDamage / checkHit
|
|
572
|
-
_ = 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;
|
|
573
627
|
|
|
574
628
|
return .done;
|
|
575
629
|
}
|
|
576
630
|
}
|
|
577
631
|
}
|
|
578
632
|
|
|
579
|
-
if (!showdown and try disabled(side, ident,
|
|
580
|
-
|
|
581
|
-
if (Status.is(stored.status, .PAR)) {
|
|
582
|
-
const paralyzed = if (showdown)
|
|
583
|
-
battle.rng.chance(u8, 63, 256)
|
|
584
|
-
else
|
|
585
|
-
battle.rng.next() < Gen12.percent(25);
|
|
633
|
+
if (!showdown and try disabled(side, ident, options)) return .done;
|
|
586
634
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
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;
|
|
597
645
|
}
|
|
598
646
|
|
|
599
647
|
if (volatiles.Bide) {
|
|
600
648
|
assert(!volatiles.Thrashing and !volatiles.Rage);
|
|
601
649
|
|
|
602
|
-
|
|
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
|
+
}
|
|
603
658
|
|
|
604
659
|
assert(volatiles.attacks > 0);
|
|
605
660
|
|
|
606
|
-
|
|
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
|
+
|
|
607
668
|
if (volatiles.attacks != 0) {
|
|
608
669
|
try log.activate(ident, .Bide);
|
|
609
670
|
return .done;
|
|
@@ -621,15 +682,15 @@ fn beforeMove(
|
|
|
621
682
|
}
|
|
622
683
|
|
|
623
684
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
624
|
-
_ = try applyDamage(battle, player.foe(), player.foe(), .None,
|
|
625
|
-
if (foe.stored().hp > 0 and !sub) try buildRage(battle, player.foe(),
|
|
685
|
+
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
686
|
+
if (foe.stored().hp > 0 and !sub) try buildRage(battle, player.foe(), options);
|
|
626
687
|
|
|
627
688
|
// For reasons passing understanding, Pokémon Showdown still inflicts residual damage to
|
|
628
689
|
// Bide's user even if the above damage has caused the foe to faint. It's simpler to always
|
|
629
690
|
// run residual here regardless of whether the foe fainted and opt-out of the default flow
|
|
630
691
|
if (showdown) {
|
|
631
692
|
residual.* = false;
|
|
632
|
-
try handleResidual(battle, player,
|
|
693
|
+
try handleResidual(battle, player, options);
|
|
633
694
|
}
|
|
634
695
|
|
|
635
696
|
return .done;
|
|
@@ -638,20 +699,39 @@ fn beforeMove(
|
|
|
638
699
|
if (volatiles.Thrashing) {
|
|
639
700
|
assert(volatiles.attacks > 0);
|
|
640
701
|
assert(from != null);
|
|
641
|
-
|
|
642
|
-
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)) {
|
|
643
711
|
try log.start(battle.active(player), .ConfusionSilent);
|
|
644
712
|
}
|
|
645
713
|
try log.move(ident, side.last_selected_move, battle.active(player.foe()), from);
|
|
646
|
-
if (showdown
|
|
647
|
-
|
|
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
|
+
}
|
|
648
721
|
}
|
|
649
722
|
return .skip_can;
|
|
650
723
|
}
|
|
651
724
|
|
|
652
725
|
if (volatiles.Binding) {
|
|
653
726
|
assert(volatiles.attacks > 0);
|
|
654
|
-
|
|
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
|
+
|
|
655
735
|
try log.move(
|
|
656
736
|
battle.active(player),
|
|
657
737
|
side.last_selected_move,
|
|
@@ -659,7 +739,11 @@ fn beforeMove(
|
|
|
659
739
|
side.last_selected_move,
|
|
660
740
|
);
|
|
661
741
|
if (showdown or battle.last_damage != 0) {
|
|
662
|
-
|
|
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
|
+
}
|
|
663
747
|
}
|
|
664
748
|
return .done;
|
|
665
749
|
}
|
|
@@ -675,7 +759,7 @@ fn canMove(
|
|
|
675
759
|
skip_pp: bool,
|
|
676
760
|
from: ?Move,
|
|
677
761
|
residual: *bool,
|
|
678
|
-
|
|
762
|
+
options: anytype,
|
|
679
763
|
) !bool {
|
|
680
764
|
var side = battle.side(player);
|
|
681
765
|
const player_ident = battle.active(player);
|
|
@@ -690,8 +774,9 @@ fn canMove(
|
|
|
690
774
|
side.active.volatiles.Charging = false;
|
|
691
775
|
side.active.volatiles.Invulnerable = false;
|
|
692
776
|
} else if (move.effect == .Charge) {
|
|
693
|
-
try log.move(player_ident, side.last_selected_move, .{}, from);
|
|
694
|
-
|
|
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);
|
|
695
780
|
return false;
|
|
696
781
|
}
|
|
697
782
|
|
|
@@ -706,23 +791,35 @@ fn canMove(
|
|
|
706
791
|
// relevant for control flow, but we don't have anything useful to put in the log. This is
|
|
707
792
|
// regrettable, but this information is more "nice to have" than required
|
|
708
793
|
const f = if (from != null and from.? == .None) null else from;
|
|
709
|
-
try log.move(player_ident, side.last_selected_move, battle.active(target), f);
|
|
794
|
+
try options.log.move(player_ident, side.last_selected_move, battle.active(target), f);
|
|
795
|
+
setCounterable(battle, player, side, move);
|
|
710
796
|
|
|
711
797
|
if (move.effect.onBegin()) {
|
|
712
|
-
try onBegin(battle, player, move, mslot, residual,
|
|
798
|
+
try onBegin(battle, player, move, mslot, residual, options);
|
|
713
799
|
return false;
|
|
714
800
|
}
|
|
715
801
|
|
|
716
802
|
if (move.effect == .Thrashing) {
|
|
717
|
-
Effects.thrashing(battle, player);
|
|
803
|
+
Effects.thrashing(battle, player, options);
|
|
718
804
|
} else if (!showdown and move.effect == .Binding) {
|
|
719
805
|
// Pokémon Showdown handles this after hit/miss checks and damage calculation
|
|
720
|
-
Effects.binding(battle, player);
|
|
806
|
+
try Effects.binding(battle, player, false, options);
|
|
721
807
|
}
|
|
722
808
|
|
|
723
809
|
return true;
|
|
724
810
|
}
|
|
725
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
|
+
|
|
726
823
|
fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
|
|
727
824
|
if (side.last_selected_move == .Struggle) return;
|
|
728
825
|
|
|
@@ -733,11 +830,11 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
|
|
|
733
830
|
if (volatiles.Bide) return;
|
|
734
831
|
|
|
735
832
|
assert(active.move(mslot).pp > 0 or auto);
|
|
736
|
-
active.move(mslot).pp = @
|
|
833
|
+
active.move(mslot).pp = @as(u6, @intCast(active.move(mslot).pp)) -% 1;
|
|
737
834
|
if (volatiles.Transform) return;
|
|
738
835
|
|
|
739
836
|
assert(side.stored().move(mslot).pp > 0 or auto);
|
|
740
|
-
side.stored().move(mslot).pp = @
|
|
837
|
+
side.stored().move(mslot).pp = @as(u6, @intCast(side.stored().move(mslot).pp)) -% 1;
|
|
741
838
|
assert(active.move(mslot).pp == side.stored().move(mslot).pp);
|
|
742
839
|
}
|
|
743
840
|
|
|
@@ -745,12 +842,12 @@ fn incrementPP(side: *Side, mslot: u4) void {
|
|
|
745
842
|
var active = &side.active;
|
|
746
843
|
const volatiles = &active.volatiles;
|
|
747
844
|
|
|
748
|
-
active.move(mslot).pp = @
|
|
845
|
+
active.move(mslot).pp = @as(u6, @intCast(active.move(mslot).pp)) +% 1;
|
|
749
846
|
// GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
|
|
750
847
|
if (showdown and volatiles.Transform) return;
|
|
751
848
|
|
|
752
849
|
assert(mslot > 0 and mslot <= 4);
|
|
753
|
-
side.stored().moves[mslot - 1].pp = @
|
|
850
|
+
side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% 1;
|
|
754
851
|
}
|
|
755
852
|
|
|
756
853
|
// Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
|
|
@@ -758,10 +855,12 @@ fn doMove(
|
|
|
758
855
|
battle: anytype,
|
|
759
856
|
player: Player,
|
|
760
857
|
mslot: u4,
|
|
858
|
+
rewrap: bool,
|
|
761
859
|
auto: bool,
|
|
762
860
|
residual: *bool,
|
|
763
|
-
|
|
861
|
+
options: anytype,
|
|
764
862
|
) !?Result {
|
|
863
|
+
var log = options.log;
|
|
765
864
|
var side = battle.side(player);
|
|
766
865
|
const foe = battle.foe(player);
|
|
767
866
|
|
|
@@ -776,40 +875,44 @@ fn doMove(
|
|
|
776
875
|
var hits: u4 = 1;
|
|
777
876
|
var effectiveness = Effectiveness.neutral;
|
|
778
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
|
+
|
|
779
881
|
// The cartridge handles set damage moves in applyDamage but we short circuit to simplify things
|
|
780
882
|
if (move.effect == .SuperFang or move.effect == .SpecialDamage) {
|
|
781
|
-
return specialDamage(battle, player, move,
|
|
883
|
+
return specialDamage(battle, player, move, options);
|
|
782
884
|
}
|
|
783
885
|
|
|
784
886
|
// Pokémon Showdown runs invulnerability / immunity checks before checking accuracy - simply
|
|
785
887
|
// calling moveHit early covers most of that but we also need to check type immunity first
|
|
786
888
|
var miss = showdown and miss: {
|
|
787
889
|
immune = move.target != .Self and !status and !counter and
|
|
788
|
-
(@
|
|
789
|
-
@
|
|
890
|
+
(@intFromEnum(move.type.effectiveness(foe.active.types.type1)) == 0 or
|
|
891
|
+
@intFromEnum(move.type.effectiveness(foe.active.types.type2)) == 0);
|
|
790
892
|
if (immune and move.effect != .Binding) break :miss true;
|
|
791
893
|
if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) {
|
|
792
894
|
battle.last_damage = 0;
|
|
793
895
|
break :miss true;
|
|
794
896
|
}
|
|
795
|
-
break :miss move.target != .Self and
|
|
897
|
+
break :miss (move.target != .Self and
|
|
898
|
+
!moveHit(battle, player, move, &immune, &mist, options));
|
|
796
899
|
};
|
|
797
900
|
assert(!immune or miss or (showdown and move.effect == .Binding));
|
|
798
901
|
|
|
799
|
-
var late = showdown;
|
|
902
|
+
var late = showdown and move.effect != .Explode;
|
|
800
903
|
const skip = status or immune;
|
|
801
904
|
if ((!showdown or (!skip or counter)) and !miss) blk: {
|
|
802
905
|
if (showdown and move.effect.isMulti()) {
|
|
803
|
-
Effects.multiHit(battle, player, move);
|
|
906
|
+
try Effects.multiHit(battle, player, move, options);
|
|
804
907
|
hits = side.active.volatiles.attacks;
|
|
805
908
|
late = false;
|
|
806
909
|
}
|
|
807
910
|
|
|
808
911
|
// Cartridge rolls for crit even for moves that can't crit (Counter/Metronome/status/OHKO)
|
|
809
912
|
const check = !showdown or (!counter and move.effect != .OHKO);
|
|
810
|
-
if (check) crit = checkCriticalHit(battle, player, move);
|
|
913
|
+
if (check) crit = try checkCriticalHit(battle, player, move, options);
|
|
811
914
|
|
|
812
|
-
if (counter) return counterDamage(battle, player, move,
|
|
915
|
+
if (counter) return counterDamage(battle, player, move, options);
|
|
813
916
|
|
|
814
917
|
battle.last_damage = 0;
|
|
815
918
|
|
|
@@ -831,17 +934,15 @@ fn doMove(
|
|
|
831
934
|
effectiveness = adjustDamage(battle, player);
|
|
832
935
|
immune = effectiveness == 0;
|
|
833
936
|
}
|
|
834
|
-
randomizeDamage(battle);
|
|
937
|
+
try randomizeDamage(battle, player, options);
|
|
835
938
|
}
|
|
836
939
|
}
|
|
837
940
|
|
|
838
|
-
|
|
839
|
-
if (showdown and skip) battle.last_damage = 0;
|
|
840
|
-
|
|
941
|
+
var zero = battle.last_damage == 0;
|
|
841
942
|
miss = if (showdown or skip)
|
|
842
943
|
miss
|
|
843
944
|
else
|
|
844
|
-
(!moveHit(battle, player, move, &immune, &mist) or
|
|
945
|
+
(!moveHit(battle, player, move, &immune, &mist, options) or zero);
|
|
845
946
|
|
|
846
947
|
assert(showdown or miss or battle.last_damage > 0 or skip);
|
|
847
948
|
assert((!showdown and miss) or !(ohko and immune));
|
|
@@ -849,11 +950,11 @@ fn doMove(
|
|
|
849
950
|
|
|
850
951
|
if (!showdown or !miss) {
|
|
851
952
|
if (move.effect == .MirrorMove) {
|
|
852
|
-
return mirrorMove(battle, player, mslot, auto, residual,
|
|
953
|
+
return mirrorMove(battle, player, mslot, rewrap, auto, residual, options);
|
|
853
954
|
} else if (move.effect == .Metronome) {
|
|
854
|
-
return metronome(battle, player, mslot, auto, residual,
|
|
955
|
+
return metronome(battle, player, mslot, rewrap, auto, residual, options);
|
|
855
956
|
} else if (move.effect.onEnd()) {
|
|
856
|
-
try onEnd(battle, player, move,
|
|
957
|
+
try onEnd(battle, player, move, options);
|
|
857
958
|
return null;
|
|
858
959
|
}
|
|
859
960
|
}
|
|
@@ -866,35 +967,40 @@ fn doMove(
|
|
|
866
967
|
move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
|
|
867
968
|
if (ohko) {
|
|
868
969
|
try log.immune(foe_ident, .OHKO);
|
|
869
|
-
} else if (immune and !invulnerable) {
|
|
970
|
+
} else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
|
|
870
971
|
try log.immune(foe_ident, .None);
|
|
871
972
|
} else if (mist) {
|
|
872
973
|
if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
|
|
873
974
|
try log.fail(foe_ident, .None);
|
|
874
975
|
} else {
|
|
976
|
+
if (showdown or !zero) try options.chance.commit(player, .miss);
|
|
875
977
|
try log.lastmiss();
|
|
876
978
|
try log.miss(battle.active(player));
|
|
877
979
|
}
|
|
878
980
|
if (move.effect == .JumpKick) {
|
|
879
981
|
// Recoil is supposed to be damage/8 but damage will always be 0 here
|
|
880
|
-
assert(battle.last_damage == 0);
|
|
982
|
+
assert(showdown or battle.last_damage == 0);
|
|
881
983
|
battle.last_damage = 1;
|
|
882
|
-
_ = try applyDamage(battle, player, player.foe(), .None,
|
|
984
|
+
_ = try applyDamage(battle, player, player.foe(), .None, options);
|
|
883
985
|
if (showdown and side.stored().hp == 0) residual.* = false;
|
|
884
986
|
} else if (move.effect == .Explode) {
|
|
885
987
|
try Effects.explode(battle, player);
|
|
886
|
-
try buildRage(battle, player.foe(),
|
|
988
|
+
try buildRage(battle, player.foe(), options);
|
|
887
989
|
} else if (showdown and move.effect == .Disable) {
|
|
888
|
-
try buildRage(battle, player.foe(),
|
|
990
|
+
try buildRage(battle, player.foe(), options);
|
|
991
|
+
} else if (!showdown and immune and move.effect == .Binding) {
|
|
992
|
+
try options.chance.commit(player, .binding);
|
|
889
993
|
}
|
|
890
994
|
return null;
|
|
995
|
+
} else {
|
|
996
|
+
try options.chance.commit(player, .hit);
|
|
891
997
|
}
|
|
892
998
|
|
|
893
999
|
// On the cartridge MultiHit doesn't get set up until after damage has been applied for the
|
|
894
1000
|
// first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
|
|
895
1001
|
// it up above before damage calculation).
|
|
896
1002
|
if (!showdown and move.effect.isMulti()) {
|
|
897
|
-
Effects.multiHit(battle, player, move);
|
|
1003
|
+
try Effects.multiHit(battle, player, move, options);
|
|
898
1004
|
hits = side.active.volatiles.attacks;
|
|
899
1005
|
}
|
|
900
1006
|
|
|
@@ -912,11 +1018,13 @@ fn doMove(
|
|
|
912
1018
|
try log.resisted(battle.active(player.foe()));
|
|
913
1019
|
}
|
|
914
1020
|
}
|
|
915
|
-
if (!skip) nullified = try applyDamage(battle, player.foe(), player.foe(), .None,
|
|
1021
|
+
if (!skip) nullified = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
916
1022
|
if (hit == 0 and ohko) try log.ohko();
|
|
917
1023
|
hit += 1;
|
|
918
1024
|
if (foe.stored().hp == 0) break;
|
|
919
|
-
if (!late and !sub
|
|
1025
|
+
if (!late and (!sub or move.effect == .Explode)) {
|
|
1026
|
+
try buildRage(battle, player.foe(), options);
|
|
1027
|
+
}
|
|
920
1028
|
// If the substitute breaks during a multi-hit attack, the attack ends
|
|
921
1029
|
if (nullified) break;
|
|
922
1030
|
}
|
|
@@ -926,17 +1034,23 @@ fn doMove(
|
|
|
926
1034
|
assert(nullified or foe.stored().hp == 0 or side.active.volatiles.attacks - hit == 0);
|
|
927
1035
|
side.active.volatiles.attacks = 0;
|
|
928
1036
|
if (showdown and move.effect == .Twineedle and !nullified and foe.stored().hp > 0) {
|
|
929
|
-
try Effects.poison(battle, player, Move.get(.PoisonSting),
|
|
1037
|
+
try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
930
1038
|
}
|
|
931
1039
|
try log.hitcount(battle.active(player.foe()), hit);
|
|
932
1040
|
} else if (showdown) {
|
|
933
1041
|
// This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
|
|
934
1042
|
if (move.effect == .Binding) {
|
|
935
|
-
Effects.binding(battle, player);
|
|
1043
|
+
try Effects.binding(battle, player, rewrap, options);
|
|
936
1044
|
if (immune) {
|
|
937
1045
|
battle.last_damage = 0;
|
|
1046
|
+
assert(foe.stored().hp > 0);
|
|
938
1047
|
// Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
|
|
939
|
-
|
|
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
|
+
}
|
|
940
1054
|
return null;
|
|
941
1055
|
}
|
|
942
1056
|
}
|
|
@@ -949,12 +1063,12 @@ fn doMove(
|
|
|
949
1063
|
// On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
|
|
950
1064
|
// but this is only done to setup the MultiHit looping in the first place. Moving the MultiHit
|
|
951
1065
|
// setup before the loop means we can avoid having to waste time doing no-op handler searches.
|
|
952
|
-
if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual,
|
|
1066
|
+
if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
|
|
953
1067
|
|
|
954
1068
|
if (foe.stored().hp == 0) return null;
|
|
955
1069
|
|
|
956
1070
|
// Pokémon Showdown builds Rage at the wrong time for non-MultiHit move
|
|
957
|
-
if (late and !sub and move.effect != .Disable) try buildRage(battle, player.foe(),
|
|
1071
|
+
if (late and !sub and move.effect != .Disable) try buildRage(battle, player.foe(), options);
|
|
958
1072
|
|
|
959
1073
|
if (!move.effect.isSpecial()) {
|
|
960
1074
|
// On the cartridge Rage is not considered to be "special" and thus gets executed for a
|
|
@@ -965,30 +1079,29 @@ fn doMove(
|
|
|
965
1079
|
// Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
|
|
966
1080
|
// puts the |-status| message before |-hitcount| instead of after.
|
|
967
1081
|
if (move.effect == .Twineedle) {
|
|
968
|
-
if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting),
|
|
1082
|
+
if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
969
1083
|
} else if (move.effect == .Disable) {
|
|
970
|
-
const result = try Effects.disable(battle, player, move,
|
|
971
|
-
if (showdown) try buildRage(battle, player.foe(),
|
|
1084
|
+
const result = try Effects.disable(battle, player, move, options);
|
|
1085
|
+
if (showdown) try buildRage(battle, player.foe(), options);
|
|
972
1086
|
return result;
|
|
973
1087
|
} else {
|
|
974
|
-
try moveEffect(battle, player, move,
|
|
1088
|
+
try moveEffect(battle, player, move, options);
|
|
975
1089
|
}
|
|
976
1090
|
}
|
|
977
1091
|
|
|
978
1092
|
return null;
|
|
979
1093
|
}
|
|
980
1094
|
|
|
981
|
-
fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
|
|
1095
|
+
fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !bool {
|
|
982
1096
|
const side = battle.side(player);
|
|
983
1097
|
|
|
984
1098
|
// Base speed is used for the critical hit calculation, even when Transform-ed
|
|
985
|
-
var
|
|
986
|
-
// GLITCH: Focus Energy reduces critical hit
|
|
987
|
-
|
|
988
|
-
|
|
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;
|
|
989
1103
|
|
|
990
|
-
|
|
991
|
-
return std.math.rotl(u8, battle.rng.next(), 3) < chance;
|
|
1104
|
+
return Rolls.criticalHit(battle, player, @intCast(rate), options);
|
|
992
1105
|
}
|
|
993
1106
|
|
|
994
1107
|
fn calcDamage(
|
|
@@ -1054,7 +1167,7 @@ fn calcDamage(
|
|
|
1054
1167
|
d = @min(997, d);
|
|
1055
1168
|
d += 2;
|
|
1056
1169
|
|
|
1057
|
-
battle.last_damage = @intCast(
|
|
1170
|
+
battle.last_damage = @intCast(d);
|
|
1058
1171
|
|
|
1059
1172
|
return true;
|
|
1060
1173
|
}
|
|
@@ -1068,9 +1181,9 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
|
1068
1181
|
var d = battle.last_damage;
|
|
1069
1182
|
if (side.active.types.includes(move.type)) d +%= d / 2;
|
|
1070
1183
|
|
|
1071
|
-
const neutral = @
|
|
1072
|
-
const eff1: u16 = @
|
|
1073
|
-
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));
|
|
1074
1187
|
|
|
1075
1188
|
// Type effectiveness matchup precedence only matters with (NVE, SE)
|
|
1076
1189
|
if (!showdown and (eff1 + eff2) == Effectiveness.mismatch and
|
|
@@ -1090,26 +1203,20 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
|
1090
1203
|
return if (types.type1 == types.type2) eff1 * neutral else eff1 * eff2;
|
|
1091
1204
|
}
|
|
1092
1205
|
|
|
1093
|
-
fn randomizeDamage(battle: anytype) void {
|
|
1206
|
+
fn randomizeDamage(battle: anytype, player: Player, options: anytype) !void {
|
|
1094
1207
|
if (battle.last_damage <= 1) return;
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
while (true) {
|
|
1100
|
-
const r = std.math.rotr(u8, battle.rng.next(), 1);
|
|
1101
|
-
if (r >= 217) break :loop r;
|
|
1102
|
-
}
|
|
1103
|
-
};
|
|
1104
|
-
|
|
1105
|
-
battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
|
|
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);
|
|
1106
1212
|
}
|
|
1107
1213
|
|
|
1108
|
-
fn specialDamage(battle: anytype, player: Player, move: Move.Data,
|
|
1214
|
+
fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
|
|
1109
1215
|
const side = battle.side(player);
|
|
1110
1216
|
const foe = battle.foe(player);
|
|
1111
1217
|
|
|
1112
|
-
if (!try checkHit(battle, player, move,
|
|
1218
|
+
if (!try checkHit(battle, player, move, options)) return null;
|
|
1219
|
+
try options.chance.commit(player, .hit);
|
|
1113
1220
|
|
|
1114
1221
|
battle.last_damage = switch (side.last_selected_move) {
|
|
1115
1222
|
.SuperFang => @max(foe.stored().hp / 2, 1),
|
|
@@ -1118,17 +1225,10 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
|
|
|
1118
1225
|
.DragonRage => 40,
|
|
1119
1226
|
// GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
|
|
1120
1227
|
.Psywave => power: {
|
|
1121
|
-
const max = @intCast(
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
// GLITCH: Psywave infinite glitch loop
|
|
1126
|
-
if (max <= 1) return Result.Error;
|
|
1127
|
-
while (true) {
|
|
1128
|
-
const r = battle.rng.next();
|
|
1129
|
-
if (r < max) break :power r;
|
|
1130
|
-
}
|
|
1131
|
-
}
|
|
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);
|
|
1132
1232
|
},
|
|
1133
1233
|
else => unreachable,
|
|
1134
1234
|
};
|
|
@@ -1136,57 +1236,54 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
|
|
|
1136
1236
|
if (battle.last_damage == 0) return if (showdown) null else Result.Error;
|
|
1137
1237
|
|
|
1138
1238
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1139
|
-
_ = try applyDamage(battle, player.foe(), player.foe(), .None,
|
|
1140
|
-
if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(),
|
|
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);
|
|
1141
1241
|
|
|
1142
1242
|
return null;
|
|
1143
1243
|
}
|
|
1144
1244
|
|
|
1145
|
-
fn counterDamage(battle: anytype, player: Player, move: Move.Data,
|
|
1245
|
+
fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
|
|
1146
1246
|
const foe = battle.foe(player);
|
|
1147
1247
|
|
|
1148
1248
|
if (battle.last_damage == 0) {
|
|
1149
|
-
try log.fail(battle.active(player), .None);
|
|
1249
|
+
try options.log.fail(battle.active(player), .None);
|
|
1150
1250
|
return null;
|
|
1151
1251
|
}
|
|
1152
1252
|
|
|
1153
|
-
// Pretend
|
|
1154
|
-
const foe_last_used_move =
|
|
1155
|
-
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
|
|
1156
1254
|
const foe_last_selected_move =
|
|
1157
1255
|
Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
|
|
1158
|
-
.
|
|
1256
|
+
.Splash
|
|
1159
1257
|
else
|
|
1160
1258
|
foe.last_selected_move);
|
|
1161
1259
|
|
|
1162
|
-
const used =
|
|
1163
|
-
foe.last_used_move != .Counter and
|
|
1164
|
-
(foe_last_used_move.type == .Normal or
|
|
1165
|
-
foe_last_used_move.type == .Fighting);
|
|
1166
|
-
|
|
1260
|
+
const used = battle.lastMove(player.foe()).counterable != 0;
|
|
1167
1261
|
const selected = foe_last_selected_move.bp > 0 and
|
|
1168
1262
|
foe.last_selected_move != .Counter and
|
|
1169
1263
|
(foe_last_selected_move.type == .Normal or
|
|
1170
1264
|
foe_last_selected_move.type == .Fighting);
|
|
1171
1265
|
|
|
1172
1266
|
if (!used and !selected) {
|
|
1173
|
-
try log.fail(battle.active(player), .None);
|
|
1267
|
+
try options.log.fail(battle.active(player), .None);
|
|
1174
1268
|
return null;
|
|
1175
1269
|
}
|
|
1176
1270
|
|
|
1177
1271
|
if (!used or !selected) {
|
|
1178
1272
|
// GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
|
|
1179
1273
|
if (!showdown) return Result.Error;
|
|
1180
|
-
try log.fail(battle.active(player), .None);
|
|
1274
|
+
try options.log.fail(battle.active(player), .None);
|
|
1181
1275
|
return null;
|
|
1182
1276
|
}
|
|
1183
1277
|
|
|
1184
|
-
battle.last_damage
|
|
1278
|
+
battle.last_damage *|= 2;
|
|
1185
1279
|
|
|
1186
1280
|
// Pokémon Showdown calls checkHit before Counter
|
|
1187
|
-
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);
|
|
1188
1283
|
|
|
1189
|
-
|
|
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);
|
|
1190
1287
|
return null;
|
|
1191
1288
|
}
|
|
1192
1289
|
|
|
@@ -1195,7 +1292,7 @@ fn applyDamage(
|
|
|
1195
1292
|
target_player: Player,
|
|
1196
1293
|
sub_player: Player,
|
|
1197
1294
|
reason: Damage,
|
|
1198
|
-
|
|
1295
|
+
options: anytype,
|
|
1199
1296
|
) !bool {
|
|
1200
1297
|
assert(showdown or battle.last_damage != 0);
|
|
1201
1298
|
|
|
@@ -1210,19 +1307,20 @@ fn applyDamage(
|
|
|
1210
1307
|
subbed.active.volatiles.substitute = 0;
|
|
1211
1308
|
subbed.active.volatiles.Substitute = false;
|
|
1212
1309
|
// battle.last_damage is not updated with the amount of HP the Substitute had
|
|
1213
|
-
try log.end(battle.active(sub_player), .Substitute);
|
|
1310
|
+
try options.log.end(battle.active(sub_player), .Substitute);
|
|
1311
|
+
options.calc.capped(target_player);
|
|
1214
1312
|
return true;
|
|
1215
1313
|
} else {
|
|
1216
1314
|
// Safe to truncate since less than subbed.volatiles.substitute which is a u8
|
|
1217
|
-
subbed.active.volatiles.substitute -= @intCast(
|
|
1218
|
-
try log.activate(battle.active(sub_player), .Substitute);
|
|
1315
|
+
subbed.active.volatiles.substitute -= @intCast(battle.last_damage);
|
|
1316
|
+
try options.log.activate(battle.active(sub_player), .Substitute);
|
|
1219
1317
|
return false;
|
|
1220
1318
|
}
|
|
1221
1319
|
}
|
|
1222
1320
|
|
|
1223
1321
|
if (battle.last_damage > target.stored().hp) battle.last_damage = target.stored().hp;
|
|
1224
1322
|
target.stored().hp -= battle.last_damage;
|
|
1225
|
-
try log.damage(battle.active(target_player), target.stored(), reason);
|
|
1323
|
+
try options.log.damage(battle.active(target_player), target.stored(), reason);
|
|
1226
1324
|
return false;
|
|
1227
1325
|
}
|
|
1228
1326
|
|
|
@@ -1230,73 +1328,78 @@ fn mirrorMove(
|
|
|
1230
1328
|
battle: anytype,
|
|
1231
1329
|
player: Player,
|
|
1232
1330
|
mslot: u4,
|
|
1331
|
+
rewrap: bool,
|
|
1233
1332
|
auto: bool,
|
|
1234
1333
|
residual: *bool,
|
|
1235
|
-
|
|
1236
|
-
) !?Result {
|
|
1334
|
+
options: anytype,
|
|
1335
|
+
) anyerror!?Result {
|
|
1237
1336
|
var side = battle.side(player);
|
|
1238
1337
|
const foe = battle.foe(player);
|
|
1239
1338
|
|
|
1240
1339
|
side.last_selected_move = foe.last_used_move;
|
|
1241
1340
|
|
|
1242
1341
|
if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
|
|
1243
|
-
try log.fail(battle.active(player), .None);
|
|
1342
|
+
try options.log.fail(battle.active(player), .None);
|
|
1244
1343
|
return null;
|
|
1344
|
+
} else if (!showdown or foe.last_used_move != .Struggle) {
|
|
1345
|
+
incrementPP(side, mslot);
|
|
1245
1346
|
}
|
|
1246
1347
|
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
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);
|
|
1251
1352
|
}
|
|
1252
1353
|
|
|
1253
1354
|
fn metronome(
|
|
1254
1355
|
battle: anytype,
|
|
1255
1356
|
player: Player,
|
|
1256
1357
|
mslot: u4,
|
|
1358
|
+
rewrap: bool,
|
|
1257
1359
|
auto: bool,
|
|
1258
1360
|
residual: *bool,
|
|
1259
|
-
|
|
1260
|
-
) !?Result {
|
|
1361
|
+
options: anytype,
|
|
1362
|
+
) anyerror!?Result {
|
|
1261
1363
|
var side = battle.side(player);
|
|
1262
1364
|
|
|
1263
|
-
side.last_selected_move =
|
|
1264
|
-
const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
|
|
1265
|
-
const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
|
|
1266
|
-
break :blk @intToEnum(Move, r + mod);
|
|
1267
|
-
} else loop: {
|
|
1268
|
-
while (true) {
|
|
1269
|
-
const r = battle.rng.next();
|
|
1270
|
-
if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
|
|
1271
|
-
if (r >= @enumToInt(Move.Struggle)) continue;
|
|
1272
|
-
break :loop @intToEnum(Move, r);
|
|
1273
|
-
}
|
|
1274
|
-
};
|
|
1275
|
-
|
|
1365
|
+
side.last_selected_move = try Rolls.metronome(battle, player, options);
|
|
1276
1366
|
incrementPP(side, mslot);
|
|
1277
1367
|
|
|
1278
|
-
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual,
|
|
1279
|
-
|
|
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);
|
|
1280
1372
|
}
|
|
1281
1373
|
|
|
1282
|
-
fn checkHit(battle: anytype, player: Player, move: Move.Data,
|
|
1374
|
+
fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !bool {
|
|
1283
1375
|
var immune = false;
|
|
1284
1376
|
var mist = false;
|
|
1285
|
-
|
|
1377
|
+
|
|
1378
|
+
if (moveHit(battle, player, move, &immune, &mist, options)) return true;
|
|
1379
|
+
|
|
1286
1380
|
assert(!immune);
|
|
1287
1381
|
if (mist) {
|
|
1288
1382
|
assert(!showdown);
|
|
1289
1383
|
const foe_ident = battle.active(player.foe());
|
|
1290
|
-
try log.activate(foe_ident, .Mist);
|
|
1291
|
-
try log.fail(foe_ident, .None);
|
|
1384
|
+
try options.log.activate(foe_ident, .Mist);
|
|
1385
|
+
try options.log.fail(foe_ident, .None);
|
|
1292
1386
|
} else {
|
|
1293
|
-
try
|
|
1294
|
-
try log.
|
|
1387
|
+
try options.chance.commit(player, .miss);
|
|
1388
|
+
try options.log.lastmiss();
|
|
1389
|
+
try options.log.miss(battle.active(player));
|
|
1295
1390
|
}
|
|
1391
|
+
|
|
1296
1392
|
return false;
|
|
1297
1393
|
}
|
|
1298
1394
|
|
|
1299
|
-
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 {
|
|
1300
1403
|
var side = battle.side(player);
|
|
1301
1404
|
const foe = battle.foe(player);
|
|
1302
1405
|
|
|
@@ -1330,23 +1433,24 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
|
|
|
1330
1433
|
}
|
|
1331
1434
|
|
|
1332
1435
|
// GLITCH: Thrash / Petal Dance / Rage get their accuracy overwritten on subsequent hits
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
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))];
|
|
1338
1443
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1339
|
-
boost = BOOSTS[@
|
|
1444
|
+
boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
|
|
1340
1445
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1341
1446
|
accuracy = @min(255, @max(1, accuracy));
|
|
1342
1447
|
|
|
1343
|
-
|
|
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;
|
|
1344
1451
|
|
|
1345
1452
|
// GLITCH: max accuracy is 255 so 1/256 chance of miss
|
|
1346
|
-
break :miss
|
|
1347
|
-
!battle.rng.chance(u8, @intCast(u8, accuracy), 256)
|
|
1348
|
-
else
|
|
1349
|
-
battle.rng.next() >= accuracy;
|
|
1453
|
+
break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
|
|
1350
1454
|
};
|
|
1351
1455
|
|
|
1352
1456
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1356,7 +1460,7 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
|
|
|
1356
1460
|
}
|
|
1357
1461
|
|
|
1358
1462
|
if (!miss) return true;
|
|
1359
|
-
battle.last_damage = 0;
|
|
1463
|
+
if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
|
|
1360
1464
|
side.active.volatiles.Binding = false;
|
|
1361
1465
|
return false;
|
|
1362
1466
|
}
|
|
@@ -1364,8 +1468,8 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
|
|
|
1364
1468
|
fn checkFaint(
|
|
1365
1469
|
battle: anytype,
|
|
1366
1470
|
player: Player,
|
|
1367
|
-
|
|
1368
|
-
) @TypeOf(log).Error!?Result {
|
|
1471
|
+
options: anytype,
|
|
1472
|
+
) @TypeOf(options.log).Error!?Result {
|
|
1369
1473
|
const side = battle.side(player);
|
|
1370
1474
|
if (side.stored().hp > 0) return null;
|
|
1371
1475
|
|
|
@@ -1377,20 +1481,20 @@ fn checkFaint(
|
|
|
1377
1481
|
const tie = player_out and foe_out;
|
|
1378
1482
|
const more = tie or player_out or foe_out;
|
|
1379
1483
|
|
|
1380
|
-
if (try faint(battle, player,
|
|
1381
|
-
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;
|
|
1382
1486
|
|
|
1383
1487
|
assert(!side.active.volatiles.MultiHit);
|
|
1384
1488
|
assert(!foe.active.volatiles.MultiHit);
|
|
1385
1489
|
|
|
1386
1490
|
if (tie) {
|
|
1387
|
-
try log.tie();
|
|
1491
|
+
try options.log.tie();
|
|
1388
1492
|
return Result.Tie;
|
|
1389
1493
|
} else if (player_out) {
|
|
1390
|
-
try log.win(player.foe());
|
|
1494
|
+
try options.log.win(player.foe());
|
|
1391
1495
|
return if (player == .P1) Result.Lose else Result.Win;
|
|
1392
1496
|
} else if (foe_out) {
|
|
1393
|
-
try log.win(player);
|
|
1497
|
+
try options.log.win(player);
|
|
1394
1498
|
return if (player == .P1) Result.Win else Result.Lose;
|
|
1395
1499
|
}
|
|
1396
1500
|
|
|
@@ -1399,7 +1503,7 @@ fn checkFaint(
|
|
|
1399
1503
|
return Result{ .p1 = foe_choice, .p2 = .Switch };
|
|
1400
1504
|
}
|
|
1401
1505
|
|
|
1402
|
-
fn faint(battle: anytype, player: Player,
|
|
1506
|
+
fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
|
|
1403
1507
|
var side = battle.side(player);
|
|
1404
1508
|
var foe = battle.foe(player);
|
|
1405
1509
|
assert(side.stored().hp == 0);
|
|
@@ -1432,11 +1536,12 @@ fn faint(battle: anytype, player: Player, log: anytype, done: bool) !?Result {
|
|
|
1432
1536
|
side.stored().stats.spe;
|
|
1433
1537
|
}
|
|
1434
1538
|
|
|
1435
|
-
try log.faint(battle.active(player), done);
|
|
1539
|
+
try options.log.faint(battle.active(player), done);
|
|
1540
|
+
options.calc.capped(player);
|
|
1436
1541
|
return null;
|
|
1437
1542
|
}
|
|
1438
1543
|
|
|
1439
|
-
fn handleResidual(battle: anytype, player: Player,
|
|
1544
|
+
fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
|
|
1440
1545
|
var side = battle.side(player);
|
|
1441
1546
|
var stored = side.stored();
|
|
1442
1547
|
const ident = battle.active(player);
|
|
@@ -1457,7 +1562,7 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
|
|
|
1457
1562
|
stored.hp -= amount;
|
|
1458
1563
|
|
|
1459
1564
|
// Pokémon Showdown uses damageOf here but its not relevant in Generation I
|
|
1460
|
-
try log.damage(ident, stored, if (brn) Damage.Burn else Damage.Poison);
|
|
1565
|
+
try options.log.damage(ident, stored, if (brn) Damage.Burn else Damage.Poison);
|
|
1461
1566
|
}
|
|
1462
1567
|
|
|
1463
1568
|
if (volatiles.LeechSeed) {
|
|
@@ -1478,49 +1583,46 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
|
|
|
1478
1583
|
damage *= volatiles.toxic;
|
|
1479
1584
|
}
|
|
1480
1585
|
|
|
1481
|
-
// Pokémon Showdown erroneously updates last damage with uncapped Leech Seed damage
|
|
1482
|
-
if (showdown) battle.last_damage = damage;
|
|
1483
|
-
|
|
1484
1586
|
const amount = @min(damage, stored.hp);
|
|
1485
1587
|
stored.hp -= amount;
|
|
1486
1588
|
|
|
1487
1589
|
// As above, Pokémon Showdown uses damageOf but its not relevant
|
|
1488
|
-
if (amount > 0) try log.damage(ident, stored, .LeechSeed);
|
|
1590
|
+
if (amount > 0) try options.log.damage(ident, stored, .LeechSeed);
|
|
1489
1591
|
|
|
1490
1592
|
const before = foe_stored.hp;
|
|
1491
1593
|
// Uncapped damage is added back to the foe
|
|
1492
1594
|
foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
|
|
1493
1595
|
// Pokémon Showdown uses the less specific heal here instead of drain... because reasons?
|
|
1494
|
-
if (foe_stored.hp > before) try log.heal(foe_ident, foe_stored, .Silent);
|
|
1596
|
+
if (foe_stored.hp > before) try options.log.heal(foe_ident, foe_stored, .Silent);
|
|
1495
1597
|
}
|
|
1496
1598
|
}
|
|
1497
1599
|
|
|
1498
|
-
fn endTurn(battle: anytype,
|
|
1600
|
+
fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result {
|
|
1499
1601
|
assert(!battle.side(.P1).active.volatiles.MultiHit);
|
|
1500
1602
|
assert(!battle.side(.P2).active.volatiles.MultiHit);
|
|
1501
1603
|
|
|
1502
|
-
|
|
1503
|
-
|
|
1604
|
+
battle.turn += 1;
|
|
1605
|
+
|
|
1606
|
+
if (showdown and pkmn.options.ebc and checkEBC(battle)) {
|
|
1607
|
+
try options.log.tie();
|
|
1504
1608
|
return Result.Tie;
|
|
1505
1609
|
}
|
|
1506
1610
|
|
|
1507
|
-
battle.turn += 1;
|
|
1508
|
-
|
|
1509
1611
|
if (showdown and battle.turn >= 1000) {
|
|
1510
|
-
try log.tie();
|
|
1612
|
+
try options.log.tie();
|
|
1511
1613
|
return Result.Tie;
|
|
1512
1614
|
} else if (battle.turn >= 65535) {
|
|
1513
1615
|
return Result.Error;
|
|
1514
1616
|
}
|
|
1515
1617
|
|
|
1516
|
-
try log.turn(battle.turn);
|
|
1618
|
+
try options.log.turn(battle.turn);
|
|
1517
1619
|
|
|
1518
1620
|
return Result.Default;
|
|
1519
1621
|
}
|
|
1520
1622
|
|
|
1521
1623
|
fn checkEBC(battle: anytype) bool {
|
|
1522
1624
|
for (battle.sides, 0..) |side, i| {
|
|
1523
|
-
const foe = battle.sides[~@
|
|
1625
|
+
const foe = battle.sides[~@as(u1, @intCast(i))];
|
|
1524
1626
|
|
|
1525
1627
|
var foe_all_ghosts = true;
|
|
1526
1628
|
var foe_all_transform = true;
|
|
@@ -1572,48 +1674,84 @@ fn checkEBC(battle: anytype) bool {
|
|
|
1572
1674
|
return false;
|
|
1573
1675
|
}
|
|
1574
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
|
+
|
|
1575
1713
|
inline fn onBegin(
|
|
1576
1714
|
battle: anytype,
|
|
1577
1715
|
player: Player,
|
|
1578
1716
|
move: Move.Data,
|
|
1579
1717
|
mslot: u8,
|
|
1580
1718
|
residual: *bool,
|
|
1581
|
-
|
|
1719
|
+
options: anytype,
|
|
1582
1720
|
) !void {
|
|
1583
1721
|
assert(move.effect.onBegin());
|
|
1584
1722
|
return switch (move.effect) {
|
|
1585
|
-
.Confusion => Effects.confusion(battle, player, move,
|
|
1586
|
-
.Conversion => Effects.conversion(battle, player,
|
|
1587
|
-
.FocusEnergy => Effects.focusEnergy(battle, player,
|
|
1588
|
-
.Haze => Effects.haze(battle, player,
|
|
1589
|
-
.Heal => Effects.heal(battle, player,
|
|
1590
|
-
.LeechSeed => Effects.leechSeed(battle, player, move,
|
|
1591
|
-
.LightScreen => Effects.lightScreen(battle, player,
|
|
1592
|
-
.Mimic => Effects.mimic(battle, player, move, mslot,
|
|
1593
|
-
.Mist => Effects.mist(battle, player,
|
|
1594
|
-
.Paralyze => Effects.paralyze(battle, player, move,
|
|
1595
|
-
.Poison => Effects.poison(battle, player, move,
|
|
1596
|
-
.Reflect => Effects.reflect(battle, player,
|
|
1597
|
-
.Splash => Effects.splash(battle, player,
|
|
1598
|
-
.Substitute => Effects.substitute(battle, player, residual,
|
|
1599
|
-
.SwitchAndTeleport => Effects.switchAndTeleport(battle, player, move,
|
|
1600
|
-
.Transform => Effects.transform(battle, player,
|
|
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),
|
|
1601
1739
|
else => unreachable,
|
|
1602
1740
|
};
|
|
1603
1741
|
}
|
|
1604
1742
|
|
|
1605
|
-
inline fn onEnd(battle: anytype, player: Player, move: Move.Data,
|
|
1743
|
+
inline fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1606
1744
|
assert(move.effect.onEnd());
|
|
1607
1745
|
return switch (move.effect) {
|
|
1608
|
-
.Bide => Effects.bide(battle, player,
|
|
1746
|
+
.Bide => Effects.bide(battle, player, options),
|
|
1609
1747
|
// zig fmt: off
|
|
1610
1748
|
.AttackUp1, .AttackUp2, .DefenseUp1, .DefenseUp2,
|
|
1611
1749
|
.EvasionUp1, .SpecialUp1, .SpecialUp2, .SpeedUp2 =>
|
|
1612
|
-
Effects.boost(battle, player, move,
|
|
1750
|
+
Effects.boost(battle, player, move, options),
|
|
1613
1751
|
.AccuracyDown1, .AttackDown1, .DefenseDown1, .DefenseDown2, .SpeedDown1 =>
|
|
1614
|
-
Effects.unboost(battle, player, move,
|
|
1752
|
+
Effects.unboost(battle, player, move, options),
|
|
1615
1753
|
// zig fmt: on
|
|
1616
|
-
.Sleep => Effects.sleep(battle, player, move,
|
|
1754
|
+
.Sleep => Effects.sleep(battle, player, move, options),
|
|
1617
1755
|
else => unreachable,
|
|
1618
1756
|
};
|
|
1619
1757
|
}
|
|
@@ -1623,54 +1761,51 @@ inline fn alwaysHappens(
|
|
|
1623
1761
|
player: Player,
|
|
1624
1762
|
move: Move.Data,
|
|
1625
1763
|
residual: *bool,
|
|
1626
|
-
|
|
1764
|
+
options: anytype,
|
|
1627
1765
|
) !void {
|
|
1628
1766
|
assert(move.effect.alwaysHappens());
|
|
1629
1767
|
return switch (move.effect) {
|
|
1630
|
-
.DrainHP, .DreamEater => Effects.drainHP(battle, player,
|
|
1768
|
+
.DrainHP, .DreamEater => Effects.drainHP(battle, player, options),
|
|
1631
1769
|
.Explode => Effects.explode(battle, player),
|
|
1632
|
-
.PayDay => Effects.payDay(battle, player,
|
|
1770
|
+
.PayDay => Effects.payDay(battle, player, options),
|
|
1633
1771
|
.Rage => Effects.rage(battle, player),
|
|
1634
|
-
.Recoil => Effects.recoil(battle, player, residual,
|
|
1772
|
+
.Recoil => Effects.recoil(battle, player, residual, options),
|
|
1635
1773
|
.JumpKick, .Binding => {},
|
|
1636
1774
|
else => unreachable,
|
|
1637
1775
|
};
|
|
1638
1776
|
}
|
|
1639
1777
|
|
|
1640
|
-
inline fn moveEffect(battle: anytype, player: Player, move: Move.Data,
|
|
1778
|
+
inline fn moveEffect(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1641
1779
|
return switch (move.effect) {
|
|
1642
|
-
.BurnChance1, .BurnChance2 => Effects.burnChance(battle, player, move,
|
|
1643
|
-
.ConfusionChance => Effects.confusion(battle, player, move,
|
|
1644
|
-
.FlinchChance1, .FlinchChance2 => Effects.flinchChance(battle, player, move),
|
|
1645
|
-
.FreezeChance => Effects.freezeChance(battle, player, move,
|
|
1646
|
-
.HyperBeam => Effects.hyperBeam(battle, player,
|
|
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),
|
|
1647
1785
|
.MultiHit, .DoubleHit, .Twineedle => unreachable,
|
|
1648
|
-
.ParalyzeChance1, .ParalyzeChance2 => Effects.paralyzeChance(battle, player, move,
|
|
1649
|
-
.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),
|
|
1650
1788
|
// zig fmt: off
|
|
1651
1789
|
.AttackDownChance, .DefenseDownChance, .SpecialDownChance, .SpeedDownChance =>
|
|
1652
|
-
Effects.unboost(battle, player, move,
|
|
1790
|
+
Effects.unboost(battle, player, move, options),
|
|
1653
1791
|
// zig fmt: on
|
|
1654
1792
|
else => {},
|
|
1655
1793
|
};
|
|
1656
1794
|
}
|
|
1657
1795
|
|
|
1658
1796
|
pub const Effects = struct {
|
|
1659
|
-
fn bide(battle: anytype, player: Player,
|
|
1797
|
+
fn bide(battle: anytype, player: Player, options: anytype) !void {
|
|
1660
1798
|
var side = battle.side(player);
|
|
1661
1799
|
|
|
1662
1800
|
side.active.volatiles.Bide = true;
|
|
1663
1801
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1664
1802
|
side.active.volatiles.state = 0;
|
|
1665
|
-
side.active.volatiles.attacks =
|
|
1666
|
-
battle.rng.range(u4, 2, 4)
|
|
1667
|
-
else
|
|
1668
|
-
(battle.rng.next() & 1) + 2);
|
|
1803
|
+
side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
1669
1804
|
|
|
1670
|
-
try log.start(battle.active(player), .Bide);
|
|
1805
|
+
try options.log.start(battle.active(player), .Bide);
|
|
1671
1806
|
}
|
|
1672
1807
|
|
|
1673
|
-
fn burnChance(battle: anytype, player: Player, move: Move.Data,
|
|
1808
|
+
fn burnChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1674
1809
|
var foe = battle.foe(player);
|
|
1675
1810
|
var foe_stored = foe.stored();
|
|
1676
1811
|
|
|
@@ -1681,96 +1816,97 @@ pub const Effects = struct {
|
|
|
1681
1816
|
// GLITCH: Freeze top move selection desync can occur if thawed Pokémon is slower
|
|
1682
1817
|
if (Status.is(foe_stored.status, .FRZ)) {
|
|
1683
1818
|
assert(move.type == .Fire);
|
|
1684
|
-
try log.curestatus(
|
|
1819
|
+
try options.log.curestatus(
|
|
1820
|
+
battle.active(player.foe()),
|
|
1821
|
+
foe_stored.status,
|
|
1822
|
+
.Message,
|
|
1823
|
+
);
|
|
1685
1824
|
foe_stored.status = 0;
|
|
1686
1825
|
}
|
|
1687
1826
|
return;
|
|
1688
1827
|
}
|
|
1689
1828
|
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
Gen12.percent(10)
|
|
1695
|
-
else
|
|
1696
|
-
Gen12.percent(30));
|
|
1697
|
-
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
|
+
}
|
|
1698
1833
|
|
|
1699
1834
|
foe_stored.status = Status.init(.BRN);
|
|
1700
1835
|
foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
|
|
1701
1836
|
|
|
1702
|
-
try log.status(battle.active(player.foe()), foe_stored.status, .None);
|
|
1837
|
+
try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
|
|
1703
1838
|
}
|
|
1704
1839
|
|
|
1705
|
-
fn charge(battle: anytype, player: Player,
|
|
1840
|
+
fn charge(battle: anytype, player: Player, options: anytype) !void {
|
|
1706
1841
|
var side = battle.side(player);
|
|
1707
1842
|
var volatiles = &side.active.volatiles;
|
|
1708
1843
|
|
|
1709
1844
|
volatiles.Charging = true;
|
|
1710
1845
|
const move = side.last_selected_move;
|
|
1711
1846
|
if (move == .Fly or move == .Dig) volatiles.Invulnerable = true;
|
|
1712
|
-
try log.laststill();
|
|
1713
|
-
try log.prepare(battle.active(player), move);
|
|
1847
|
+
try options.log.laststill();
|
|
1848
|
+
try options.log.prepare(battle.active(player), move);
|
|
1714
1849
|
}
|
|
1715
1850
|
|
|
1716
|
-
fn confusion(battle: anytype, player: Player, move: Move.Data,
|
|
1851
|
+
fn confusion(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1717
1852
|
var foe = battle.foe(player);
|
|
1718
1853
|
const sub = foe.active.volatiles.Substitute;
|
|
1719
1854
|
|
|
1720
1855
|
if (move.effect == .ConfusionChance) {
|
|
1721
|
-
|
|
1722
|
-
battle.rng.chance(u8, 25, 256)
|
|
1723
|
-
else
|
|
1724
|
-
battle.rng.next() < Gen12.percent(10);
|
|
1725
|
-
if (!chance) return;
|
|
1856
|
+
if (!try Rolls.confusionChance(battle, player, options)) return;
|
|
1726
1857
|
} else {
|
|
1727
1858
|
if (showdown) {
|
|
1728
|
-
if (!try checkHit(battle, player, move,
|
|
1859
|
+
if (!try checkHit(battle, player, move, options)) {
|
|
1729
1860
|
return;
|
|
1730
1861
|
} else if (sub) {
|
|
1731
|
-
return log.fail(battle.active(player.foe()), .None);
|
|
1862
|
+
return options.log.fail(battle.active(player.foe()), .None);
|
|
1732
1863
|
}
|
|
1733
1864
|
} else {
|
|
1734
1865
|
if (sub) {
|
|
1735
|
-
return log.fail(battle.active(player.foe()), .None);
|
|
1736
|
-
} 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)) {
|
|
1737
1868
|
return;
|
|
1738
1869
|
}
|
|
1739
1870
|
}
|
|
1871
|
+
try options.chance.commit(player, .hit);
|
|
1740
1872
|
}
|
|
1741
1873
|
|
|
1742
1874
|
if (foe.active.volatiles.Confusion) return;
|
|
1743
1875
|
foe.active.volatiles.Confusion = true;
|
|
1744
|
-
foe.active.volatiles.confusion =
|
|
1745
|
-
battle.rng.range(u8, 2, 6)
|
|
1746
|
-
else
|
|
1747
|
-
(battle.rng.next() & 3) + 2);
|
|
1876
|
+
foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
|
|
1748
1877
|
|
|
1749
|
-
try log.start(battle.active(player.foe()), .Confusion);
|
|
1878
|
+
try options.log.start(battle.active(player.foe()), .Confusion);
|
|
1750
1879
|
}
|
|
1751
1880
|
|
|
1752
|
-
fn conversion(battle: anytype, player: Player,
|
|
1881
|
+
fn conversion(battle: anytype, player: Player, options: anytype) !void {
|
|
1753
1882
|
const foe = battle.foe(player);
|
|
1754
1883
|
|
|
1755
1884
|
if (foe.active.volatiles.Invulnerable) {
|
|
1756
|
-
try log.lastmiss();
|
|
1757
|
-
return log.miss(battle.active(player));
|
|
1885
|
+
try options.log.lastmiss();
|
|
1886
|
+
return options.log.miss(battle.active(player));
|
|
1758
1887
|
}
|
|
1759
1888
|
|
|
1760
1889
|
battle.side(player).active.types = foe.active.types;
|
|
1761
|
-
return log.typechange(
|
|
1890
|
+
return options.log.typechange(
|
|
1891
|
+
battle.active(player),
|
|
1892
|
+
foe.active.types,
|
|
1893
|
+
battle.active(player.foe()),
|
|
1894
|
+
);
|
|
1762
1895
|
}
|
|
1763
1896
|
|
|
1764
|
-
fn disable(battle: anytype, player: Player, move: Move.Data,
|
|
1897
|
+
fn disable(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
|
|
1765
1898
|
var foe = battle.foe(player);
|
|
1766
1899
|
var volatiles = &foe.active.volatiles;
|
|
1767
1900
|
const foe_ident = battle.active(player.foe());
|
|
1768
1901
|
|
|
1769
1902
|
// Pokémon Showdown handles hit/miss earlier in doMove
|
|
1770
|
-
if (!showdown
|
|
1903
|
+
if (!showdown) {
|
|
1904
|
+
if (!try checkHit(battle, player, move, options)) return null;
|
|
1905
|
+
try options.chance.commit(player, .hit);
|
|
1906
|
+
}
|
|
1771
1907
|
|
|
1772
1908
|
if (volatiles.disabled_move != 0) {
|
|
1773
|
-
try log.fail(foe_ident, .None);
|
|
1909
|
+
try options.log.fail(foe_ident, .None);
|
|
1774
1910
|
return null;
|
|
1775
1911
|
}
|
|
1776
1912
|
|
|
@@ -1786,7 +1922,7 @@ pub const Effects = struct {
|
|
|
1786
1922
|
// Technically this is still considered simply a "miss" on the cartridge,
|
|
1787
1923
|
// but diverging from Pokémon Showdown here would mostly just be pedantic
|
|
1788
1924
|
if (n == 0) {
|
|
1789
|
-
try log.fail(foe_ident, .None);
|
|
1925
|
+
try options.log.fail(foe_ident, .None);
|
|
1790
1926
|
return null;
|
|
1791
1927
|
} else if (err) {
|
|
1792
1928
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
@@ -1794,23 +1930,22 @@ pub const Effects = struct {
|
|
|
1794
1930
|
return Result.Error;
|
|
1795
1931
|
}
|
|
1796
1932
|
|
|
1797
|
-
volatiles.disabled_move =
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
else
|
|
1801
|
-
(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);
|
|
1802
1936
|
|
|
1803
|
-
|
|
1937
|
+
const id = foe.active.move(volatiles.disabled_move).id;
|
|
1938
|
+
try options.log.startEffect(foe_ident, .Disable, id);
|
|
1804
1939
|
return null;
|
|
1805
1940
|
}
|
|
1806
1941
|
|
|
1807
|
-
fn drainHP(battle: anytype, player: Player,
|
|
1942
|
+
fn drainHP(battle: anytype, player: Player, options: anytype) !void {
|
|
1808
1943
|
var side = battle.side(player);
|
|
1809
1944
|
var stored = side.stored();
|
|
1810
1945
|
|
|
1811
1946
|
if (battle.last_damage == 0) {
|
|
1812
1947
|
assert(showdown);
|
|
1813
|
-
return;
|
|
1948
|
+
if (!battle.foe(player).active.volatiles.Substitute) return;
|
|
1814
1949
|
}
|
|
1815
1950
|
|
|
1816
1951
|
const drain = @max(battle.last_damage / 2, 1);
|
|
@@ -1819,7 +1954,7 @@ pub const Effects = struct {
|
|
|
1819
1954
|
if (stored.hp == stored.stats.hp) return;
|
|
1820
1955
|
stored.hp = @min(stored.stats.hp, stored.hp + drain);
|
|
1821
1956
|
|
|
1822
|
-
try log.drain(battle.active(player), stored, battle.active(player.foe()));
|
|
1957
|
+
try options.log.drain(battle.active(player), stored, battle.active(player.foe()));
|
|
1823
1958
|
}
|
|
1824
1959
|
|
|
1825
1960
|
fn explode(battle: anytype, player: Player) !void {
|
|
@@ -1833,34 +1968,28 @@ pub const Effects = struct {
|
|
|
1833
1968
|
side.active.volatiles.LeechSeed = false;
|
|
1834
1969
|
}
|
|
1835
1970
|
|
|
1836
|
-
fn flinchChance(battle: anytype, player: Player, move: Move.Data) void {
|
|
1971
|
+
fn flinchChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1837
1972
|
var volatiles = &battle.foe(player).active.volatiles;
|
|
1838
1973
|
|
|
1839
1974
|
if (volatiles.Substitute) return;
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
else
|
|
1844
|
-
battle.rng.next() < 1 + (if (move.effect == .FlinchChance1)
|
|
1845
|
-
Gen12.percent(10)
|
|
1846
|
-
else
|
|
1847
|
-
Gen12.percent(30));
|
|
1848
|
-
if (!chance) return;
|
|
1975
|
+
if (!try Rolls.secondaryChance(battle, player, move.effect == .FlinchChance1, options)) {
|
|
1976
|
+
return;
|
|
1977
|
+
}
|
|
1849
1978
|
|
|
1850
1979
|
volatiles.Flinch = true;
|
|
1851
1980
|
volatiles.Recharging = false;
|
|
1852
1981
|
}
|
|
1853
1982
|
|
|
1854
|
-
fn focusEnergy(battle: anytype, player: Player,
|
|
1983
|
+
fn focusEnergy(battle: anytype, player: Player, options: anytype) !void {
|
|
1855
1984
|
var side = battle.side(player);
|
|
1856
1985
|
|
|
1857
1986
|
if (side.active.volatiles.FocusEnergy) return;
|
|
1858
1987
|
side.active.volatiles.FocusEnergy = true;
|
|
1859
1988
|
|
|
1860
|
-
try log.start(battle.active(player), .FocusEnergy);
|
|
1989
|
+
try options.log.start(battle.active(player), .FocusEnergy);
|
|
1861
1990
|
}
|
|
1862
1991
|
|
|
1863
|
-
fn freezeChance(battle: anytype, player: Player, move: Move.Data,
|
|
1992
|
+
fn freezeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1864
1993
|
var foe = battle.foe(player);
|
|
1865
1994
|
var foe_stored = foe.stored();
|
|
1866
1995
|
const foe_ident = battle.active(player.foe());
|
|
@@ -1871,22 +2000,19 @@ pub const Effects = struct {
|
|
|
1871
2000
|
return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
|
|
1872
2001
|
}
|
|
1873
2002
|
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
else
|
|
1877
|
-
battle.rng.next() < 1 + Gen12.percent(10);
|
|
1878
|
-
if (!chance) return;
|
|
1879
|
-
|
|
2003
|
+
if (foe.active.types.includes(move.type)) return;
|
|
2004
|
+
if (!try Rolls.secondaryChance(battle, player, true, options)) return;
|
|
1880
2005
|
// Freeze Clause Mod
|
|
1881
2006
|
if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
1882
2007
|
|
|
1883
2008
|
foe_stored.status = Status.init(.FRZ);
|
|
1884
2009
|
// GLITCH: Hyper Beam recharging status is not cleared
|
|
1885
2010
|
|
|
1886
|
-
try log.status(foe_ident, foe_stored.status, .None);
|
|
2011
|
+
try options.log.status(foe_ident, foe_stored.status, .None);
|
|
1887
2012
|
}
|
|
1888
2013
|
|
|
1889
|
-
fn haze(battle: anytype, player: Player,
|
|
2014
|
+
fn haze(battle: anytype, player: Player, options: anytype) !void {
|
|
2015
|
+
var log = options.log;
|
|
1890
2016
|
var side = battle.side(player);
|
|
1891
2017
|
var foe = battle.foe(player);
|
|
1892
2018
|
|
|
@@ -1908,7 +2034,7 @@ pub const Effects = struct {
|
|
|
1908
2034
|
// Pokémon Showdown clears P1 then P2 instead of status -> side -> foe
|
|
1909
2035
|
if (showdown) {
|
|
1910
2036
|
for (&battle.sides, 0..) |*s, i| {
|
|
1911
|
-
const p = @
|
|
2037
|
+
const p: Player = @enumFromInt(i);
|
|
1912
2038
|
// Pokémon Showdown incorrectly does not prevent sleep/freeze from moving
|
|
1913
2039
|
if (p != player and Status.any(s.stored().status)) {
|
|
1914
2040
|
try log.curestatus(foe_ident, foe_stored.status, .Silent);
|
|
@@ -1917,7 +2043,7 @@ pub const Effects = struct {
|
|
|
1917
2043
|
s.stored().status = Status.init(.PSN);
|
|
1918
2044
|
try log.status(battle.active(p), s.stored().status, .None);
|
|
1919
2045
|
}
|
|
1920
|
-
try clearVolatiles(battle, p,
|
|
2046
|
+
try clearVolatiles(battle, p, p != player, options);
|
|
1921
2047
|
}
|
|
1922
2048
|
} else {
|
|
1923
2049
|
if (Status.any(foe_stored.status)) {
|
|
@@ -1927,71 +2053,76 @@ pub const Effects = struct {
|
|
|
1927
2053
|
try log.curestatus(foe_ident, foe_stored.status, .Silent);
|
|
1928
2054
|
foe_stored.status = 0;
|
|
1929
2055
|
}
|
|
1930
|
-
try clearVolatiles(battle, player,
|
|
1931
|
-
try clearVolatiles(battle, player.foe(),
|
|
2056
|
+
try clearVolatiles(battle, player, false, options);
|
|
2057
|
+
try clearVolatiles(battle, player.foe(), true, options);
|
|
1932
2058
|
}
|
|
1933
2059
|
}
|
|
1934
2060
|
|
|
1935
|
-
fn heal(battle: anytype, player: Player,
|
|
2061
|
+
fn heal(battle: anytype, player: Player, options: anytype) !void {
|
|
1936
2062
|
var side = battle.side(player);
|
|
1937
2063
|
var stored = side.stored();
|
|
1938
2064
|
const ident = battle.active(player);
|
|
1939
2065
|
|
|
1940
2066
|
// GLITCH: HP recovery move failure glitches
|
|
1941
2067
|
const delta = stored.stats.hp - stored.hp;
|
|
1942
|
-
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
|
+
}
|
|
1943
2071
|
|
|
1944
2072
|
const rest = side.last_selected_move == .Rest;
|
|
1945
2073
|
if (rest) {
|
|
1946
2074
|
// Adding the sleep status runs the sleep condition handler to roll duration
|
|
1947
2075
|
if (showdown) battle.rng.advance(1);
|
|
1948
2076
|
stored.status = Status.slf(2);
|
|
1949
|
-
try log.statusFrom(ident, stored.status, Move.Rest);
|
|
2077
|
+
try options.log.statusFrom(ident, stored.status, Move.Rest);
|
|
1950
2078
|
stored.hp = stored.stats.hp;
|
|
1951
2079
|
} else {
|
|
1952
2080
|
stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
|
|
1953
2081
|
}
|
|
1954
|
-
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);
|
|
1955
2083
|
}
|
|
1956
2084
|
|
|
1957
|
-
fn hyperBeam(battle: anytype, player: Player,
|
|
2085
|
+
fn hyperBeam(battle: anytype, player: Player, options: anytype) !void {
|
|
1958
2086
|
battle.side(player).active.volatiles.Recharging = true;
|
|
1959
|
-
try log.mustrecharge(battle.active(player));
|
|
2087
|
+
try options.log.mustrecharge(battle.active(player));
|
|
1960
2088
|
}
|
|
1961
2089
|
|
|
1962
|
-
fn leechSeed(battle: anytype, player: Player, move: Move.Data,
|
|
2090
|
+
fn leechSeed(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1963
2091
|
var foe = battle.foe(player);
|
|
1964
2092
|
|
|
1965
2093
|
if (showdown) {
|
|
1966
2094
|
// Invulnerability trumps type immunity on Pokémon Showdown
|
|
1967
2095
|
if (!foe.active.volatiles.Invulnerable and foe.active.types.includes(.Grass)) {
|
|
1968
|
-
return log.immune(battle.active(player.foe()), .None);
|
|
2096
|
+
return options.log.immune(battle.active(player.foe()), .None);
|
|
1969
2097
|
}
|
|
1970
|
-
if (!try checkHit(battle, player, move,
|
|
2098
|
+
if (!try checkHit(battle, player, move, options)) return;
|
|
1971
2099
|
if (foe.active.volatiles.LeechSeed) return;
|
|
1972
2100
|
} else {
|
|
1973
|
-
if (!try checkHit(battle, player, move,
|
|
2101
|
+
if (!try checkHit(battle, player, move, options)) return;
|
|
1974
2102
|
if (foe.active.types.includes(.Grass) or foe.active.volatiles.LeechSeed) {
|
|
1975
|
-
try log.lastmiss();
|
|
1976
|
-
return log.miss(battle.active(player));
|
|
2103
|
+
try options.log.lastmiss();
|
|
2104
|
+
return options.log.miss(battle.active(player));
|
|
1977
2105
|
}
|
|
1978
2106
|
}
|
|
1979
2107
|
|
|
2108
|
+
try options.chance.commit(player, .hit);
|
|
1980
2109
|
foe.active.volatiles.LeechSeed = true;
|
|
1981
2110
|
|
|
1982
|
-
try log.start(battle.active(player.foe()), .LeechSeed);
|
|
2111
|
+
try options.log.start(battle.active(player.foe()), .LeechSeed);
|
|
1983
2112
|
}
|
|
1984
2113
|
|
|
1985
|
-
fn lightScreen(battle: anytype, player: Player,
|
|
2114
|
+
fn lightScreen(battle: anytype, player: Player, options: anytype) !void {
|
|
1986
2115
|
var side = battle.side(player);
|
|
1987
2116
|
|
|
1988
|
-
if (side.active.volatiles.LightScreen)
|
|
2117
|
+
if (side.active.volatiles.LightScreen) {
|
|
2118
|
+
return options.log.fail(battle.active(player), .None);
|
|
2119
|
+
}
|
|
1989
2120
|
side.active.volatiles.LightScreen = true;
|
|
1990
2121
|
|
|
1991
|
-
try log.start(battle.active(player), .LightScreen);
|
|
2122
|
+
try options.log.start(battle.active(player), .LightScreen);
|
|
1992
2123
|
}
|
|
1993
2124
|
|
|
1994
|
-
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 {
|
|
1995
2126
|
var side = battle.side(player);
|
|
1996
2127
|
var foe = battle.foe(player);
|
|
1997
2128
|
|
|
@@ -2007,45 +2138,51 @@ pub const Effects = struct {
|
|
|
2007
2138
|
const has_mimic = has_mimic: {
|
|
2008
2139
|
for (side.active.moves, 0..) |m, i| {
|
|
2009
2140
|
if (m.id == .Mimic) {
|
|
2010
|
-
oslot = @intCast(
|
|
2141
|
+
oslot = @intCast(i + 1);
|
|
2011
2142
|
break :has_mimic true;
|
|
2012
2143
|
}
|
|
2013
2144
|
}
|
|
2014
2145
|
break :has_mimic false;
|
|
2015
2146
|
};
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
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);
|
|
2020
2152
|
}
|
|
2021
2153
|
}
|
|
2022
|
-
if (!try checkHit(battle, player, move,
|
|
2154
|
+
if (!try checkHit(battle, player, move, options)) return;
|
|
2155
|
+
try options.chance.commit(player, .hit);
|
|
2023
2156
|
|
|
2024
|
-
const rslot =
|
|
2157
|
+
const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
|
|
2025
2158
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
2026
2159
|
|
|
2027
|
-
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);
|
|
2028
2161
|
}
|
|
2029
2162
|
|
|
2030
|
-
fn mist(battle: anytype, player: Player,
|
|
2163
|
+
fn mist(battle: anytype, player: Player, options: anytype) !void {
|
|
2031
2164
|
var side = battle.side(player);
|
|
2032
2165
|
|
|
2033
2166
|
if (side.active.volatiles.Mist) return;
|
|
2034
2167
|
side.active.volatiles.Mist = true;
|
|
2035
2168
|
|
|
2036
|
-
try log.start(battle.active(player), .Mist);
|
|
2169
|
+
try options.log.start(battle.active(player), .Mist);
|
|
2037
2170
|
}
|
|
2038
2171
|
|
|
2039
|
-
fn multiHit(battle: anytype, player: Player, move: Move.Data) void {
|
|
2172
|
+
fn multiHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
2040
2173
|
var side = battle.side(player);
|
|
2041
2174
|
|
|
2042
2175
|
assert(!side.active.volatiles.MultiHit);
|
|
2043
2176
|
side.active.volatiles.MultiHit = true;
|
|
2044
2177
|
|
|
2045
|
-
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;
|
|
2046
2182
|
}
|
|
2047
2183
|
|
|
2048
|
-
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;
|
|
2049
2186
|
var foe = battle.foe(player);
|
|
2050
2187
|
var foe_stored = foe.stored();
|
|
2051
2188
|
const foe_ident = battle.active(player.foe());
|
|
@@ -2056,7 +2193,7 @@ pub const Effects = struct {
|
|
|
2056
2193
|
if (showdown) {
|
|
2057
2194
|
// Invulnerability trumps type immunity on Pokémon Showdown
|
|
2058
2195
|
if (immune and !foe.active.volatiles.Invulnerable) return log.immune(foe_ident, .None);
|
|
2059
|
-
if (!try checkHit(battle, player, move,
|
|
2196
|
+
if (!try checkHit(battle, player, move, options)) return;
|
|
2060
2197
|
}
|
|
2061
2198
|
if (Status.any(foe_stored.status)) {
|
|
2062
2199
|
return log.fail(
|
|
@@ -2066,8 +2203,9 @@ pub const Effects = struct {
|
|
|
2066
2203
|
}
|
|
2067
2204
|
if (!showdown) {
|
|
2068
2205
|
if (immune) return log.immune(foe_ident, .None);
|
|
2069
|
-
if (!try checkHit(battle, player, move,
|
|
2206
|
+
if (!try checkHit(battle, player, move, options)) return;
|
|
2070
2207
|
}
|
|
2208
|
+
try options.chance.commit(player, .hit);
|
|
2071
2209
|
|
|
2072
2210
|
foe_stored.status = Status.init(.PAR);
|
|
2073
2211
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
@@ -2075,7 +2213,7 @@ pub const Effects = struct {
|
|
|
2075
2213
|
try log.status(foe_ident, foe_stored.status, .None);
|
|
2076
2214
|
}
|
|
2077
2215
|
|
|
2078
|
-
fn paralyzeChance(battle: anytype, player: Player, move: Move.Data,
|
|
2216
|
+
fn paralyzeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
2079
2217
|
var foe = battle.foe(player);
|
|
2080
2218
|
var foe_stored = foe.stored();
|
|
2081
2219
|
|
|
@@ -2086,32 +2224,31 @@ pub const Effects = struct {
|
|
|
2086
2224
|
}
|
|
2087
2225
|
|
|
2088
2226
|
// Body Slam can't paralyze a Normal type Pokémon
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
Gen12.percent(10)
|
|
2094
|
-
else
|
|
2095
|
-
Gen12.percent(30));
|
|
2096
|
-
if (!chance) return;
|
|
2227
|
+
if (foe.active.types.includes(move.type)) return;
|
|
2228
|
+
if (!try Rolls.secondaryChance(battle, player, move.effect == .ParalyzeChance1, options)) {
|
|
2229
|
+
return;
|
|
2230
|
+
}
|
|
2097
2231
|
|
|
2098
2232
|
foe_stored.status = Status.init(.PAR);
|
|
2099
2233
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
2100
2234
|
|
|
2101
|
-
try log.status(battle.active(player.foe()), foe_stored.status, .None);
|
|
2235
|
+
try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
|
|
2102
2236
|
}
|
|
2103
2237
|
|
|
2104
|
-
fn payDay(battle: anytype, player: Player,
|
|
2105
|
-
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
|
+
}
|
|
2106
2242
|
}
|
|
2107
2243
|
|
|
2108
|
-
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;
|
|
2109
2246
|
var foe = battle.foe(player);
|
|
2110
2247
|
var foe_stored = foe.stored();
|
|
2111
2248
|
const foe_ident = battle.active(player.foe());
|
|
2112
2249
|
const toxic = battle.side(player).last_selected_move == .Toxic;
|
|
2113
2250
|
|
|
2114
|
-
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)) {
|
|
2115
2252
|
return;
|
|
2116
2253
|
} else if (foe.active.volatiles.Substitute) {
|
|
2117
2254
|
if (move.effect != .Poison) return;
|
|
@@ -2135,16 +2272,12 @@ pub const Effects = struct {
|
|
|
2135
2272
|
}
|
|
2136
2273
|
|
|
2137
2274
|
if (move.effect == .Poison) {
|
|
2138
|
-
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);
|
|
2139
2277
|
} else {
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
battle.rng.next() < 1 + (if (move.effect == .PoisonChance1)
|
|
2144
|
-
Gen12.percent(20)
|
|
2145
|
-
else
|
|
2146
|
-
Gen12.percent(40));
|
|
2147
|
-
if (!chance) return;
|
|
2278
|
+
if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
|
|
2279
|
+
return;
|
|
2280
|
+
}
|
|
2148
2281
|
}
|
|
2149
2282
|
|
|
2150
2283
|
foe_stored.status = Status.init(.PSN);
|
|
@@ -2159,34 +2292,40 @@ pub const Effects = struct {
|
|
|
2159
2292
|
|
|
2160
2293
|
fn rage(battle: anytype, player: Player) !void {
|
|
2161
2294
|
var volatiles = &battle.side(player).active.volatiles;
|
|
2162
|
-
if (volatiles.Rage) return;
|
|
2163
|
-
volatiles.Rage = true;
|
|
2164
2295
|
assert(!volatiles.Bide);
|
|
2165
|
-
volatiles.
|
|
2296
|
+
volatiles.Rage = true;
|
|
2166
2297
|
}
|
|
2167
2298
|
|
|
2168
|
-
fn recoil(battle: anytype, player: Player, residual: *bool,
|
|
2299
|
+
fn recoil(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
|
|
2169
2300
|
var side = battle.side(player);
|
|
2170
2301
|
var stored = side.stored();
|
|
2171
2302
|
|
|
2172
|
-
|
|
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 /
|
|
2173
2307
|
@as(u8, if (side.last_selected_move == .Struggle) 2 else 4), 1));
|
|
2174
|
-
stored.hp = @intCast(
|
|
2308
|
+
stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
|
|
2175
2309
|
|
|
2176
|
-
try log.damageOf(
|
|
2310
|
+
try options.log.damageOf(
|
|
2311
|
+
battle.active(player),
|
|
2312
|
+
stored,
|
|
2313
|
+
.RecoilOf,
|
|
2314
|
+
battle.active(player.foe()),
|
|
2315
|
+
);
|
|
2177
2316
|
if (showdown and stored.hp == 0) residual.* = false;
|
|
2178
2317
|
}
|
|
2179
2318
|
|
|
2180
|
-
fn reflect(battle: anytype, player: Player,
|
|
2319
|
+
fn reflect(battle: anytype, player: Player, options: anytype) !void {
|
|
2181
2320
|
var side = battle.side(player);
|
|
2182
2321
|
|
|
2183
|
-
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);
|
|
2184
2323
|
side.active.volatiles.Reflect = true;
|
|
2185
2324
|
|
|
2186
|
-
try log.start(battle.active(player), .Reflect);
|
|
2325
|
+
try options.log.start(battle.active(player), .Reflect);
|
|
2187
2326
|
}
|
|
2188
2327
|
|
|
2189
|
-
fn sleep(battle: anytype, player: Player, move: Move.Data,
|
|
2328
|
+
fn sleep(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
2190
2329
|
var foe = battle.foe(player);
|
|
2191
2330
|
var foe_stored = foe.stored();
|
|
2192
2331
|
const foe_ident = battle.active(player.foe());
|
|
@@ -2197,13 +2336,14 @@ pub const Effects = struct {
|
|
|
2197
2336
|
// the Sleep Clause Mod might activate, causing us to not actually bypass
|
|
2198
2337
|
} else {
|
|
2199
2338
|
if (Status.any(foe_stored.status)) {
|
|
2200
|
-
return log.fail(
|
|
2339
|
+
return options.log.fail(
|
|
2201
2340
|
foe_ident,
|
|
2202
2341
|
if (Status.is(foe_stored.status, .SLP)) .Sleep else .None,
|
|
2203
2342
|
);
|
|
2204
2343
|
}
|
|
2205
2344
|
// If checkHit in doMove didn't return true Pokémon Showdown wouldn't be in here
|
|
2206
|
-
if (!showdown and !try checkHit(battle, player, move,
|
|
2345
|
+
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2346
|
+
try options.chance.commit(player, .hit);
|
|
2207
2347
|
}
|
|
2208
2348
|
|
|
2209
2349
|
// Sleep Clause Mod
|
|
@@ -2214,40 +2354,32 @@ pub const Effects = struct {
|
|
|
2214
2354
|
}
|
|
2215
2355
|
foe.active.volatiles.Recharging = false;
|
|
2216
2356
|
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
while (true) {
|
|
2221
|
-
const r = battle.rng.next() & 7;
|
|
2222
|
-
if (r != 0) break :loop r;
|
|
2223
|
-
}
|
|
2224
|
-
});
|
|
2225
|
-
|
|
2226
|
-
foe_stored.status = Status.slp(duration);
|
|
2227
|
-
try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
|
|
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);
|
|
2228
2360
|
}
|
|
2229
2361
|
|
|
2230
|
-
fn splash(battle: anytype, player: Player,
|
|
2231
|
-
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);
|
|
2232
2364
|
}
|
|
2233
2365
|
|
|
2234
|
-
fn substitute(battle: anytype, player: Player, residual: *bool,
|
|
2366
|
+
fn substitute(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
|
|
2235
2367
|
var side = battle.side(player);
|
|
2236
2368
|
if (side.active.volatiles.Substitute) {
|
|
2237
|
-
try log.fail(battle.active(player), .Substitute);
|
|
2369
|
+
try options.log.fail(battle.active(player), .Substitute);
|
|
2238
2370
|
return;
|
|
2239
2371
|
}
|
|
2240
2372
|
|
|
2241
2373
|
assert(side.stored().stats.hp <= 1023);
|
|
2242
2374
|
// Will be 0 if HP is <= 3 meaning that the user gets a 1 HP Substitute for "free"
|
|
2243
|
-
const hp = @intCast(
|
|
2375
|
+
const hp: u8 = @intCast(side.stored().stats.hp / 4);
|
|
2244
2376
|
// Pokénon Showdown incorrectly checks for 1/4 HP based on `target.maxhp / 4` which returns
|
|
2245
2377
|
// a floating point value and thus only correctly implements the Substitute 1/4 glitch when
|
|
2246
2378
|
// the target's HP is exactly divisible by 4 (here we're using an inlined divCeil routine to
|
|
2247
2379
|
// avoid having to convert to floating point)
|
|
2248
2380
|
const required_hp = if (showdown) @divFloor(side.stored().stats.hp - 1, 4) + 1 else hp;
|
|
2249
2381
|
if (side.stored().hp < required_hp) {
|
|
2250
|
-
try log.fail(battle.active(player), .Weak);
|
|
2382
|
+
try options.log.fail(battle.active(player), .Weak);
|
|
2251
2383
|
return;
|
|
2252
2384
|
}
|
|
2253
2385
|
|
|
@@ -2255,35 +2387,32 @@ pub const Effects = struct {
|
|
|
2255
2387
|
side.stored().hp -= hp;
|
|
2256
2388
|
side.active.volatiles.substitute = hp + 1;
|
|
2257
2389
|
side.active.volatiles.Substitute = true;
|
|
2258
|
-
try log.start(battle.active(player), .Substitute);
|
|
2390
|
+
try options.log.start(battle.active(player), .Substitute);
|
|
2259
2391
|
if (hp > 0) {
|
|
2260
|
-
try log.damage(battle.active(player), side.stored(), .None);
|
|
2392
|
+
try options.log.damage(battle.active(player), side.stored(), .None);
|
|
2261
2393
|
if (showdown and side.stored().hp == 0) residual.* = false;
|
|
2262
2394
|
}
|
|
2263
2395
|
}
|
|
2264
2396
|
|
|
2265
|
-
fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data,
|
|
2397
|
+
fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
2266
2398
|
if (!showdown or battle.side(player).last_selected_move == .Teleport) return;
|
|
2267
2399
|
|
|
2268
2400
|
// Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
|
|
2269
|
-
|
|
2401
|
+
if (try checkHit(battle, player, move, options)) try options.chance.commit(player, .hit);
|
|
2402
|
+
|
|
2270
2403
|
battle.last_damage = 0;
|
|
2271
2404
|
}
|
|
2272
2405
|
|
|
2273
|
-
fn thrashing(battle: anytype, player: Player) void {
|
|
2406
|
+
fn thrashing(battle: anytype, player: Player, options: anytype) void {
|
|
2274
2407
|
var volatiles = &battle.side(player).active.volatiles;
|
|
2275
2408
|
assert(!volatiles.Thrashing);
|
|
2276
2409
|
assert(!volatiles.Bide);
|
|
2277
2410
|
|
|
2278
2411
|
volatiles.Thrashing = true;
|
|
2279
|
-
volatiles.
|
|
2280
|
-
volatiles.attacks = @intCast(u3, if (showdown)
|
|
2281
|
-
battle.rng.range(u8, 2, 4)
|
|
2282
|
-
else
|
|
2283
|
-
(battle.rng.next() & 1) + 2);
|
|
2412
|
+
volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
2284
2413
|
}
|
|
2285
2414
|
|
|
2286
|
-
fn transform(battle: anytype, player: Player,
|
|
2415
|
+
fn transform(battle: anytype, player: Player, options: anytype) !void {
|
|
2287
2416
|
var side = battle.side(player);
|
|
2288
2417
|
const foe = battle.foe(player);
|
|
2289
2418
|
const foe_ident = battle.active(player.foe());
|
|
@@ -2309,10 +2438,10 @@ pub const Effects = struct {
|
|
|
2309
2438
|
side.active.moves[i].pp = if (m.id != .None) 5 else 0;
|
|
2310
2439
|
}
|
|
2311
2440
|
|
|
2312
|
-
try log.transform(battle.active(player), foe_ident);
|
|
2441
|
+
try options.log.transform(battle.active(player), foe_ident);
|
|
2313
2442
|
}
|
|
2314
2443
|
|
|
2315
|
-
fn binding(battle: anytype, player: Player) void {
|
|
2444
|
+
fn binding(battle: anytype, player: Player, rewrap: bool, options: anytype) !void {
|
|
2316
2445
|
var side = battle.side(player);
|
|
2317
2446
|
var foe = battle.foe(player);
|
|
2318
2447
|
|
|
@@ -2320,12 +2449,16 @@ pub const Effects = struct {
|
|
|
2320
2449
|
side.active.volatiles.Binding = true;
|
|
2321
2450
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2322
2451
|
// (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
|
|
2323
|
-
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);
|
|
2324
2455
|
|
|
2325
|
-
side.active.volatiles.attacks =
|
|
2456
|
+
side.active.volatiles.attacks =
|
|
2457
|
+
try Rolls.distribution(battle, .Binding, player, options) - 1;
|
|
2326
2458
|
}
|
|
2327
2459
|
|
|
2328
|
-
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;
|
|
2329
2462
|
var side = battle.side(player);
|
|
2330
2463
|
const ident = battle.active(player);
|
|
2331
2464
|
|
|
@@ -2337,7 +2470,7 @@ pub const Effects = struct {
|
|
|
2337
2470
|
assert(boosts.atk >= -6 and boosts.atk <= 6);
|
|
2338
2471
|
if (boosts.atk == 6) return try log.fail(ident, .None);
|
|
2339
2472
|
const n: u2 = if (move.effect == .AttackUp2) 2 else 1;
|
|
2340
|
-
boosts.atk = @
|
|
2473
|
+
boosts.atk = @as(i4, @intCast(@min(6, @as(i8, boosts.atk) + n)));
|
|
2341
2474
|
const reason = if (move.effect == .Rage) Boost.Rage else Boost.Attack;
|
|
2342
2475
|
if (stats.atk == MAX_STAT_VALUE) {
|
|
2343
2476
|
boosts.atk -= 1;
|
|
@@ -2348,8 +2481,8 @@ pub const Effects = struct {
|
|
|
2348
2481
|
}
|
|
2349
2482
|
return try log.fail(ident, .None);
|
|
2350
2483
|
}
|
|
2351
|
-
var mod = BOOSTS[@
|
|
2352
|
-
const stat = unmodifiedStats(battle,
|
|
2484
|
+
var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
|
|
2485
|
+
const stat = unmodifiedStats(battle, side).atk;
|
|
2353
2486
|
stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2354
2487
|
try log.boost(ident, reason, n);
|
|
2355
2488
|
// Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
|
|
@@ -2359,7 +2492,7 @@ pub const Effects = struct {
|
|
|
2359
2492
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
2360
2493
|
if (boosts.def == 6) return try log.fail(ident, .None);
|
|
2361
2494
|
const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
|
|
2362
|
-
boosts.def = @intCast(
|
|
2495
|
+
boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
|
|
2363
2496
|
if (stats.def == MAX_STAT_VALUE) {
|
|
2364
2497
|
boosts.def -= 1;
|
|
2365
2498
|
if (showdown) {
|
|
@@ -2368,15 +2501,15 @@ pub const Effects = struct {
|
|
|
2368
2501
|
}
|
|
2369
2502
|
return try log.fail(ident, .None);
|
|
2370
2503
|
}
|
|
2371
|
-
var mod = BOOSTS[@
|
|
2372
|
-
const stat = unmodifiedStats(battle,
|
|
2504
|
+
var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
|
|
2505
|
+
const stat = unmodifiedStats(battle, side).def;
|
|
2373
2506
|
stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2374
2507
|
try log.boost(ident, .Defense, n);
|
|
2375
2508
|
},
|
|
2376
2509
|
.SpeedUp2 => {
|
|
2377
2510
|
assert(boosts.spe >= -6 and boosts.spe <= 6);
|
|
2378
2511
|
if (boosts.spe == 6) return try log.fail(ident, .None);
|
|
2379
|
-
boosts.spe = @intCast(
|
|
2512
|
+
boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
|
|
2380
2513
|
if (stats.spe == MAX_STAT_VALUE) {
|
|
2381
2514
|
boosts.spe -= 1;
|
|
2382
2515
|
if (showdown) {
|
|
@@ -2385,8 +2518,8 @@ pub const Effects = struct {
|
|
|
2385
2518
|
}
|
|
2386
2519
|
return try log.fail(ident, .None);
|
|
2387
2520
|
}
|
|
2388
|
-
var mod = BOOSTS[@
|
|
2389
|
-
const stat = unmodifiedStats(battle,
|
|
2521
|
+
var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
|
|
2522
|
+
const stat = unmodifiedStats(battle, side).spe;
|
|
2390
2523
|
stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2391
2524
|
try log.boost(ident, .Speed, 2);
|
|
2392
2525
|
},
|
|
@@ -2394,7 +2527,7 @@ pub const Effects = struct {
|
|
|
2394
2527
|
assert(boosts.spc >= -6 and boosts.spc <= 6);
|
|
2395
2528
|
if (boosts.spc == 6) return try log.fail(ident, .None);
|
|
2396
2529
|
const n: u2 = if (move.effect == .SpecialUp2) 2 else 1;
|
|
2397
|
-
boosts.spc = @intCast(
|
|
2530
|
+
boosts.spc = @intCast(@min(6, @as(i8, boosts.spc) + n));
|
|
2398
2531
|
if (stats.spc == MAX_STAT_VALUE) {
|
|
2399
2532
|
boosts.spc -= 1;
|
|
2400
2533
|
if (showdown) {
|
|
@@ -2405,8 +2538,8 @@ pub const Effects = struct {
|
|
|
2405
2538
|
}
|
|
2406
2539
|
return try log.fail(ident, .None);
|
|
2407
2540
|
}
|
|
2408
|
-
var mod = BOOSTS[@
|
|
2409
|
-
const stat = unmodifiedStats(battle,
|
|
2541
|
+
var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
|
|
2542
|
+
const stat = unmodifiedStats(battle, side).spc;
|
|
2410
2543
|
stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2411
2544
|
try log.boost(ident, .SpecialAttack, n);
|
|
2412
2545
|
try log.boost(ident, .SpecialDefense, n);
|
|
@@ -2414,7 +2547,7 @@ pub const Effects = struct {
|
|
|
2414
2547
|
.EvasionUp1 => {
|
|
2415
2548
|
assert(boosts.evasion >= -6 and boosts.evasion <= 6);
|
|
2416
2549
|
if (boosts.evasion == 6) return try log.fail(ident, .None);
|
|
2417
|
-
boosts.evasion = @intCast(
|
|
2550
|
+
boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
|
|
2418
2551
|
try log.boost(ident, .Evasion, 1);
|
|
2419
2552
|
},
|
|
2420
2553
|
else => unreachable,
|
|
@@ -2424,32 +2557,32 @@ pub const Effects = struct {
|
|
|
2424
2557
|
statusModify(battle.foe(player).stored().status, &battle.foe(player).active.stats);
|
|
2425
2558
|
}
|
|
2426
2559
|
|
|
2427
|
-
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;
|
|
2428
2562
|
var foe = battle.foe(player);
|
|
2429
2563
|
const foe_ident = battle.active(player.foe());
|
|
2564
|
+
const secondary = move.effect.isStatDownChance();
|
|
2430
2565
|
|
|
2431
|
-
if (foe.active.volatiles.Substitute)
|
|
2432
|
-
return if (!move.effect.isStatDownChance()) log.fail(foe_ident, .None);
|
|
2433
|
-
}
|
|
2566
|
+
if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(foe_ident, .None);
|
|
2434
2567
|
|
|
2435
|
-
if (
|
|
2436
|
-
const
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
if (!
|
|
2441
|
-
|
|
2442
|
-
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);
|
|
2443
2575
|
}
|
|
2444
2576
|
|
|
2445
2577
|
var stats = &foe.active.stats;
|
|
2446
2578
|
var boosts = &foe.active.boosts;
|
|
2579
|
+
const fail = showdown or !secondary;
|
|
2447
2580
|
|
|
2448
2581
|
switch (move.effect) {
|
|
2449
2582
|
.AttackDown1, .AttackDownChance => {
|
|
2450
2583
|
assert(boosts.atk >= -6 and boosts.atk <= 6);
|
|
2451
|
-
if (boosts.atk == -6) return try log.fail(foe_ident, .None);
|
|
2452
|
-
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));
|
|
2453
2586
|
if (stats.atk == 1) {
|
|
2454
2587
|
boosts.atk += 1;
|
|
2455
2588
|
if (showdown) {
|
|
@@ -2458,16 +2591,16 @@ pub const Effects = struct {
|
|
|
2458
2591
|
}
|
|
2459
2592
|
return try log.fail(foe_ident, .None);
|
|
2460
2593
|
}
|
|
2461
|
-
var mod = BOOSTS[@
|
|
2462
|
-
const stat = unmodifiedStats(battle,
|
|
2594
|
+
var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
|
|
2595
|
+
const stat = unmodifiedStats(battle, foe).atk;
|
|
2463
2596
|
stats.atk = @max(1, stat * mod[0] / mod[1]);
|
|
2464
2597
|
try log.boost(foe_ident, .Attack, -1);
|
|
2465
2598
|
},
|
|
2466
2599
|
.DefenseDown1, .DefenseDown2, .DefenseDownChance => {
|
|
2467
2600
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
2468
|
-
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);
|
|
2469
2602
|
const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
|
|
2470
|
-
boosts.def = @intCast(
|
|
2603
|
+
boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
|
|
2471
2604
|
if (stats.def == 1) {
|
|
2472
2605
|
boosts.def += 1;
|
|
2473
2606
|
if (showdown) {
|
|
@@ -2476,15 +2609,15 @@ pub const Effects = struct {
|
|
|
2476
2609
|
}
|
|
2477
2610
|
return try log.fail(foe_ident, .None);
|
|
2478
2611
|
}
|
|
2479
|
-
var mod = BOOSTS[@
|
|
2480
|
-
const stat = unmodifiedStats(battle,
|
|
2612
|
+
var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
|
|
2613
|
+
const stat = unmodifiedStats(battle, foe).def;
|
|
2481
2614
|
stats.def = @max(1, stat * mod[0] / mod[1]);
|
|
2482
2615
|
try log.boost(foe_ident, .Defense, -@as(i8, n));
|
|
2483
2616
|
},
|
|
2484
2617
|
.SpeedDown1, .SpeedDownChance => {
|
|
2485
2618
|
assert(boosts.spe >= -6 and boosts.spe <= 6);
|
|
2486
|
-
if (boosts.spe == -6) return try log.fail(foe_ident, .None);
|
|
2487
|
-
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));
|
|
2488
2621
|
if (stats.spe == 1) {
|
|
2489
2622
|
boosts.spe += 1;
|
|
2490
2623
|
if (showdown) {
|
|
@@ -2493,16 +2626,16 @@ pub const Effects = struct {
|
|
|
2493
2626
|
}
|
|
2494
2627
|
return try log.fail(foe_ident, .None);
|
|
2495
2628
|
}
|
|
2496
|
-
var mod = BOOSTS[@
|
|
2497
|
-
const stat = unmodifiedStats(battle,
|
|
2629
|
+
var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
|
|
2630
|
+
const stat = unmodifiedStats(battle, foe).spe;
|
|
2498
2631
|
stats.spe = @max(1, stat * mod[0] / mod[1]);
|
|
2499
2632
|
try log.boost(foe_ident, .Speed, -1);
|
|
2500
2633
|
assert(boosts.spe >= -6);
|
|
2501
2634
|
},
|
|
2502
2635
|
.SpecialDownChance => {
|
|
2503
2636
|
assert(boosts.spc >= -6 and boosts.spc <= 6);
|
|
2504
|
-
if (boosts.spc == -6) return try log.fail(foe_ident, .None);
|
|
2505
|
-
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));
|
|
2506
2639
|
if (stats.spc == 1) {
|
|
2507
2640
|
boosts.spc += 1;
|
|
2508
2641
|
if (showdown) {
|
|
@@ -2513,16 +2646,16 @@ pub const Effects = struct {
|
|
|
2513
2646
|
}
|
|
2514
2647
|
return try log.fail(foe_ident, .None);
|
|
2515
2648
|
}
|
|
2516
|
-
var mod = BOOSTS[@
|
|
2517
|
-
const stat = unmodifiedStats(battle,
|
|
2649
|
+
var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
|
|
2650
|
+
const stat = unmodifiedStats(battle, foe).spc;
|
|
2518
2651
|
stats.spc = @max(1, stat * mod[0] / mod[1]);
|
|
2519
2652
|
try log.boost(foe_ident, .SpecialAttack, -1);
|
|
2520
2653
|
try log.boost(foe_ident, .SpecialDefense, -1);
|
|
2521
2654
|
},
|
|
2522
2655
|
.AccuracyDown1 => {
|
|
2523
2656
|
assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
|
|
2524
|
-
if (boosts.accuracy == -6) return try log.fail(foe_ident, .None);
|
|
2525
|
-
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));
|
|
2526
2659
|
try log.boost(foe_ident, .Accuracy, -1);
|
|
2527
2660
|
},
|
|
2528
2661
|
else => unreachable,
|
|
@@ -2533,8 +2666,7 @@ pub const Effects = struct {
|
|
|
2533
2666
|
}
|
|
2534
2667
|
};
|
|
2535
2668
|
|
|
2536
|
-
fn unmodifiedStats(battle: anytype,
|
|
2537
|
-
const side = battle.side(who);
|
|
2669
|
+
fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
|
|
2538
2670
|
if (!side.active.volatiles.Transform) return &side.stored().stats;
|
|
2539
2671
|
const id = ID.from(side.active.volatiles.transform);
|
|
2540
2672
|
return &battle.side(id.player).pokemon[id.id - 1].stats;
|
|
@@ -2548,16 +2680,19 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
|
|
|
2548
2680
|
}
|
|
2549
2681
|
}
|
|
2550
2682
|
|
|
2551
|
-
inline fn isForced(active:
|
|
2683
|
+
inline fn isForced(active: anytype) bool {
|
|
2552
2684
|
return active.volatiles.Recharging or active.volatiles.Rage or
|
|
2553
2685
|
active.volatiles.Thrashing or active.volatiles.Charging;
|
|
2554
2686
|
}
|
|
2555
2687
|
|
|
2556
|
-
fn clearVolatiles(battle: anytype, who: Player,
|
|
2688
|
+
fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !void {
|
|
2689
|
+
var log = options.log;
|
|
2557
2690
|
var side = battle.side(who);
|
|
2558
2691
|
var volatiles = &side.active.volatiles;
|
|
2559
2692
|
const ident = battle.active(who);
|
|
2560
2693
|
|
|
2694
|
+
options.chance.clearDurations(who, clear);
|
|
2695
|
+
|
|
2561
2696
|
if (volatiles.disabled_move != 0) {
|
|
2562
2697
|
volatiles.disabled_move = 0;
|
|
2563
2698
|
volatiles.disabled_duration = 0;
|
|
@@ -2600,86 +2735,308 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
|
|
|
2600
2735
|
}
|
|
2601
2736
|
}
|
|
2602
2737
|
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
m
|
|
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
|
|
2610
2744
|
else
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
return true;
|
|
2616
|
-
}
|
|
2745
|
+
battle.rng.next() < Gen12.percent(50) + 1;
|
|
2746
|
+
|
|
2747
|
+
try options.chance.speedTie(p1);
|
|
2748
|
+
return p1;
|
|
2617
2749
|
}
|
|
2618
|
-
return false;
|
|
2619
|
-
}
|
|
2620
2750
|
|
|
2621
|
-
inline fn
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
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;
|
|
2758
|
+
|
|
2759
|
+
try options.chance.criticalHit(player, crit, rate);
|
|
2760
|
+
return crit;
|
|
2625
2761
|
}
|
|
2626
|
-
}
|
|
2627
2762
|
|
|
2628
|
-
fn
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
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
|
+
};
|
|
2632
2773
|
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
else
|
|
2638
|
-
(battle.rng.next() & 3) + 2);
|
|
2774
|
+
assert(roll >= 217 and roll <= 255);
|
|
2775
|
+
try options.chance.damage(player, roll);
|
|
2776
|
+
return roll;
|
|
2777
|
+
}
|
|
2639
2778
|
|
|
2640
|
-
|
|
2641
|
-
|
|
2779
|
+
inline fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) bool {
|
|
2780
|
+
const ok = if (options.calc.overridden(player, .hit)) |val|
|
|
2781
|
+
val == .true
|
|
2782
|
+
else if (showdown)
|
|
2783
|
+
battle.rng.chance(u8, accuracy, 256)
|
|
2784
|
+
else
|
|
2785
|
+
battle.rng.next() < accuracy;
|
|
2642
2786
|
|
|
2643
|
-
|
|
2787
|
+
options.chance.hit(ok, accuracy);
|
|
2788
|
+
return ok;
|
|
2789
|
+
}
|
|
2644
2790
|
|
|
2645
|
-
fn
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2791
|
+
inline fn confused(battle: anytype, player: Player, options: anytype) !bool {
|
|
2792
|
+
const cfz = if (options.calc.overridden(player, .confused)) |val|
|
|
2793
|
+
val == .true
|
|
2794
|
+
else if (showdown)
|
|
2795
|
+
!battle.rng.chance(u8, 128, 256)
|
|
2796
|
+
else
|
|
2797
|
+
battle.rng.next() >= Gen12.percent(50) + 1;
|
|
2650
2798
|
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2799
|
+
try options.chance.confused(player, cfz);
|
|
2800
|
+
return cfz;
|
|
2801
|
+
}
|
|
2802
|
+
|
|
2803
|
+
inline fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
|
|
2804
|
+
const par = if (options.calc.overridden(player, .paralyzed)) |val|
|
|
2805
|
+
val == .true
|
|
2806
|
+
else if (showdown)
|
|
2807
|
+
battle.rng.chance(u8, 63, 256)
|
|
2808
|
+
else
|
|
2809
|
+
battle.rng.next() < Gen12.percent(25);
|
|
2810
|
+
|
|
2811
|
+
try options.chance.paralyzed(player, par);
|
|
2812
|
+
return par;
|
|
2813
|
+
}
|
|
2814
|
+
|
|
2815
|
+
inline fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
|
|
2816
|
+
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2817
|
+
val == .true
|
|
2818
|
+
else if (showdown)
|
|
2819
|
+
battle.rng.chance(u8, 25, 256)
|
|
2820
|
+
else
|
|
2821
|
+
battle.rng.next() < Gen12.percent(10);
|
|
2822
|
+
|
|
2823
|
+
try options.chance.secondaryChance(player, proc, 25);
|
|
2824
|
+
return proc;
|
|
2825
|
+
}
|
|
2826
|
+
|
|
2827
|
+
inline fn secondaryChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
|
|
2828
|
+
const rate: u8 = if (low) 26 else 77;
|
|
2829
|
+
|
|
2830
|
+
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2831
|
+
val == .true
|
|
2832
|
+
else if (showdown)
|
|
2833
|
+
battle.rng.chance(u8, rate, 256)
|
|
2834
|
+
else
|
|
2835
|
+
battle.rng.next() < 1 + (if (low) Gen12.percent(10) else Gen12.percent(30));
|
|
2836
|
+
|
|
2837
|
+
try options.chance.secondaryChance(player, proc, rate);
|
|
2838
|
+
return proc;
|
|
2839
|
+
}
|
|
2840
|
+
|
|
2841
|
+
inline fn poisonChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
|
|
2842
|
+
const rate: u8 = if (low) 52 else 103;
|
|
2843
|
+
|
|
2844
|
+
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2845
|
+
val == .true
|
|
2846
|
+
else if (showdown)
|
|
2847
|
+
battle.rng.chance(u8, rate, 256)
|
|
2848
|
+
else
|
|
2849
|
+
battle.rng.next() < 1 + (if (low) Gen12.percent(20) else Gen12.percent(40));
|
|
2850
|
+
|
|
2851
|
+
try options.chance.secondaryChance(player, proc, rate);
|
|
2852
|
+
return proc;
|
|
2853
|
+
}
|
|
2854
|
+
|
|
2855
|
+
inline fn unboost(battle: anytype, player: Player, options: anytype) !bool {
|
|
2856
|
+
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2857
|
+
val == .true
|
|
2858
|
+
else if (showdown)
|
|
2859
|
+
battle.rng.chance(u8, 85, 256)
|
|
2860
|
+
else
|
|
2861
|
+
battle.rng.next() < Gen12.percent(33) + 1;
|
|
2862
|
+
|
|
2863
|
+
try options.chance.secondaryChance(player, proc, 85);
|
|
2864
|
+
return proc;
|
|
2865
|
+
}
|
|
2866
|
+
|
|
2867
|
+
inline fn metronome(battle: anytype, player: Player, options: anytype) !Move {
|
|
2868
|
+
const move: Move = if (options.calc.overridden(player, .metronome)) |val|
|
|
2869
|
+
val
|
|
2870
|
+
else if (showdown) move: {
|
|
2871
|
+
const r = battle.rng.range(u8, 0, @intFromEnum(Move.Struggle) - 2);
|
|
2872
|
+
const mod = @as(u2, (if (r < @intFromEnum(Move.Metronome) - 1) 1 else 2));
|
|
2873
|
+
break :move @enumFromInt(r + mod);
|
|
2874
|
+
} else move: {
|
|
2875
|
+
while (true) {
|
|
2876
|
+
const r = battle.rng.next();
|
|
2877
|
+
if (r == 0 or r == @intFromEnum(Move.Metronome)) continue;
|
|
2878
|
+
if (r >= @intFromEnum(Move.Struggle)) continue;
|
|
2879
|
+
break :move @enumFromInt(r);
|
|
2658
2880
|
}
|
|
2881
|
+
};
|
|
2882
|
+
|
|
2883
|
+
try options.chance.metronome(player, move);
|
|
2884
|
+
return move;
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
inline fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
|
|
2888
|
+
const power = if (options.calc.overridden(player, .psywave)) |val|
|
|
2889
|
+
val - 1
|
|
2890
|
+
else if (showdown)
|
|
2891
|
+
battle.rng.range(u8, 0, max)
|
|
2892
|
+
else power: {
|
|
2893
|
+
while (true) {
|
|
2894
|
+
const r = battle.rng.next();
|
|
2895
|
+
if (r < max) break :power r;
|
|
2896
|
+
}
|
|
2897
|
+
};
|
|
2898
|
+
|
|
2899
|
+
assert(power < max);
|
|
2900
|
+
try options.chance.psywave(player, power, max);
|
|
2901
|
+
return power;
|
|
2902
|
+
}
|
|
2903
|
+
|
|
2904
|
+
inline fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2905
|
+
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2906
|
+
@intCast(val)
|
|
2907
|
+
else if (showdown)
|
|
2908
|
+
@intCast(battle.rng.range(u8, 1, 8))
|
|
2909
|
+
else duration: {
|
|
2910
|
+
while (true) {
|
|
2911
|
+
const r = battle.rng.next() & 7;
|
|
2912
|
+
if (r != 0) break :duration @intCast(r);
|
|
2913
|
+
}
|
|
2914
|
+
};
|
|
2915
|
+
|
|
2916
|
+
assert(duration >= 1 and duration <= 7);
|
|
2917
|
+
options.chance.duration(.sleep, player, player.foe(), duration);
|
|
2918
|
+
return duration;
|
|
2919
|
+
}
|
|
2920
|
+
|
|
2921
|
+
inline fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
|
|
2922
|
+
const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
|
|
2923
|
+
val
|
|
2924
|
+
else if (showdown)
|
|
2925
|
+
@intCast(battle.rng.range(u8, 1, 9))
|
|
2926
|
+
else
|
|
2927
|
+
@intCast((battle.rng.next() & 7) + 1);
|
|
2928
|
+
|
|
2929
|
+
assert(duration >= 1 and duration <= 8);
|
|
2930
|
+
options.chance.duration(.disable, player, player.foe(), duration);
|
|
2931
|
+
return duration;
|
|
2932
|
+
}
|
|
2933
|
+
|
|
2934
|
+
inline fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
|
|
2935
|
+
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2936
|
+
@intCast(val)
|
|
2937
|
+
else if (showdown)
|
|
2938
|
+
@intCast(battle.rng.range(u8, 2, 6))
|
|
2939
|
+
else
|
|
2940
|
+
@intCast((battle.rng.next() & 3) + 2);
|
|
2941
|
+
|
|
2942
|
+
assert(duration >= 2 and duration <= 5);
|
|
2943
|
+
options.chance.duration(.confusion, player, if (self) player else player.foe(), duration);
|
|
2944
|
+
return duration;
|
|
2945
|
+
}
|
|
2946
|
+
|
|
2947
|
+
inline fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2948
|
+
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2949
|
+
@intCast(val)
|
|
2950
|
+
else if (showdown)
|
|
2951
|
+
@intCast(battle.rng.range(u4, 2, 4))
|
|
2952
|
+
else
|
|
2953
|
+
@intCast((battle.rng.next() & 1) + 2);
|
|
2954
|
+
|
|
2955
|
+
assert(duration >= 2 and duration <= 3);
|
|
2956
|
+
options.chance.duration(.attacking, player, player, duration);
|
|
2957
|
+
return duration;
|
|
2958
|
+
}
|
|
2959
|
+
|
|
2960
|
+
inline fn distribution(
|
|
2961
|
+
battle: anytype,
|
|
2962
|
+
comptime effect: Move.Effect,
|
|
2963
|
+
player: Player,
|
|
2964
|
+
options: anytype,
|
|
2965
|
+
) !u3 {
|
|
2966
|
+
const roll = if (effect == .Binding) .duration else .multi_hit;
|
|
2967
|
+
const n: u3 = if (options.calc.overridden(player, roll)) |val|
|
|
2968
|
+
@intCast(val)
|
|
2969
|
+
else if (showdown)
|
|
2970
|
+
DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)]
|
|
2971
|
+
else n: {
|
|
2972
|
+
const r = (battle.rng.next() & 3);
|
|
2973
|
+
break :n @intCast((if (r < 2) r else battle.rng.next() & 3) + 2);
|
|
2974
|
+
};
|
|
2975
|
+
|
|
2976
|
+
assert(n >= 2 and n <= 5);
|
|
2977
|
+
if (effect == .Binding) {
|
|
2978
|
+
options.chance.duration(.binding, player, player, n);
|
|
2659
2979
|
} else {
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2980
|
+
try options.chance.multiHit(player, n);
|
|
2981
|
+
}
|
|
2982
|
+
return n;
|
|
2983
|
+
}
|
|
2984
|
+
|
|
2985
|
+
fn moveSlot(
|
|
2986
|
+
battle: anytype,
|
|
2987
|
+
player: Player,
|
|
2988
|
+
moves: []MoveSlot,
|
|
2989
|
+
check_pp: u4,
|
|
2990
|
+
options: anytype,
|
|
2991
|
+
) !u4 {
|
|
2992
|
+
// TODO: consider throwing error instead of rerolling?
|
|
2993
|
+
const overridden = if (options.calc.overridden(player, .move_slot)) |val|
|
|
2994
|
+
if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
|
|
2995
|
+
val
|
|
2996
|
+
else
|
|
2997
|
+
null
|
|
2998
|
+
else
|
|
2999
|
+
null;
|
|
3000
|
+
|
|
3001
|
+
const slot: u4 = overridden orelse slot: {
|
|
3002
|
+
if (showdown) {
|
|
3003
|
+
if (check_pp == 0) {
|
|
3004
|
+
var i: usize = moves.len;
|
|
3005
|
+
while (i > 0) {
|
|
3006
|
+
i -= 1;
|
|
3007
|
+
if (moves[i].id != .None) {
|
|
3008
|
+
break :slot battle.rng.range(u4, 0, @as(u4, @intCast(i + 1))) + 1;
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
} else {
|
|
3012
|
+
var r = battle.rng.range(u4, 0, check_pp) + 1;
|
|
3013
|
+
var i: usize = 0;
|
|
3014
|
+
while (i < moves.len and r > 0) : (i += 1) {
|
|
3015
|
+
if (moves[i].pp > 0) {
|
|
3016
|
+
r -= 1;
|
|
3017
|
+
if (r == 0) break;
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
break :slot @intCast(i + 1);
|
|
2666
3021
|
}
|
|
2667
3022
|
}
|
|
2668
|
-
return @intCast(u4, i + 1);
|
|
2669
|
-
}
|
|
2670
|
-
}
|
|
2671
3023
|
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
3024
|
+
while (true) {
|
|
3025
|
+
const r: u4 = @intCast(battle.rng.next() & 3);
|
|
3026
|
+
if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) break :slot r + 1;
|
|
3027
|
+
}
|
|
3028
|
+
};
|
|
3029
|
+
|
|
3030
|
+
try options.chance.moveSlot(player, slot, moves, check_pp);
|
|
3031
|
+
return slot;
|
|
2675
3032
|
}
|
|
2676
|
-
}
|
|
3033
|
+
};
|
|
2677
3034
|
|
|
2678
3035
|
test "RNG agreement" {
|
|
2679
3036
|
if (!showdown) return;
|
|
2680
3037
|
var expected: [256]u32 = undefined;
|
|
2681
3038
|
for (0..expected.len) |i| {
|
|
2682
|
-
expected[i] = @intCast(
|
|
3039
|
+
expected[i] = @intCast(i * 0x1000000);
|
|
2683
3040
|
}
|
|
2684
3041
|
|
|
2685
3042
|
var spe = rng.FixedRNG(1, expected.len){ .rolls = expected };
|
|
@@ -2722,7 +3079,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
2722
3079
|
const side = battle.side(player);
|
|
2723
3080
|
const foe = battle.foe(player);
|
|
2724
3081
|
|
|
2725
|
-
var active = side.active;
|
|
3082
|
+
var active = &side.active;
|
|
2726
3083
|
const stored = side.stored();
|
|
2727
3084
|
|
|
2728
3085
|
// While players are not given any input options on the cartridge in these cases,
|
|
@@ -2736,7 +3093,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
2736
3093
|
// strictly correct as the player would not have been presented the option to move or
|
|
2737
3094
|
// switch at all.
|
|
2738
3095
|
if (isForced(active)) {
|
|
2739
|
-
out[n] = .{ .type = .Move, .data = @
|
|
3096
|
+
out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
|
|
2740
3097
|
n += 1;
|
|
2741
3098
|
return n;
|
|
2742
3099
|
}
|