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