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