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