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