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