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