@pkmn/engine 0.1.0-dev.d716e940 → 0.1.0-dev.dc33f678
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -44
- package/build/pkg/addon.js +3 -2
- package/build/pkg/addon.js.map +1 -1
- package/build/pkg/data/ids.json +12 -12
- package/build/pkg/data/layout.json +26 -4
- package/build/pkg/data/protocol.json +69 -12
- package/build/pkg/data.d.ts +2 -2
- package/build/pkg/gen1.js +15 -18
- package/build/pkg/gen1.js.map +1 -1
- package/build/pkg/index.js.map +1 -1
- package/build/pkg/protocol.js +56 -37
- package/build/pkg/protocol.js.map +1 -1
- package/build.zig +89 -65
- package/package.json +37 -19
- package/src/bin/install-pkmn-engine +10 -5
- package/src/lib/binding/node.zig +46 -141
- package/src/lib/binding/wasm.zig +4 -2
- package/src/lib/common/battle.zig +17 -0
- package/src/lib/common/data.zig +15 -27
- 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 +733 -207
- package/src/lib/common/rational.zig +353 -0
- package/src/lib/common/rng.zig +15 -15
- package/src/lib/common/util.zig +12 -0
- package/src/lib/gen1/README.md +286 -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 +5 -5
- package/src/lib/gen1/data.zig +77 -65
- package/src/lib/gen1/helpers.zig +574 -18
- package/src/lib/gen1/mechanics.zig +827 -579
- package/src/lib/gen2/README.md +106 -85
- package/src/lib/gen2/data/items.zig +12 -18
- package/src/lib/gen2/data/moves.zig +660 -566
- package/src/lib/gen2/data/species.zig +1 -1
- package/src/lib/gen2/data/types.zig +131 -28
- package/src/lib/gen2/data.zig +132 -84
- package/src/lib/gen2/helpers.zig +16 -22
- package/src/lib/gen2/mechanics.zig +2426 -12
- package/src/lib/pkmn.zig +61 -2
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
const std = @import("std");
|
|
2
2
|
|
|
3
|
+
const pkmn = @import("../pkmn.zig");
|
|
4
|
+
|
|
3
5
|
const common = @import("../common/data.zig");
|
|
4
6
|
const DEBUG = @import("../common/debug.zig").print;
|
|
5
|
-
const options = @import("../common/options.zig");
|
|
6
7
|
const protocol = @import("../common/protocol.zig");
|
|
7
8
|
const rng = @import("../common/rng.zig");
|
|
8
9
|
|
|
@@ -12,13 +13,13 @@ const assert = std.debug.assert;
|
|
|
12
13
|
|
|
13
14
|
const expectEqual = std.testing.expectEqual;
|
|
14
15
|
|
|
16
|
+
const showdown = pkmn.options.showdown;
|
|
17
|
+
|
|
15
18
|
const Choice = common.Choice;
|
|
16
19
|
const ID = common.ID;
|
|
17
20
|
const Player = common.Player;
|
|
18
21
|
const Result = common.Result;
|
|
19
22
|
|
|
20
|
-
const showdown = options.showdown;
|
|
21
|
-
|
|
22
23
|
const Boost = protocol.Boost;
|
|
23
24
|
const Damage = protocol.Damage;
|
|
24
25
|
const Heal = protocol.Heal;
|
|
@@ -56,19 +57,15 @@ const BOOSTS = &[_][2]u8{
|
|
|
56
57
|
|
|
57
58
|
const MAX_STAT_VALUE = 999;
|
|
58
59
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
pub fn update(battle: anytype, c1: Choice, c2: Choice, log: anytype) !Result {
|
|
60
|
+
pub fn update(battle: anytype, c1: Choice, c2: Choice, options: anytype) !Result {
|
|
62
61
|
assert(c1.type != .Pass or c2.type != .Pass or battle.turn == 0);
|
|
63
|
-
if (battle.turn == 0) return start(battle,
|
|
62
|
+
if (battle.turn == 0) return start(battle, options);
|
|
64
63
|
|
|
65
64
|
var s1 = false;
|
|
66
65
|
var s2 = false;
|
|
67
|
-
var f1: ?Move = null;
|
|
68
|
-
var f2: ?Move = null;
|
|
69
66
|
|
|
70
|
-
if (selectMove(battle, .P1, c1, c2, &s1
|
|
71
|
-
if (selectMove(battle, .P2, c2, c1, &s2
|
|
67
|
+
if (selectMove(battle, .P1, c1, c2, &s1)) |r| return r;
|
|
68
|
+
if (selectMove(battle, .P2, c2, c1, &s2)) |r| return r;
|
|
72
69
|
|
|
73
70
|
var p1 = battle.side(.P1);
|
|
74
71
|
var p2 = battle.side(.P2);
|
|
@@ -76,19 +73,19 @@ pub fn update(battle: anytype, c1: Choice, c2: Choice, log: anytype) !Result {
|
|
|
76
73
|
var r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
|
|
77
74
|
var r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
|
|
78
75
|
|
|
79
|
-
if (turnOrder(battle, c1, c2) == .P1) {
|
|
80
|
-
if (try doTurn(battle, .P1, c1, r1, s1,
|
|
76
|
+
if (try turnOrder(battle, c1, c2, options) == .P1) {
|
|
77
|
+
if (try doTurn(battle, .P1, c1, r1, s1, .P2, c2, r2, s2, options)) |r| return r;
|
|
81
78
|
} else {
|
|
82
|
-
if (try doTurn(battle, .P2, c2, r2, s2,
|
|
79
|
+
if (try doTurn(battle, .P2, c2, r2, s2, .P1, c1, r1, s1, options)) |r| return r;
|
|
83
80
|
}
|
|
84
81
|
|
|
85
82
|
if (p1.active.volatiles.attacks == 0) p1.active.volatiles.Binding = false;
|
|
86
83
|
if (p2.active.volatiles.attacks == 0) p2.active.volatiles.Binding = false;
|
|
87
84
|
|
|
88
|
-
return endTurn(battle,
|
|
85
|
+
return endTurn(battle, options);
|
|
89
86
|
}
|
|
90
87
|
|
|
91
|
-
fn start(battle: anytype,
|
|
88
|
+
fn start(battle: anytype, options: anytype) !Result {
|
|
92
89
|
const p1 = battle.side(.P1);
|
|
93
90
|
const p2 = battle.side(.P2);
|
|
94
91
|
|
|
@@ -100,10 +97,10 @@ fn start(battle: anytype, log: anytype) !Result {
|
|
|
100
97
|
assert(!showdown or p2_slot == 1);
|
|
101
98
|
if (p2_slot == 0) return Result.Win;
|
|
102
99
|
|
|
103
|
-
try switchIn(battle, .P1, p1_slot, true,
|
|
104
|
-
try switchIn(battle, .P2, p2_slot, true,
|
|
100
|
+
try switchIn(battle, .P1, p1_slot, true, options);
|
|
101
|
+
try switchIn(battle, .P2, p2_slot, true, options);
|
|
105
102
|
|
|
106
|
-
return endTurn(battle,
|
|
103
|
+
return endTurn(battle, options);
|
|
107
104
|
}
|
|
108
105
|
|
|
109
106
|
fn findFirstAlive(side: *const Side) u8 {
|
|
@@ -117,7 +114,6 @@ fn selectMove(
|
|
|
117
114
|
choice: Choice,
|
|
118
115
|
foe_choice: Choice,
|
|
119
116
|
skip_turn: *bool,
|
|
120
|
-
from: *?Move,
|
|
121
117
|
) ?Result {
|
|
122
118
|
if (choice.type == .Pass) return null;
|
|
123
119
|
|
|
@@ -126,7 +122,7 @@ fn selectMove(
|
|
|
126
122
|
const stored = side.stored();
|
|
127
123
|
|
|
128
124
|
assert(!isForced(side.active) or
|
|
129
|
-
(choice.type == .Move and choice.data == @
|
|
125
|
+
(choice.type == .Move and choice.data == @intFromBool(showdown)));
|
|
130
126
|
|
|
131
127
|
// pre-battle menu
|
|
132
128
|
if (volatiles.Recharging) {
|
|
@@ -134,7 +130,6 @@ fn selectMove(
|
|
|
134
130
|
return null;
|
|
135
131
|
}
|
|
136
132
|
if (volatiles.Rage) {
|
|
137
|
-
from.* = side.last_used_move;
|
|
138
133
|
if (showdown) {
|
|
139
134
|
if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
|
|
140
135
|
saveMove(battle, player, null);
|
|
@@ -144,7 +139,6 @@ fn selectMove(
|
|
|
144
139
|
// Pokémon Showdown removes Flinch at the end-of-turn in its residual handler
|
|
145
140
|
if (!showdown) volatiles.Flinch = false;
|
|
146
141
|
if (volatiles.Thrashing or volatiles.Charging) {
|
|
147
|
-
from.* = side.last_selected_move;
|
|
148
142
|
if (showdown) {
|
|
149
143
|
if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
|
|
150
144
|
saveMove(battle, player, null);
|
|
@@ -165,9 +159,7 @@ fn selectMove(
|
|
|
165
159
|
return null;
|
|
166
160
|
}
|
|
167
161
|
if (volatiles.Binding) {
|
|
168
|
-
from.* = side.last_used_move;
|
|
169
162
|
if (showdown) {
|
|
170
|
-
if (foe_choice.type == .Switch) from.* = null;
|
|
171
163
|
// Pokémon Showdown overwrites Mirror Move with whatever was selected - really this
|
|
172
164
|
// should set side.last_selected_move = last.id to reuse Mirror Move and fail in order
|
|
173
165
|
// to satisfy the conditions of the Desync Clause Mod. However, because Binding is still
|
|
@@ -179,11 +171,7 @@ fn selectMove(
|
|
|
179
171
|
assert(choice.data == 0);
|
|
180
172
|
// GLITCH: https://glitchcity.wiki/Partial_trapping_move_Mirror_Move_link_battle_glitch
|
|
181
173
|
if (foe_choice.type == .Switch) {
|
|
182
|
-
const
|
|
183
|
-
battle.last_moves.p1_index
|
|
184
|
-
else
|
|
185
|
-
battle.last_moves.p2_index;
|
|
186
|
-
const last = side.active.move(@intCast(u4, slot));
|
|
174
|
+
const last = side.active.move(battle.lastMove(player).index);
|
|
187
175
|
if (last.id == .Metronome) side.last_selected_move = last.id;
|
|
188
176
|
if (last.id == .MirrorMove) return Result.Error;
|
|
189
177
|
}
|
|
@@ -207,16 +195,13 @@ fn selectMove(
|
|
|
207
195
|
if (choice.data == 0) {
|
|
208
196
|
const struggle = ok: {
|
|
209
197
|
for (side.active.moves, 0..) |move, i| {
|
|
210
|
-
if (move.pp > 0 and volatiles.
|
|
198
|
+
if (move.pp > 0 and volatiles.disable_move != i + 1) break :ok false;
|
|
211
199
|
}
|
|
212
200
|
break :ok true;
|
|
213
201
|
};
|
|
214
|
-
|
|
215
202
|
assert(struggle);
|
|
216
|
-
saveMove(battle, player, choice);
|
|
217
|
-
} else {
|
|
218
|
-
saveMove(battle, player, choice);
|
|
219
203
|
}
|
|
204
|
+
saveMove(battle, player, choice);
|
|
220
205
|
|
|
221
206
|
return null;
|
|
222
207
|
}
|
|
@@ -229,23 +214,19 @@ fn saveMove(battle: anytype, player: Player, choice: ?Choice) void {
|
|
|
229
214
|
if (c.data == 0) {
|
|
230
215
|
side.last_selected_move = .Struggle;
|
|
231
216
|
} else {
|
|
232
|
-
assert(showdown or side.active.volatiles.
|
|
217
|
+
assert(showdown or side.active.volatiles.disable_move != c.data);
|
|
233
218
|
const move = side.active.move(c.data);
|
|
234
219
|
// You cannot *select* a move with 0 PP (except on Pokémon Showdown where that is
|
|
235
220
|
// sometimes required...), but a 0 PP move can be used automatically
|
|
236
221
|
assert(showdown or move.pp != 0);
|
|
237
222
|
|
|
238
223
|
side.last_selected_move = move.id;
|
|
239
|
-
|
|
240
|
-
battle.last_moves.p1_index = @intCast(u3, c.data);
|
|
241
|
-
} else {
|
|
242
|
-
battle.last_moves.p2_index = @intCast(u3, c.data);
|
|
243
|
-
}
|
|
224
|
+
battle.lastMove(player).index = @intCast(c.data);
|
|
244
225
|
}
|
|
245
226
|
}
|
|
246
227
|
}
|
|
247
228
|
|
|
248
|
-
fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool,
|
|
229
|
+
fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: anytype) !void {
|
|
249
230
|
var side = battle.side(player);
|
|
250
231
|
var foe = battle.foe(player);
|
|
251
232
|
var active = &side.active;
|
|
@@ -258,11 +239,7 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, log: anyty
|
|
|
258
239
|
side.order[0] = side.order[slot - 1];
|
|
259
240
|
side.order[slot - 1] = out;
|
|
260
241
|
|
|
261
|
-
|
|
262
|
-
battle.last_moves.p1_index = 1;
|
|
263
|
-
} else {
|
|
264
|
-
battle.last_moves.p2_index = 1;
|
|
265
|
-
}
|
|
242
|
+
battle.lastMove(player).index = 1;
|
|
266
243
|
|
|
267
244
|
side.last_used_move = .None;
|
|
268
245
|
foe.last_used_move = .None;
|
|
@@ -278,17 +255,18 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, log: anyty
|
|
|
278
255
|
|
|
279
256
|
foe.active.volatiles.Binding = false;
|
|
280
257
|
|
|
281
|
-
try log.switched(battle.active(player), incoming);
|
|
258
|
+
try options.log.switched(battle.active(player), incoming);
|
|
259
|
+
options.chance.switched(player, side.order[0], out);
|
|
282
260
|
|
|
283
261
|
if (showdown and incoming.status == Status.TOX) {
|
|
284
262
|
incoming.status = Status.init(.PSN);
|
|
285
263
|
// Technically, Pokémon Showdown adds these after *both* Pokémon have switched, but we'd
|
|
286
264
|
// rather not clutter up turnOrder just for this (incorrect) log message
|
|
287
|
-
try log.status(battle.active(player), incoming.status, .Silent);
|
|
265
|
+
try options.log.status(battle.active(player), incoming.status, .Silent);
|
|
288
266
|
}
|
|
289
267
|
}
|
|
290
268
|
|
|
291
|
-
fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
|
|
269
|
+
fn turnOrder(battle: anytype, c1: Choice, c2: Choice, options: anytype) !Player {
|
|
292
270
|
assert(c1.type != .Pass or c2.type != .Pass);
|
|
293
271
|
|
|
294
272
|
if (c1.type == .Pass) return .P2;
|
|
@@ -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,
|
|
399
|
+
try switchIn(battle, player, choice.data, false, options);
|
|
424
400
|
return null;
|
|
425
401
|
}
|
|
426
402
|
|
|
@@ -430,13 +406,13 @@ 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
|
}
|
|
437
413
|
|
|
438
414
|
assert(choice.type == .Move);
|
|
439
|
-
var mslot = @intCast(
|
|
415
|
+
var mslot: u4 = @intCast(choice.data);
|
|
440
416
|
// Sadly, we can't even check `Move.get(side.last_selected_move).effect == .Binding` here
|
|
441
417
|
// because Pokémon Showdown's Mirror Move implementation clobbers side.last_selected_move
|
|
442
418
|
var auto = showdown and side.last_selected_move != .None;
|
|
@@ -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 = @boolToInt(counterable);
|
|
784
|
-
} else {
|
|
785
|
-
battle.last_moves.p2_counterable = @boolToInt(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
|
|
|
@@ -808,12 +805,12 @@ fn incrementPP(side: *Side, mslot: u4) void {
|
|
|
808
805
|
var active = &side.active;
|
|
809
806
|
const volatiles = &active.volatiles;
|
|
810
807
|
|
|
811
|
-
active.move(mslot).pp = @
|
|
808
|
+
active.move(mslot).pp = @as(u6, @intCast(active.move(mslot).pp)) +% 1;
|
|
812
809
|
// GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
|
|
813
810
|
if (showdown and volatiles.Transform) return;
|
|
814
811
|
|
|
815
812
|
assert(mslot > 0 and mslot <= 4);
|
|
816
|
-
side.stored().moves[mslot - 1].pp = @
|
|
813
|
+
side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% 1;
|
|
817
814
|
}
|
|
818
815
|
|
|
819
816
|
// Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
|
|
@@ -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,23 +838,27 @@ 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
|
|
849
850
|
// calling moveHit early covers most of that but we also need to check type immunity first
|
|
850
851
|
var miss = showdown and miss: {
|
|
851
852
|
immune = move.target != .Self and !status and !counter and
|
|
852
|
-
(@
|
|
853
|
-
@
|
|
853
|
+
(@intFromEnum(move.type.effectiveness(foe.active.types.type1)) == 0 or
|
|
854
|
+
@intFromEnum(move.type.effectiveness(foe.active.types.type2)) == 0);
|
|
854
855
|
if (immune and move.effect != .Binding) break :miss true;
|
|
855
856
|
if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) {
|
|
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,29 +936,34 @@ 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
|
}
|
|
942
943
|
if (move.effect == .JumpKick) {
|
|
943
944
|
// Recoil is supposed to be damage/8 but damage will always be 0 here
|
|
944
|
-
assert(battle.last_damage == 0);
|
|
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(
|
|
@@ -1123,7 +1130,7 @@ fn calcDamage(
|
|
|
1123
1130
|
d = @min(997, d);
|
|
1124
1131
|
d += 2;
|
|
1125
1132
|
|
|
1126
|
-
battle.last_damage = @intCast(
|
|
1133
|
+
battle.last_damage = @intCast(d);
|
|
1127
1134
|
|
|
1128
1135
|
return true;
|
|
1129
1136
|
}
|
|
@@ -1137,9 +1144,9 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
|
1137
1144
|
var d = battle.last_damage;
|
|
1138
1145
|
if (side.active.types.includes(move.type)) d +%= d / 2;
|
|
1139
1146
|
|
|
1140
|
-
const neutral = @
|
|
1141
|
-
const eff1: u16 = @
|
|
1142
|
-
const eff2: u16 = @
|
|
1147
|
+
const neutral = @intFromEnum(Effectiveness.Neutral);
|
|
1148
|
+
const eff1: u16 = @intFromEnum(move.type.effectiveness(types.type1));
|
|
1149
|
+
const eff2: u16 = @intFromEnum(move.type.effectiveness(types.type2));
|
|
1143
1150
|
|
|
1144
1151
|
// Type effectiveness matchup precedence only matters with (NVE, SE)
|
|
1145
1152
|
if (!showdown and (eff1 + eff2) == Effectiveness.mismatch and
|
|
@@ -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);
|
|
1165
|
-
|
|
1171
|
+
const random = try Rolls.damage(battle, player, options);
|
|
1172
|
+
options.calc.base(player.foe(), battle.last_damage);
|
|
1173
|
+
battle.last_damage = @intCast(@as(u32, battle.last_damage) *% random / 255);
|
|
1174
|
+
options.calc.final(player.foe(), battle.last_damage);
|
|
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),
|
|
@@ -1178,10 +1188,10 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
|
|
|
1178
1188
|
.DragonRage => 40,
|
|
1179
1189
|
// GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
|
|
1180
1190
|
.Psywave => power: {
|
|
1181
|
-
const max = @intCast(
|
|
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
|
-
subbed.active.volatiles.substitute -= @intCast(
|
|
1271
|
-
try log.activate(battle.active(sub_player), .Substitute);
|
|
1278
|
+
subbed.active.volatiles.substitute -= @intCast(battle.last_damage);
|
|
1279
|
+
try options.log.activate(battle.active(sub_player), .Substitute);
|
|
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[@
|
|
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[@
|
|
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
|
|
@@ -1400,7 +1423,7 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
|
|
|
1400
1423
|
}
|
|
1401
1424
|
|
|
1402
1425
|
if (!miss) return true;
|
|
1403
|
-
battle.last_damage = 0;
|
|
1426
|
+
if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
|
|
1404
1427
|
side.active.volatiles.Binding = false;
|
|
1405
1428
|
return false;
|
|
1406
1429
|
}
|
|
@@ -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
|
|
|
@@ -1421,29 +1444,29 @@ fn checkFaint(
|
|
|
1421
1444
|
const tie = player_out and foe_out;
|
|
1422
1445
|
const more = tie or player_out or foe_out;
|
|
1423
1446
|
|
|
1424
|
-
if (try faint(battle, player,
|
|
1425
|
-
if (foe_fainted) if (try faint(battle, player.foe(),
|
|
1447
|
+
if (try faint(battle, player, !(more or foe_fainted), options)) |r| return r;
|
|
1448
|
+
if (foe_fainted) if (try faint(battle, player.foe(), !more, options)) |r| return r;
|
|
1426
1449
|
|
|
1427
1450
|
assert(!side.active.volatiles.MultiHit);
|
|
1428
1451
|
assert(!foe.active.volatiles.MultiHit);
|
|
1429
1452
|
|
|
1430
1453
|
if (tie) {
|
|
1431
|
-
try log.tie();
|
|
1454
|
+
try options.log.tie();
|
|
1432
1455
|
return Result.Tie;
|
|
1433
1456
|
} else if (player_out) {
|
|
1434
|
-
try log.win(player.foe());
|
|
1457
|
+
try options.log.win(player.foe());
|
|
1435
1458
|
return if (player == .P1) Result.Lose else Result.Win;
|
|
1436
1459
|
} else if (foe_out) {
|
|
1437
|
-
try log.win(player);
|
|
1460
|
+
try options.log.win(player);
|
|
1438
1461
|
return if (player == .P1) Result.Win else Result.Lose;
|
|
1439
1462
|
}
|
|
1440
1463
|
|
|
1441
1464
|
const foe_choice: Choice.Type = if (foe_fainted) .Switch else .Pass;
|
|
1442
|
-
if (player == .P1) return
|
|
1443
|
-
return
|
|
1465
|
+
if (player == .P1) return .{ .p1 = .Switch, .p2 = foe_choice };
|
|
1466
|
+
return .{ .p1 = foe_choice, .p2 = .Switch };
|
|
1444
1467
|
}
|
|
1445
1468
|
|
|
1446
|
-
fn faint(battle: anytype, player: Player,
|
|
1469
|
+
fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
|
|
1447
1470
|
var side = battle.side(player);
|
|
1448
1471
|
var foe = battle.foe(player);
|
|
1449
1472
|
assert(side.stored().hp == 0);
|
|
@@ -1476,32 +1499,31 @@ fn faint(battle: anytype, player: Player, log: anytype, done: bool) !?Result {
|
|
|
1476
1499
|
side.stored().stats.spe;
|
|
1477
1500
|
}
|
|
1478
1501
|
|
|
1479
|
-
try log.faint(battle.active(player), done);
|
|
1502
|
+
try options.log.faint(battle.active(player), done);
|
|
1503
|
+
options.calc.capped(player);
|
|
1480
1504
|
return null;
|
|
1481
1505
|
}
|
|
1482
1506
|
|
|
1483
|
-
fn handleResidual(battle: anytype, player: Player,
|
|
1507
|
+
fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
|
|
1484
1508
|
var side = battle.side(player);
|
|
1485
1509
|
var stored = side.stored();
|
|
1486
1510
|
const ident = battle.active(player);
|
|
1487
1511
|
var volatiles = &side.active.volatiles;
|
|
1488
1512
|
|
|
1489
1513
|
const brn = Status.is(stored.status, .BRN);
|
|
1490
|
-
if (brn or Status.is(stored.status, .PSN))
|
|
1514
|
+
if (brn or Status.is(stored.status, .PSN)) {
|
|
1491
1515
|
var damage = @max(stored.stats.hp / 16, 1);
|
|
1492
|
-
|
|
1493
1516
|
if (volatiles.Toxic) {
|
|
1494
1517
|
volatiles.toxic += 1;
|
|
1495
1518
|
damage *= volatiles.toxic;
|
|
1496
1519
|
}
|
|
1520
|
+
damage = @min(damage, stored.hp);
|
|
1497
1521
|
|
|
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);
|
|
1522
|
+
if (!showdown or damage > 0) {
|
|
1523
|
+
stored.hp -= damage;
|
|
1524
|
+
// Pokémon Showdown uses damageOf here but its not relevant in Generation I
|
|
1525
|
+
try options.log.damage(ident, stored, if (brn) Damage.Burn else Damage.Poison);
|
|
1526
|
+
}
|
|
1505
1527
|
}
|
|
1506
1528
|
|
|
1507
1529
|
if (volatiles.LeechSeed) {
|
|
@@ -1526,42 +1548,42 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
|
|
|
1526
1548
|
stored.hp -= amount;
|
|
1527
1549
|
|
|
1528
1550
|
// As above, Pokémon Showdown uses damageOf but its not relevant
|
|
1529
|
-
if (amount > 0) try log.damage(ident, stored, .LeechSeed);
|
|
1551
|
+
if (amount > 0) try options.log.damage(ident, stored, .LeechSeed);
|
|
1530
1552
|
|
|
1531
1553
|
const before = foe_stored.hp;
|
|
1532
1554
|
// Uncapped damage is added back to the foe
|
|
1533
1555
|
foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
|
|
1534
1556
|
// 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);
|
|
1557
|
+
if (foe_stored.hp > before) try options.log.heal(foe_ident, foe_stored, .Silent);
|
|
1536
1558
|
}
|
|
1537
1559
|
}
|
|
1538
1560
|
|
|
1539
|
-
fn endTurn(battle: anytype,
|
|
1561
|
+
fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result {
|
|
1540
1562
|
assert(!battle.side(.P1).active.volatiles.MultiHit);
|
|
1541
1563
|
assert(!battle.side(.P2).active.volatiles.MultiHit);
|
|
1542
1564
|
|
|
1543
1565
|
battle.turn += 1;
|
|
1544
1566
|
|
|
1545
|
-
if (showdown and options.ebc and checkEBC(battle)) {
|
|
1546
|
-
try log.tie();
|
|
1567
|
+
if (showdown and pkmn.options.ebc and checkEBC(battle)) {
|
|
1568
|
+
try options.log.tie();
|
|
1547
1569
|
return Result.Tie;
|
|
1548
1570
|
}
|
|
1549
1571
|
|
|
1550
1572
|
if (showdown and battle.turn >= 1000) {
|
|
1551
|
-
try log.tie();
|
|
1573
|
+
try options.log.tie();
|
|
1552
1574
|
return Result.Tie;
|
|
1553
1575
|
} else if (battle.turn >= 65535) {
|
|
1554
1576
|
return Result.Error;
|
|
1555
1577
|
}
|
|
1556
1578
|
|
|
1557
|
-
try log.turn(battle.turn);
|
|
1579
|
+
try options.log.turn(battle.turn);
|
|
1558
1580
|
|
|
1559
1581
|
return Result.Default;
|
|
1560
1582
|
}
|
|
1561
1583
|
|
|
1562
1584
|
fn checkEBC(battle: anytype) bool {
|
|
1563
1585
|
for (battle.sides, 0..) |side, i| {
|
|
1564
|
-
const foe = battle.sides[~@
|
|
1586
|
+
const foe = battle.sides[~@as(u1, @intCast(i))];
|
|
1565
1587
|
|
|
1566
1588
|
var foe_all_ghosts = true;
|
|
1567
1589
|
var foe_all_transform = true;
|
|
@@ -1613,10 +1635,10 @@ fn checkEBC(battle: anytype) bool {
|
|
|
1613
1635
|
return false;
|
|
1614
1636
|
}
|
|
1615
1637
|
|
|
1616
|
-
fn disabled(side: *Side, ident: ID,
|
|
1617
|
-
if (side.active.volatiles.
|
|
1638
|
+
fn disabled(side: *Side, ident: ID, options: anytype) !bool {
|
|
1639
|
+
if (side.active.volatiles.disable_move != 0) {
|
|
1618
1640
|
// A Pokémon that transforms after being disabled may end up with less move slots
|
|
1619
|
-
const m = side.active.moves[side.active.volatiles.
|
|
1641
|
+
const m = side.active.moves[side.active.volatiles.disable_move - 1].id;
|
|
1620
1642
|
// side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
|
|
1621
1643
|
const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
|
|
1622
1644
|
m
|
|
@@ -1624,28 +1646,28 @@ fn disabled(side: *Side, ident: ID, log: anytype) !bool {
|
|
|
1624
1646
|
side.last_selected_move;
|
|
1625
1647
|
if (m != .None and m == last) {
|
|
1626
1648
|
side.active.volatiles.Charging = false;
|
|
1627
|
-
try log.disabled(ident, last);
|
|
1649
|
+
try options.log.disabled(ident, last);
|
|
1628
1650
|
return true;
|
|
1629
1651
|
}
|
|
1630
1652
|
}
|
|
1631
1653
|
return false;
|
|
1632
1654
|
}
|
|
1633
1655
|
|
|
1634
|
-
inline fn buildRage(battle: anytype, who: Player,
|
|
1656
|
+
inline fn buildRage(battle: anytype, who: Player, options: anytype) !void {
|
|
1635
1657
|
const side = battle.side(who);
|
|
1636
1658
|
if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
|
|
1637
|
-
try Effects.boost(battle, who, Move.get(.Rage),
|
|
1659
|
+
try Effects.boost(battle, who, Move.get(.Rage), options);
|
|
1638
1660
|
}
|
|
1639
1661
|
}
|
|
1640
1662
|
|
|
1641
|
-
fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
|
|
1663
|
+
fn handleThrashing(battle: anytype, active: *ActivePokemon, player: Player, options: anytype) bool {
|
|
1642
1664
|
var volatiles = &active.volatiles;
|
|
1643
1665
|
assert(volatiles.Thrashing);
|
|
1644
1666
|
if (volatiles.attacks > 0) return false;
|
|
1645
1667
|
|
|
1646
1668
|
volatiles.Thrashing = false;
|
|
1647
1669
|
volatiles.Confusion = true;
|
|
1648
|
-
volatiles.confusion = Rolls.confusionDuration(battle);
|
|
1670
|
+
volatiles.confusion = Rolls.confusionDuration(battle, player, true, options);
|
|
1649
1671
|
return true;
|
|
1650
1672
|
}
|
|
1651
1673
|
|
|
@@ -1655,42 +1677,42 @@ inline fn onBegin(
|
|
|
1655
1677
|
move: Move.Data,
|
|
1656
1678
|
mslot: u8,
|
|
1657
1679
|
residual: *bool,
|
|
1658
|
-
|
|
1680
|
+
options: anytype,
|
|
1659
1681
|
) !void {
|
|
1660
1682
|
assert(move.effect.onBegin());
|
|
1661
1683
|
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,
|
|
1684
|
+
.Confusion => Effects.confusion(battle, player, move, options),
|
|
1685
|
+
.Conversion => Effects.conversion(battle, player, options),
|
|
1686
|
+
.FocusEnergy => Effects.focusEnergy(battle, player, options),
|
|
1687
|
+
.Haze => Effects.haze(battle, player, options),
|
|
1688
|
+
.Heal => Effects.heal(battle, player, options),
|
|
1689
|
+
.LeechSeed => Effects.leechSeed(battle, player, move, options),
|
|
1690
|
+
.LightScreen => Effects.lightScreen(battle, player, options),
|
|
1691
|
+
.Mimic => Effects.mimic(battle, player, move, mslot, options),
|
|
1692
|
+
.Mist => Effects.mist(battle, player, options),
|
|
1693
|
+
.Paralyze => Effects.paralyze(battle, player, move, options),
|
|
1694
|
+
.Poison => Effects.poison(battle, player, move, options),
|
|
1695
|
+
.Reflect => Effects.reflect(battle, player, options),
|
|
1696
|
+
.Splash => Effects.splash(battle, player, options),
|
|
1697
|
+
.Substitute => Effects.substitute(battle, player, residual, options),
|
|
1698
|
+
.SwitchAndTeleport => Effects.switchAndTeleport(battle, player, move, options),
|
|
1699
|
+
.Transform => Effects.transform(battle, player, options),
|
|
1678
1700
|
else => unreachable,
|
|
1679
1701
|
};
|
|
1680
1702
|
}
|
|
1681
1703
|
|
|
1682
|
-
inline fn onEnd(battle: anytype, player: Player, move: Move.Data,
|
|
1704
|
+
inline fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1683
1705
|
assert(move.effect.onEnd());
|
|
1684
1706
|
return switch (move.effect) {
|
|
1685
|
-
.Bide => Effects.bide(battle, player,
|
|
1707
|
+
.Bide => Effects.bide(battle, player, options),
|
|
1686
1708
|
// zig fmt: off
|
|
1687
1709
|
.AttackUp1, .AttackUp2, .DefenseUp1, .DefenseUp2,
|
|
1688
1710
|
.EvasionUp1, .SpecialUp1, .SpecialUp2, .SpeedUp2 =>
|
|
1689
|
-
Effects.boost(battle, player, move,
|
|
1711
|
+
Effects.boost(battle, player, move, options),
|
|
1690
1712
|
.AccuracyDown1, .AttackDown1, .DefenseDown1, .DefenseDown2, .SpeedDown1 =>
|
|
1691
|
-
Effects.unboost(battle, player, move,
|
|
1713
|
+
Effects.unboost(battle, player, move, options),
|
|
1692
1714
|
// zig fmt: on
|
|
1693
|
-
.Sleep => Effects.sleep(battle, player, move,
|
|
1715
|
+
.Sleep => Effects.sleep(battle, player, move, options),
|
|
1694
1716
|
else => unreachable,
|
|
1695
1717
|
};
|
|
1696
1718
|
}
|
|
@@ -1700,51 +1722,51 @@ inline fn alwaysHappens(
|
|
|
1700
1722
|
player: Player,
|
|
1701
1723
|
move: Move.Data,
|
|
1702
1724
|
residual: *bool,
|
|
1703
|
-
|
|
1725
|
+
options: anytype,
|
|
1704
1726
|
) !void {
|
|
1705
1727
|
assert(move.effect.alwaysHappens());
|
|
1706
1728
|
return switch (move.effect) {
|
|
1707
|
-
.DrainHP, .DreamEater => Effects.drainHP(battle, player,
|
|
1729
|
+
.DrainHP, .DreamEater => Effects.drainHP(battle, player, options),
|
|
1708
1730
|
.Explode => Effects.explode(battle, player),
|
|
1709
|
-
.PayDay => Effects.payDay(battle, player,
|
|
1731
|
+
.PayDay => Effects.payDay(battle, player, options),
|
|
1710
1732
|
.Rage => Effects.rage(battle, player),
|
|
1711
|
-
.Recoil => Effects.recoil(battle, player, residual,
|
|
1733
|
+
.Recoil => Effects.recoil(battle, player, residual, options),
|
|
1712
1734
|
.JumpKick, .Binding => {},
|
|
1713
1735
|
else => unreachable,
|
|
1714
1736
|
};
|
|
1715
1737
|
}
|
|
1716
1738
|
|
|
1717
|
-
inline fn moveEffect(battle: anytype, player: Player, move: Move.Data,
|
|
1739
|
+
inline fn moveEffect(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1718
1740
|
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,
|
|
1741
|
+
.BurnChance1, .BurnChance2 => Effects.burnChance(battle, player, move, options),
|
|
1742
|
+
.ConfusionChance => Effects.confusion(battle, player, move, options),
|
|
1743
|
+
.FlinchChance1, .FlinchChance2 => Effects.flinchChance(battle, player, move, options),
|
|
1744
|
+
.FreezeChance => Effects.freezeChance(battle, player, move, options),
|
|
1745
|
+
.HyperBeam => Effects.hyperBeam(battle, player, options),
|
|
1724
1746
|
.MultiHit, .DoubleHit, .Twineedle => unreachable,
|
|
1725
|
-
.ParalyzeChance1, .ParalyzeChance2 => Effects.paralyzeChance(battle, player, move,
|
|
1726
|
-
.PoisonChance1, .PoisonChance2 => Effects.poison(battle, player, move,
|
|
1747
|
+
.ParalyzeChance1, .ParalyzeChance2 => Effects.paralyzeChance(battle, player, move, options),
|
|
1748
|
+
.PoisonChance1, .PoisonChance2 => Effects.poison(battle, player, move, options),
|
|
1727
1749
|
// zig fmt: off
|
|
1728
1750
|
.AttackDownChance, .DefenseDownChance, .SpecialDownChance, .SpeedDownChance =>
|
|
1729
|
-
Effects.unboost(battle, player, move,
|
|
1751
|
+
Effects.unboost(battle, player, move, options),
|
|
1730
1752
|
// zig fmt: on
|
|
1731
1753
|
else => {},
|
|
1732
1754
|
};
|
|
1733
1755
|
}
|
|
1734
1756
|
|
|
1735
1757
|
pub const Effects = struct {
|
|
1736
|
-
fn bide(battle: anytype, player: Player,
|
|
1758
|
+
fn bide(battle: anytype, player: Player, options: anytype) !void {
|
|
1737
1759
|
var side = battle.side(player);
|
|
1738
1760
|
|
|
1739
1761
|
side.active.volatiles.Bide = true;
|
|
1740
1762
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1741
1763
|
side.active.volatiles.state = 0;
|
|
1742
|
-
side.active.volatiles.attacks = Rolls.
|
|
1764
|
+
side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
1743
1765
|
|
|
1744
|
-
try log.start(battle.active(player), .Bide);
|
|
1766
|
+
try options.log.start(battle.active(player), .Bide);
|
|
1745
1767
|
}
|
|
1746
1768
|
|
|
1747
|
-
fn burnChance(battle: anytype, player: Player, move: Move.Data,
|
|
1769
|
+
fn burnChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1748
1770
|
var foe = battle.foe(player);
|
|
1749
1771
|
var foe_stored = foe.stored();
|
|
1750
1772
|
|
|
@@ -1755,83 +1777,97 @@ pub const Effects = struct {
|
|
|
1755
1777
|
// GLITCH: Freeze top move selection desync can occur if thawed Pokémon is slower
|
|
1756
1778
|
if (Status.is(foe_stored.status, .FRZ)) {
|
|
1757
1779
|
assert(move.type == .Fire);
|
|
1758
|
-
try log.curestatus(
|
|
1780
|
+
try options.log.curestatus(
|
|
1781
|
+
battle.active(player.foe()),
|
|
1782
|
+
foe_stored.status,
|
|
1783
|
+
.Message,
|
|
1784
|
+
);
|
|
1759
1785
|
foe_stored.status = 0;
|
|
1760
1786
|
}
|
|
1761
1787
|
return;
|
|
1762
1788
|
}
|
|
1763
1789
|
|
|
1764
1790
|
if (foe.active.types.includes(move.type)) return;
|
|
1765
|
-
if (!Rolls.secondaryChance(battle, move.effect == .BurnChance1))
|
|
1791
|
+
if (!try Rolls.secondaryChance(battle, player, move.effect == .BurnChance1, options)) {
|
|
1792
|
+
return;
|
|
1793
|
+
}
|
|
1766
1794
|
|
|
1767
1795
|
foe_stored.status = Status.init(.BRN);
|
|
1768
1796
|
foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
|
|
1769
1797
|
|
|
1770
|
-
try log.status(battle.active(player.foe()), foe_stored.status, .None);
|
|
1798
|
+
try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
|
|
1771
1799
|
}
|
|
1772
1800
|
|
|
1773
|
-
fn charge(battle: anytype, player: Player,
|
|
1801
|
+
fn charge(battle: anytype, player: Player, options: anytype) !void {
|
|
1774
1802
|
var side = battle.side(player);
|
|
1775
1803
|
var volatiles = &side.active.volatiles;
|
|
1776
1804
|
|
|
1777
1805
|
volatiles.Charging = true;
|
|
1778
1806
|
const move = side.last_selected_move;
|
|
1779
1807
|
if (move == .Fly or move == .Dig) volatiles.Invulnerable = true;
|
|
1780
|
-
try log.laststill();
|
|
1781
|
-
try log.prepare(battle.active(player), move);
|
|
1808
|
+
try options.log.laststill();
|
|
1809
|
+
try options.log.prepare(battle.active(player), move);
|
|
1782
1810
|
}
|
|
1783
1811
|
|
|
1784
|
-
fn confusion(battle: anytype, player: Player, move: Move.Data,
|
|
1812
|
+
fn confusion(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1785
1813
|
var foe = battle.foe(player);
|
|
1786
1814
|
const sub = foe.active.volatiles.Substitute;
|
|
1787
1815
|
|
|
1788
1816
|
if (move.effect == .ConfusionChance) {
|
|
1789
|
-
if (!Rolls.confusionChance(battle)) return;
|
|
1817
|
+
if (!try Rolls.confusionChance(battle, player, options)) return;
|
|
1790
1818
|
} else {
|
|
1791
1819
|
if (showdown) {
|
|
1792
|
-
if (!try checkHit(battle, player, move,
|
|
1820
|
+
if (!try checkHit(battle, player, move, options)) {
|
|
1793
1821
|
return;
|
|
1794
1822
|
} else if (sub) {
|
|
1795
|
-
return log.fail(battle.active(player.foe()), .None);
|
|
1823
|
+
return options.log.fail(battle.active(player.foe()), .None);
|
|
1796
1824
|
}
|
|
1797
1825
|
} else {
|
|
1798
1826
|
if (sub) {
|
|
1799
|
-
return log.fail(battle.active(player.foe()), .None);
|
|
1800
|
-
} else if (!try checkHit(battle, player, move,
|
|
1827
|
+
return options.log.fail(battle.active(player.foe()), .None);
|
|
1828
|
+
} else if (!try checkHit(battle, player, move, options)) {
|
|
1801
1829
|
return;
|
|
1802
1830
|
}
|
|
1803
1831
|
}
|
|
1832
|
+
try options.chance.commit(player, .hit);
|
|
1804
1833
|
}
|
|
1805
1834
|
|
|
1806
1835
|
if (foe.active.volatiles.Confusion) return;
|
|
1807
1836
|
foe.active.volatiles.Confusion = true;
|
|
1808
|
-
foe.active.volatiles.confusion = Rolls.confusionDuration(battle);
|
|
1837
|
+
foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
|
|
1809
1838
|
|
|
1810
|
-
try log.start(battle.active(player.foe()), .Confusion);
|
|
1839
|
+
try options.log.start(battle.active(player.foe()), .Confusion);
|
|
1811
1840
|
}
|
|
1812
1841
|
|
|
1813
|
-
fn conversion(battle: anytype, player: Player,
|
|
1842
|
+
fn conversion(battle: anytype, player: Player, options: anytype) !void {
|
|
1814
1843
|
const foe = battle.foe(player);
|
|
1815
1844
|
|
|
1816
1845
|
if (foe.active.volatiles.Invulnerable) {
|
|
1817
|
-
try log.lastmiss();
|
|
1818
|
-
return log.miss(battle.active(player));
|
|
1846
|
+
try options.log.lastmiss();
|
|
1847
|
+
return options.log.miss(battle.active(player));
|
|
1819
1848
|
}
|
|
1820
1849
|
|
|
1821
1850
|
battle.side(player).active.types = foe.active.types;
|
|
1822
|
-
return log.typechange(
|
|
1851
|
+
return options.log.typechange(
|
|
1852
|
+
battle.active(player),
|
|
1853
|
+
foe.active.types,
|
|
1854
|
+
battle.active(player.foe()),
|
|
1855
|
+
);
|
|
1823
1856
|
}
|
|
1824
1857
|
|
|
1825
|
-
fn disable(battle: anytype, player: Player, move: Move.Data,
|
|
1858
|
+
fn disable(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
|
|
1826
1859
|
var foe = battle.foe(player);
|
|
1827
1860
|
var volatiles = &foe.active.volatiles;
|
|
1828
1861
|
const foe_ident = battle.active(player.foe());
|
|
1829
1862
|
|
|
1830
1863
|
// Pokémon Showdown handles hit/miss earlier in doMove
|
|
1831
|
-
if (!showdown
|
|
1864
|
+
if (!showdown) {
|
|
1865
|
+
if (!try checkHit(battle, player, move, options)) return null;
|
|
1866
|
+
try options.chance.commit(player, .hit);
|
|
1867
|
+
}
|
|
1832
1868
|
|
|
1833
|
-
if (volatiles.
|
|
1834
|
-
try log.fail(foe_ident, .None);
|
|
1869
|
+
if (volatiles.disable_move != 0) {
|
|
1870
|
+
try options.log.fail(foe_ident, .None);
|
|
1835
1871
|
return null;
|
|
1836
1872
|
}
|
|
1837
1873
|
|
|
@@ -1847,7 +1883,7 @@ pub const Effects = struct {
|
|
|
1847
1883
|
// Technically this is still considered simply a "miss" on the cartridge,
|
|
1848
1884
|
// but diverging from Pokémon Showdown here would mostly just be pedantic
|
|
1849
1885
|
if (n == 0) {
|
|
1850
|
-
try log.fail(foe_ident, .None);
|
|
1886
|
+
try options.log.fail(foe_ident, .None);
|
|
1851
1887
|
return null;
|
|
1852
1888
|
} else if (err) {
|
|
1853
1889
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
@@ -1855,14 +1891,16 @@ pub const Effects = struct {
|
|
|
1855
1891
|
return Result.Error;
|
|
1856
1892
|
}
|
|
1857
1893
|
|
|
1858
|
-
volatiles.
|
|
1859
|
-
|
|
1894
|
+
volatiles.disable_move =
|
|
1895
|
+
@intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
|
|
1896
|
+
volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
|
|
1860
1897
|
|
|
1861
|
-
|
|
1898
|
+
const id = foe.active.move(volatiles.disable_move).id;
|
|
1899
|
+
try options.log.startEffect(foe_ident, .Disable, id);
|
|
1862
1900
|
return null;
|
|
1863
1901
|
}
|
|
1864
1902
|
|
|
1865
|
-
fn drainHP(battle: anytype, player: Player,
|
|
1903
|
+
fn drainHP(battle: anytype, player: Player, options: anytype) !void {
|
|
1866
1904
|
var side = battle.side(player);
|
|
1867
1905
|
var stored = side.stored();
|
|
1868
1906
|
|
|
@@ -1877,7 +1915,7 @@ pub const Effects = struct {
|
|
|
1877
1915
|
if (stored.hp == stored.stats.hp) return;
|
|
1878
1916
|
stored.hp = @min(stored.stats.hp, stored.hp + drain);
|
|
1879
1917
|
|
|
1880
|
-
try log.drain(battle.active(player), stored, battle.active(player.foe()));
|
|
1918
|
+
try options.log.drain(battle.active(player), stored, battle.active(player.foe()));
|
|
1881
1919
|
}
|
|
1882
1920
|
|
|
1883
1921
|
fn explode(battle: anytype, player: Player) !void {
|
|
@@ -1891,26 +1929,28 @@ pub const Effects = struct {
|
|
|
1891
1929
|
side.active.volatiles.LeechSeed = false;
|
|
1892
1930
|
}
|
|
1893
1931
|
|
|
1894
|
-
fn flinchChance(battle: anytype, player: Player, move: Move.Data) void {
|
|
1932
|
+
fn flinchChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1895
1933
|
var volatiles = &battle.foe(player).active.volatiles;
|
|
1896
1934
|
|
|
1897
1935
|
if (volatiles.Substitute) return;
|
|
1898
|
-
if (!Rolls.secondaryChance(battle, move.effect == .FlinchChance1))
|
|
1936
|
+
if (!try Rolls.secondaryChance(battle, player, move.effect == .FlinchChance1, options)) {
|
|
1937
|
+
return;
|
|
1938
|
+
}
|
|
1899
1939
|
|
|
1900
1940
|
volatiles.Flinch = true;
|
|
1901
1941
|
volatiles.Recharging = false;
|
|
1902
1942
|
}
|
|
1903
1943
|
|
|
1904
|
-
fn focusEnergy(battle: anytype, player: Player,
|
|
1944
|
+
fn focusEnergy(battle: anytype, player: Player, options: anytype) !void {
|
|
1905
1945
|
var side = battle.side(player);
|
|
1906
1946
|
|
|
1907
1947
|
if (side.active.volatiles.FocusEnergy) return;
|
|
1908
1948
|
side.active.volatiles.FocusEnergy = true;
|
|
1909
1949
|
|
|
1910
|
-
try log.start(battle.active(player), .FocusEnergy);
|
|
1950
|
+
try options.log.start(battle.active(player), .FocusEnergy);
|
|
1911
1951
|
}
|
|
1912
1952
|
|
|
1913
|
-
fn freezeChance(battle: anytype, player: Player, move: Move.Data,
|
|
1953
|
+
fn freezeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1914
1954
|
var foe = battle.foe(player);
|
|
1915
1955
|
var foe_stored = foe.stored();
|
|
1916
1956
|
const foe_ident = battle.active(player.foe());
|
|
@@ -1922,17 +1962,18 @@ pub const Effects = struct {
|
|
|
1922
1962
|
}
|
|
1923
1963
|
|
|
1924
1964
|
if (foe.active.types.includes(move.type)) return;
|
|
1925
|
-
if (!Rolls.secondaryChance(battle, true)) return;
|
|
1965
|
+
if (!try Rolls.secondaryChance(battle, player, true, options)) return;
|
|
1926
1966
|
// Freeze Clause Mod
|
|
1927
1967
|
if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
1928
1968
|
|
|
1929
1969
|
foe_stored.status = Status.init(.FRZ);
|
|
1930
1970
|
// GLITCH: Hyper Beam recharging status is not cleared
|
|
1931
1971
|
|
|
1932
|
-
try log.status(foe_ident, foe_stored.status, .None);
|
|
1972
|
+
try options.log.status(foe_ident, foe_stored.status, .None);
|
|
1933
1973
|
}
|
|
1934
1974
|
|
|
1935
|
-
fn haze(battle: anytype, player: Player,
|
|
1975
|
+
fn haze(battle: anytype, player: Player, options: anytype) !void {
|
|
1976
|
+
var log = options.log;
|
|
1936
1977
|
var side = battle.side(player);
|
|
1937
1978
|
var foe = battle.foe(player);
|
|
1938
1979
|
|
|
@@ -1954,7 +1995,7 @@ pub const Effects = struct {
|
|
|
1954
1995
|
// Pokémon Showdown clears P1 then P2 instead of status -> side -> foe
|
|
1955
1996
|
if (showdown) {
|
|
1956
1997
|
for (&battle.sides, 0..) |*s, i| {
|
|
1957
|
-
const p = @
|
|
1998
|
+
const p: Player = @enumFromInt(i);
|
|
1958
1999
|
// Pokémon Showdown incorrectly does not prevent sleep/freeze from moving
|
|
1959
2000
|
if (p != player and Status.any(s.stored().status)) {
|
|
1960
2001
|
try log.curestatus(foe_ident, foe_stored.status, .Silent);
|
|
@@ -1963,7 +2004,7 @@ pub const Effects = struct {
|
|
|
1963
2004
|
s.stored().status = Status.init(.PSN);
|
|
1964
2005
|
try log.status(battle.active(p), s.stored().status, .None);
|
|
1965
2006
|
}
|
|
1966
|
-
try clearVolatiles(battle, p,
|
|
2007
|
+
try clearVolatiles(battle, p, p != player, options);
|
|
1967
2008
|
}
|
|
1968
2009
|
} else {
|
|
1969
2010
|
if (Status.any(foe_stored.status)) {
|
|
@@ -1973,71 +2014,76 @@ pub const Effects = struct {
|
|
|
1973
2014
|
try log.curestatus(foe_ident, foe_stored.status, .Silent);
|
|
1974
2015
|
foe_stored.status = 0;
|
|
1975
2016
|
}
|
|
1976
|
-
try clearVolatiles(battle, player,
|
|
1977
|
-
try clearVolatiles(battle, player.foe(),
|
|
2017
|
+
try clearVolatiles(battle, player, false, options);
|
|
2018
|
+
try clearVolatiles(battle, player.foe(), true, options);
|
|
1978
2019
|
}
|
|
1979
2020
|
}
|
|
1980
2021
|
|
|
1981
|
-
fn heal(battle: anytype, player: Player,
|
|
2022
|
+
fn heal(battle: anytype, player: Player, options: anytype) !void {
|
|
1982
2023
|
var side = battle.side(player);
|
|
1983
2024
|
var stored = side.stored();
|
|
1984
2025
|
const ident = battle.active(player);
|
|
1985
2026
|
|
|
1986
2027
|
// GLITCH: HP recovery move failure glitches
|
|
1987
2028
|
const delta = stored.stats.hp - stored.hp;
|
|
1988
|
-
if (delta == 0 or delta & 255 == 255
|
|
2029
|
+
if (delta == 0 or (delta & 255 == 255 and stored.hp % 256 != 0)) {
|
|
2030
|
+
return try options.log.fail(ident, .None);
|
|
2031
|
+
}
|
|
1989
2032
|
|
|
1990
2033
|
const rest = side.last_selected_move == .Rest;
|
|
1991
2034
|
if (rest) {
|
|
1992
2035
|
// Adding the sleep status runs the sleep condition handler to roll duration
|
|
1993
2036
|
if (showdown) battle.rng.advance(1);
|
|
1994
2037
|
stored.status = Status.slf(2);
|
|
1995
|
-
try log.statusFrom(ident, stored.status, Move.Rest);
|
|
2038
|
+
try options.log.statusFrom(ident, stored.status, Move.Rest);
|
|
1996
2039
|
stored.hp = stored.stats.hp;
|
|
1997
2040
|
} else {
|
|
1998
2041
|
stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
|
|
1999
2042
|
}
|
|
2000
|
-
try log.heal(ident, stored, if (rest) Heal.Silent else Heal.None);
|
|
2043
|
+
try options.log.heal(ident, stored, if (rest) Heal.Silent else Heal.None);
|
|
2001
2044
|
}
|
|
2002
2045
|
|
|
2003
|
-
fn hyperBeam(battle: anytype, player: Player,
|
|
2046
|
+
fn hyperBeam(battle: anytype, player: Player, options: anytype) !void {
|
|
2004
2047
|
battle.side(player).active.volatiles.Recharging = true;
|
|
2005
|
-
try log.mustrecharge(battle.active(player));
|
|
2048
|
+
try options.log.mustrecharge(battle.active(player));
|
|
2006
2049
|
}
|
|
2007
2050
|
|
|
2008
|
-
fn leechSeed(battle: anytype, player: Player, move: Move.Data,
|
|
2051
|
+
fn leechSeed(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
2009
2052
|
var foe = battle.foe(player);
|
|
2010
2053
|
|
|
2011
2054
|
if (showdown) {
|
|
2012
2055
|
// Invulnerability trumps type immunity on Pokémon Showdown
|
|
2013
2056
|
if (!foe.active.volatiles.Invulnerable and foe.active.types.includes(.Grass)) {
|
|
2014
|
-
return log.immune(battle.active(player.foe()), .None);
|
|
2057
|
+
return options.log.immune(battle.active(player.foe()), .None);
|
|
2015
2058
|
}
|
|
2016
|
-
if (!try checkHit(battle, player, move,
|
|
2059
|
+
if (!try checkHit(battle, player, move, options)) return;
|
|
2017
2060
|
if (foe.active.volatiles.LeechSeed) return;
|
|
2018
2061
|
} else {
|
|
2019
|
-
if (!try checkHit(battle, player, move,
|
|
2062
|
+
if (!try checkHit(battle, player, move, options)) return;
|
|
2020
2063
|
if (foe.active.types.includes(.Grass) or foe.active.volatiles.LeechSeed) {
|
|
2021
|
-
try log.lastmiss();
|
|
2022
|
-
return log.miss(battle.active(player));
|
|
2064
|
+
try options.log.lastmiss();
|
|
2065
|
+
return options.log.miss(battle.active(player));
|
|
2023
2066
|
}
|
|
2024
2067
|
}
|
|
2025
2068
|
|
|
2069
|
+
try options.chance.commit(player, .hit);
|
|
2026
2070
|
foe.active.volatiles.LeechSeed = true;
|
|
2027
2071
|
|
|
2028
|
-
try log.start(battle.active(player.foe()), .LeechSeed);
|
|
2072
|
+
try options.log.start(battle.active(player.foe()), .LeechSeed);
|
|
2029
2073
|
}
|
|
2030
2074
|
|
|
2031
|
-
fn lightScreen(battle: anytype, player: Player,
|
|
2075
|
+
fn lightScreen(battle: anytype, player: Player, options: anytype) !void {
|
|
2032
2076
|
var side = battle.side(player);
|
|
2033
2077
|
|
|
2034
|
-
if (side.active.volatiles.LightScreen)
|
|
2078
|
+
if (side.active.volatiles.LightScreen) {
|
|
2079
|
+
return options.log.fail(battle.active(player), .None);
|
|
2080
|
+
}
|
|
2035
2081
|
side.active.volatiles.LightScreen = true;
|
|
2036
2082
|
|
|
2037
|
-
try log.start(battle.active(player), .LightScreen);
|
|
2083
|
+
try options.log.start(battle.active(player), .LightScreen);
|
|
2038
2084
|
}
|
|
2039
2085
|
|
|
2040
|
-
fn mimic(battle: anytype, player: Player, move: Move.Data, mslot: u8,
|
|
2086
|
+
fn mimic(battle: anytype, player: Player, move: Move.Data, mslot: u8, options: anytype) !void {
|
|
2041
2087
|
var side = battle.side(player);
|
|
2042
2088
|
var foe = battle.foe(player);
|
|
2043
2089
|
|
|
@@ -2053,7 +2099,7 @@ pub const Effects = struct {
|
|
|
2053
2099
|
const has_mimic = has_mimic: {
|
|
2054
2100
|
for (side.active.moves, 0..) |m, i| {
|
|
2055
2101
|
if (m.id == .Mimic) {
|
|
2056
|
-
oslot = @intCast(
|
|
2102
|
+
oslot = @intCast(i + 1);
|
|
2057
2103
|
break :has_mimic true;
|
|
2058
2104
|
}
|
|
2059
2105
|
}
|
|
@@ -2061,38 +2107,43 @@ pub const Effects = struct {
|
|
|
2061
2107
|
};
|
|
2062
2108
|
if (!has_mimic) {
|
|
2063
2109
|
// Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
|
|
2064
|
-
if (!try checkHit(battle, player, move,
|
|
2065
|
-
|
|
2110
|
+
if (!try checkHit(battle, player, move, options)) return;
|
|
2111
|
+
try options.chance.commit(player, .hit);
|
|
2112
|
+
return try options.log.fail(battle.active(player.foe()), .None);
|
|
2066
2113
|
}
|
|
2067
2114
|
}
|
|
2068
|
-
if (!try checkHit(battle, player, move,
|
|
2115
|
+
if (!try checkHit(battle, player, move, options)) return;
|
|
2116
|
+
try options.chance.commit(player, .hit);
|
|
2069
2117
|
|
|
2070
|
-
const rslot = Rolls.moveSlot(battle, &foe.active.moves, 0);
|
|
2118
|
+
const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
|
|
2071
2119
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
2072
2120
|
|
|
2073
|
-
try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
|
|
2121
|
+
try options.log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
|
|
2074
2122
|
}
|
|
2075
2123
|
|
|
2076
|
-
fn mist(battle: anytype, player: Player,
|
|
2124
|
+
fn mist(battle: anytype, player: Player, options: anytype) !void {
|
|
2077
2125
|
var side = battle.side(player);
|
|
2078
2126
|
|
|
2079
2127
|
if (side.active.volatiles.Mist) return;
|
|
2080
2128
|
side.active.volatiles.Mist = true;
|
|
2081
2129
|
|
|
2082
|
-
try log.start(battle.active(player), .Mist);
|
|
2130
|
+
try options.log.start(battle.active(player), .Mist);
|
|
2083
2131
|
}
|
|
2084
2132
|
|
|
2085
|
-
fn multiHit(battle: anytype, player: Player, move: Move.Data) void {
|
|
2133
|
+
fn multiHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
2086
2134
|
var side = battle.side(player);
|
|
2087
2135
|
|
|
2088
2136
|
assert(!side.active.volatiles.MultiHit);
|
|
2089
2137
|
side.active.volatiles.MultiHit = true;
|
|
2090
2138
|
|
|
2091
|
-
side.active.volatiles.attacks =
|
|
2092
|
-
|
|
2139
|
+
side.active.volatiles.attacks = if (move.effect == .MultiHit)
|
|
2140
|
+
try Rolls.distribution(battle, .MultiHit, player, options)
|
|
2141
|
+
else
|
|
2142
|
+
2;
|
|
2093
2143
|
}
|
|
2094
2144
|
|
|
2095
|
-
fn paralyze(battle: anytype, player: Player, move: Move.Data,
|
|
2145
|
+
fn paralyze(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
2146
|
+
var log = options.log;
|
|
2096
2147
|
var foe = battle.foe(player);
|
|
2097
2148
|
var foe_stored = foe.stored();
|
|
2098
2149
|
const foe_ident = battle.active(player.foe());
|
|
@@ -2103,7 +2154,7 @@ pub const Effects = struct {
|
|
|
2103
2154
|
if (showdown) {
|
|
2104
2155
|
// Invulnerability trumps type immunity on Pokémon Showdown
|
|
2105
2156
|
if (immune and !foe.active.volatiles.Invulnerable) return log.immune(foe_ident, .None);
|
|
2106
|
-
if (!try checkHit(battle, player, move,
|
|
2157
|
+
if (!try checkHit(battle, player, move, options)) return;
|
|
2107
2158
|
}
|
|
2108
2159
|
if (Status.any(foe_stored.status)) {
|
|
2109
2160
|
return log.fail(
|
|
@@ -2113,8 +2164,9 @@ pub const Effects = struct {
|
|
|
2113
2164
|
}
|
|
2114
2165
|
if (!showdown) {
|
|
2115
2166
|
if (immune) return log.immune(foe_ident, .None);
|
|
2116
|
-
if (!try checkHit(battle, player, move,
|
|
2167
|
+
if (!try checkHit(battle, player, move, options)) return;
|
|
2117
2168
|
}
|
|
2169
|
+
try options.chance.commit(player, .hit);
|
|
2118
2170
|
|
|
2119
2171
|
foe_stored.status = Status.init(.PAR);
|
|
2120
2172
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
@@ -2122,7 +2174,7 @@ pub const Effects = struct {
|
|
|
2122
2174
|
try log.status(foe_ident, foe_stored.status, .None);
|
|
2123
2175
|
}
|
|
2124
2176
|
|
|
2125
|
-
fn paralyzeChance(battle: anytype, player: Player, move: Move.Data,
|
|
2177
|
+
fn paralyzeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
2126
2178
|
var foe = battle.foe(player);
|
|
2127
2179
|
var foe_stored = foe.stored();
|
|
2128
2180
|
|
|
@@ -2134,25 +2186,30 @@ pub const Effects = struct {
|
|
|
2134
2186
|
|
|
2135
2187
|
// Body Slam can't paralyze a Normal type Pokémon
|
|
2136
2188
|
if (foe.active.types.includes(move.type)) return;
|
|
2137
|
-
if (!Rolls.secondaryChance(battle, move.effect == .ParalyzeChance1))
|
|
2189
|
+
if (!try Rolls.secondaryChance(battle, player, move.effect == .ParalyzeChance1, options)) {
|
|
2190
|
+
return;
|
|
2191
|
+
}
|
|
2138
2192
|
|
|
2139
2193
|
foe_stored.status = Status.init(.PAR);
|
|
2140
2194
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
2141
2195
|
|
|
2142
|
-
try log.status(battle.active(player.foe()), foe_stored.status, .None);
|
|
2196
|
+
try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
|
|
2143
2197
|
}
|
|
2144
2198
|
|
|
2145
|
-
fn payDay(battle: anytype, player: Player,
|
|
2146
|
-
if (!showdown or !battle.foe(player).active.volatiles.Substitute)
|
|
2199
|
+
fn payDay(battle: anytype, player: Player, options: anytype) !void {
|
|
2200
|
+
if (!showdown or !battle.foe(player).active.volatiles.Substitute) {
|
|
2201
|
+
try options.log.fieldactivate();
|
|
2202
|
+
}
|
|
2147
2203
|
}
|
|
2148
2204
|
|
|
2149
|
-
fn poison(battle: anytype, player: Player, move: Move.Data,
|
|
2205
|
+
fn poison(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
2206
|
+
var log = options.log;
|
|
2150
2207
|
var foe = battle.foe(player);
|
|
2151
2208
|
var foe_stored = foe.stored();
|
|
2152
2209
|
const foe_ident = battle.active(player.foe());
|
|
2153
2210
|
const toxic = battle.side(player).last_selected_move == .Toxic;
|
|
2154
2211
|
|
|
2155
|
-
if (showdown and move.effect == .Poison and !try checkHit(battle, player, move,
|
|
2212
|
+
if (showdown and move.effect == .Poison and !try checkHit(battle, player, move, options)) {
|
|
2156
2213
|
return;
|
|
2157
2214
|
} else if (foe.active.volatiles.Substitute) {
|
|
2158
2215
|
if (move.effect != .Poison) return;
|
|
@@ -2176,8 +2233,13 @@ pub const Effects = struct {
|
|
|
2176
2233
|
}
|
|
2177
2234
|
|
|
2178
2235
|
if (move.effect == .Poison) {
|
|
2179
|
-
if (!showdown and !try checkHit(battle, player, move,
|
|
2180
|
-
|
|
2236
|
+
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2237
|
+
try options.chance.commit(player, .hit);
|
|
2238
|
+
} else {
|
|
2239
|
+
if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
|
|
2240
|
+
return;
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2181
2243
|
|
|
2182
2244
|
foe_stored.status = Status.init(.PSN);
|
|
2183
2245
|
if (toxic) {
|
|
@@ -2195,28 +2257,36 @@ pub const Effects = struct {
|
|
|
2195
2257
|
volatiles.Rage = true;
|
|
2196
2258
|
}
|
|
2197
2259
|
|
|
2198
|
-
fn recoil(battle: anytype, player: Player, residual: *bool,
|
|
2260
|
+
fn recoil(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
|
|
2199
2261
|
var side = battle.side(player);
|
|
2200
2262
|
var stored = side.stored();
|
|
2201
2263
|
|
|
2202
|
-
|
|
2264
|
+
assert(showdown or battle.last_damage > 0);
|
|
2265
|
+
if (showdown and battle.last_damage == 0) return;
|
|
2266
|
+
|
|
2267
|
+
const damage: i16 = @intCast(@max(battle.last_damage /
|
|
2203
2268
|
@as(u8, if (side.last_selected_move == .Struggle) 2 else 4), 1));
|
|
2204
|
-
stored.hp = @intCast(
|
|
2269
|
+
stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
|
|
2205
2270
|
|
|
2206
|
-
try log.damageOf(
|
|
2271
|
+
try options.log.damageOf(
|
|
2272
|
+
battle.active(player),
|
|
2273
|
+
stored,
|
|
2274
|
+
.RecoilOf,
|
|
2275
|
+
battle.active(player.foe()),
|
|
2276
|
+
);
|
|
2207
2277
|
if (showdown and stored.hp == 0) residual.* = false;
|
|
2208
2278
|
}
|
|
2209
2279
|
|
|
2210
|
-
fn reflect(battle: anytype, player: Player,
|
|
2280
|
+
fn reflect(battle: anytype, player: Player, options: anytype) !void {
|
|
2211
2281
|
var side = battle.side(player);
|
|
2212
2282
|
|
|
2213
|
-
if (side.active.volatiles.Reflect) return log.fail(battle.active(player), .None);
|
|
2283
|
+
if (side.active.volatiles.Reflect) return options.log.fail(battle.active(player), .None);
|
|
2214
2284
|
side.active.volatiles.Reflect = true;
|
|
2215
2285
|
|
|
2216
|
-
try log.start(battle.active(player), .Reflect);
|
|
2286
|
+
try options.log.start(battle.active(player), .Reflect);
|
|
2217
2287
|
}
|
|
2218
2288
|
|
|
2219
|
-
fn sleep(battle: anytype, player: Player, move: Move.Data,
|
|
2289
|
+
fn sleep(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
2220
2290
|
var foe = battle.foe(player);
|
|
2221
2291
|
var foe_stored = foe.stored();
|
|
2222
2292
|
const foe_ident = battle.active(player.foe());
|
|
@@ -2227,13 +2297,14 @@ pub const Effects = struct {
|
|
|
2227
2297
|
// the Sleep Clause Mod might activate, causing us to not actually bypass
|
|
2228
2298
|
} else {
|
|
2229
2299
|
if (Status.any(foe_stored.status)) {
|
|
2230
|
-
return log.fail(
|
|
2300
|
+
return options.log.fail(
|
|
2231
2301
|
foe_ident,
|
|
2232
2302
|
if (Status.is(foe_stored.status, .SLP)) .Sleep else .None,
|
|
2233
2303
|
);
|
|
2234
2304
|
}
|
|
2235
2305
|
// If checkHit in doMove didn't return true Pokémon Showdown wouldn't be in here
|
|
2236
|
-
if (!showdown and !try checkHit(battle, player, move,
|
|
2306
|
+
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2307
|
+
try options.chance.commit(player, .hit);
|
|
2237
2308
|
}
|
|
2238
2309
|
|
|
2239
2310
|
// Sleep Clause Mod
|
|
@@ -2244,31 +2315,32 @@ pub const Effects = struct {
|
|
|
2244
2315
|
}
|
|
2245
2316
|
foe.active.volatiles.Recharging = false;
|
|
2246
2317
|
|
|
2247
|
-
foe_stored.status = Status.slp(Rolls.sleepDuration(battle));
|
|
2248
|
-
|
|
2318
|
+
foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
|
|
2319
|
+
const last = battle.side(player).last_selected_move;
|
|
2320
|
+
try options.log.statusFrom(foe_ident, foe_stored.status, last);
|
|
2249
2321
|
}
|
|
2250
2322
|
|
|
2251
|
-
fn splash(battle: anytype, player: Player,
|
|
2252
|
-
try log.activate(battle.active(player), .Splash);
|
|
2323
|
+
fn splash(battle: anytype, player: Player, options: anytype) !void {
|
|
2324
|
+
try options.log.activate(battle.active(player), .Splash);
|
|
2253
2325
|
}
|
|
2254
2326
|
|
|
2255
|
-
fn substitute(battle: anytype, player: Player, residual: *bool,
|
|
2327
|
+
fn substitute(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
|
|
2256
2328
|
var side = battle.side(player);
|
|
2257
2329
|
if (side.active.volatiles.Substitute) {
|
|
2258
|
-
try log.fail(battle.active(player), .Substitute);
|
|
2330
|
+
try options.log.fail(battle.active(player), .Substitute);
|
|
2259
2331
|
return;
|
|
2260
2332
|
}
|
|
2261
2333
|
|
|
2262
2334
|
assert(side.stored().stats.hp <= 1023);
|
|
2263
2335
|
// Will be 0 if HP is <= 3 meaning that the user gets a 1 HP Substitute for "free"
|
|
2264
|
-
const hp = @intCast(
|
|
2336
|
+
const hp: u8 = @intCast(side.stored().stats.hp / 4);
|
|
2265
2337
|
// Pokénon Showdown incorrectly checks for 1/4 HP based on `target.maxhp / 4` which returns
|
|
2266
2338
|
// a floating point value and thus only correctly implements the Substitute 1/4 glitch when
|
|
2267
2339
|
// the target's HP is exactly divisible by 4 (here we're using an inlined divCeil routine to
|
|
2268
2340
|
// avoid having to convert to floating point)
|
|
2269
2341
|
const required_hp = if (showdown) @divFloor(side.stored().stats.hp - 1, 4) + 1 else hp;
|
|
2270
2342
|
if (side.stored().hp < required_hp) {
|
|
2271
|
-
try log.fail(battle.active(player), .Weak);
|
|
2343
|
+
try options.log.fail(battle.active(player), .Weak);
|
|
2272
2344
|
return;
|
|
2273
2345
|
}
|
|
2274
2346
|
|
|
@@ -2276,31 +2348,37 @@ pub const Effects = struct {
|
|
|
2276
2348
|
side.stored().hp -= hp;
|
|
2277
2349
|
side.active.volatiles.substitute = hp + 1;
|
|
2278
2350
|
side.active.volatiles.Substitute = true;
|
|
2279
|
-
try log.start(battle.active(player), .Substitute);
|
|
2351
|
+
try options.log.start(battle.active(player), .Substitute);
|
|
2280
2352
|
if (hp > 0) {
|
|
2281
|
-
try log.damage(battle.active(player), side.stored(), .None);
|
|
2353
|
+
try options.log.damage(battle.active(player), side.stored(), .None);
|
|
2282
2354
|
if (showdown and side.stored().hp == 0) residual.* = false;
|
|
2283
2355
|
}
|
|
2284
2356
|
}
|
|
2285
2357
|
|
|
2286
|
-
fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data,
|
|
2287
|
-
if (!showdown or battle.side(player).last_selected_move == .Teleport) return;
|
|
2288
|
-
|
|
2358
|
+
fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
2289
2359
|
// Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
|
|
2290
|
-
|
|
2291
|
-
|
|
2360
|
+
if (showdown) {
|
|
2361
|
+
if (battle.side(player).last_selected_move == .Teleport) return;
|
|
2362
|
+
if (try checkHit(battle, player, move, options)) {
|
|
2363
|
+
try options.chance.commit(player, .hit);
|
|
2364
|
+
}
|
|
2365
|
+
battle.last_damage = 0;
|
|
2366
|
+
} else {
|
|
2367
|
+
try options.log.fail(battle.active(player), .None);
|
|
2368
|
+
try options.log.laststill();
|
|
2369
|
+
}
|
|
2292
2370
|
}
|
|
2293
2371
|
|
|
2294
|
-
fn thrashing(battle: anytype, player: Player) void {
|
|
2372
|
+
fn thrashing(battle: anytype, player: Player, options: anytype) void {
|
|
2295
2373
|
var volatiles = &battle.side(player).active.volatiles;
|
|
2296
2374
|
assert(!volatiles.Thrashing);
|
|
2297
2375
|
assert(!volatiles.Bide);
|
|
2298
2376
|
|
|
2299
2377
|
volatiles.Thrashing = true;
|
|
2300
|
-
volatiles.attacks = Rolls.
|
|
2378
|
+
volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
2301
2379
|
}
|
|
2302
2380
|
|
|
2303
|
-
fn transform(battle: anytype, player: Player,
|
|
2381
|
+
fn transform(battle: anytype, player: Player, options: anytype) !void {
|
|
2304
2382
|
var side = battle.side(player);
|
|
2305
2383
|
const foe = battle.foe(player);
|
|
2306
2384
|
const foe_ident = battle.active(player.foe());
|
|
@@ -2326,10 +2404,10 @@ pub const Effects = struct {
|
|
|
2326
2404
|
side.active.moves[i].pp = if (m.id != .None) 5 else 0;
|
|
2327
2405
|
}
|
|
2328
2406
|
|
|
2329
|
-
try log.transform(battle.active(player), foe_ident);
|
|
2407
|
+
try options.log.transform(battle.active(player), foe_ident);
|
|
2330
2408
|
}
|
|
2331
2409
|
|
|
2332
|
-
fn binding(battle: anytype, player: Player, rewrap: bool) void {
|
|
2410
|
+
fn binding(battle: anytype, player: Player, rewrap: bool, options: anytype) !void {
|
|
2333
2411
|
var side = battle.side(player);
|
|
2334
2412
|
var foe = battle.foe(player);
|
|
2335
2413
|
|
|
@@ -2341,10 +2419,12 @@ pub const Effects = struct {
|
|
|
2341
2419
|
foe.active.volatiles.Recharging = false;
|
|
2342
2420
|
} else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
|
|
2343
2421
|
|
|
2344
|
-
side.active.volatiles.attacks =
|
|
2422
|
+
side.active.volatiles.attacks =
|
|
2423
|
+
try Rolls.distribution(battle, .Binding, player, options) - 1;
|
|
2345
2424
|
}
|
|
2346
2425
|
|
|
2347
|
-
fn boost(battle: anytype, player: Player, move: Move.Data,
|
|
2426
|
+
fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
2427
|
+
var log = options.log;
|
|
2348
2428
|
var side = battle.side(player);
|
|
2349
2429
|
const ident = battle.active(player);
|
|
2350
2430
|
|
|
@@ -2356,7 +2436,7 @@ pub const Effects = struct {
|
|
|
2356
2436
|
assert(boosts.atk >= -6 and boosts.atk <= 6);
|
|
2357
2437
|
if (boosts.atk == 6) return try log.fail(ident, .None);
|
|
2358
2438
|
const n: u2 = if (move.effect == .AttackUp2) 2 else 1;
|
|
2359
|
-
boosts.atk = @
|
|
2439
|
+
boosts.atk = @as(i4, @intCast(@min(6, @as(i8, boosts.atk) + n)));
|
|
2360
2440
|
const reason = if (move.effect == .Rage) Boost.Rage else Boost.Attack;
|
|
2361
2441
|
if (stats.atk == MAX_STAT_VALUE) {
|
|
2362
2442
|
boosts.atk -= 1;
|
|
@@ -2367,7 +2447,7 @@ pub const Effects = struct {
|
|
|
2367
2447
|
}
|
|
2368
2448
|
return try log.fail(ident, .None);
|
|
2369
2449
|
}
|
|
2370
|
-
var mod = BOOSTS[@
|
|
2450
|
+
var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
|
|
2371
2451
|
const stat = unmodifiedStats(battle, side).atk;
|
|
2372
2452
|
stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2373
2453
|
try log.boost(ident, reason, n);
|
|
@@ -2378,7 +2458,7 @@ pub const Effects = struct {
|
|
|
2378
2458
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
2379
2459
|
if (boosts.def == 6) return try log.fail(ident, .None);
|
|
2380
2460
|
const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
|
|
2381
|
-
boosts.def = @intCast(
|
|
2461
|
+
boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
|
|
2382
2462
|
if (stats.def == MAX_STAT_VALUE) {
|
|
2383
2463
|
boosts.def -= 1;
|
|
2384
2464
|
if (showdown) {
|
|
@@ -2387,7 +2467,7 @@ pub const Effects = struct {
|
|
|
2387
2467
|
}
|
|
2388
2468
|
return try log.fail(ident, .None);
|
|
2389
2469
|
}
|
|
2390
|
-
var mod = BOOSTS[@
|
|
2470
|
+
var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
|
|
2391
2471
|
const stat = unmodifiedStats(battle, side).def;
|
|
2392
2472
|
stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2393
2473
|
try log.boost(ident, .Defense, n);
|
|
@@ -2395,7 +2475,7 @@ pub const Effects = struct {
|
|
|
2395
2475
|
.SpeedUp2 => {
|
|
2396
2476
|
assert(boosts.spe >= -6 and boosts.spe <= 6);
|
|
2397
2477
|
if (boosts.spe == 6) return try log.fail(ident, .None);
|
|
2398
|
-
boosts.spe = @intCast(
|
|
2478
|
+
boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
|
|
2399
2479
|
if (stats.spe == MAX_STAT_VALUE) {
|
|
2400
2480
|
boosts.spe -= 1;
|
|
2401
2481
|
if (showdown) {
|
|
@@ -2404,7 +2484,7 @@ pub const Effects = struct {
|
|
|
2404
2484
|
}
|
|
2405
2485
|
return try log.fail(ident, .None);
|
|
2406
2486
|
}
|
|
2407
|
-
var mod = BOOSTS[@
|
|
2487
|
+
var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
|
|
2408
2488
|
const stat = unmodifiedStats(battle, side).spe;
|
|
2409
2489
|
stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2410
2490
|
try log.boost(ident, .Speed, 2);
|
|
@@ -2413,7 +2493,7 @@ pub const Effects = struct {
|
|
|
2413
2493
|
assert(boosts.spc >= -6 and boosts.spc <= 6);
|
|
2414
2494
|
if (boosts.spc == 6) return try log.fail(ident, .None);
|
|
2415
2495
|
const n: u2 = if (move.effect == .SpecialUp2) 2 else 1;
|
|
2416
|
-
boosts.spc = @intCast(
|
|
2496
|
+
boosts.spc = @intCast(@min(6, @as(i8, boosts.spc) + n));
|
|
2417
2497
|
if (stats.spc == MAX_STAT_VALUE) {
|
|
2418
2498
|
boosts.spc -= 1;
|
|
2419
2499
|
if (showdown) {
|
|
@@ -2424,7 +2504,7 @@ pub const Effects = struct {
|
|
|
2424
2504
|
}
|
|
2425
2505
|
return try log.fail(ident, .None);
|
|
2426
2506
|
}
|
|
2427
|
-
var mod = BOOSTS[@
|
|
2507
|
+
var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
|
|
2428
2508
|
const stat = unmodifiedStats(battle, side).spc;
|
|
2429
2509
|
stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2430
2510
|
try log.boost(ident, .SpecialAttack, n);
|
|
@@ -2433,7 +2513,7 @@ pub const Effects = struct {
|
|
|
2433
2513
|
.EvasionUp1 => {
|
|
2434
2514
|
assert(boosts.evasion >= -6 and boosts.evasion <= 6);
|
|
2435
2515
|
if (boosts.evasion == 6) return try log.fail(ident, .None);
|
|
2436
|
-
boosts.evasion = @intCast(
|
|
2516
|
+
boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
|
|
2437
2517
|
try log.boost(ident, .Evasion, 1);
|
|
2438
2518
|
},
|
|
2439
2519
|
else => unreachable,
|
|
@@ -2443,28 +2523,32 @@ pub const Effects = struct {
|
|
|
2443
2523
|
statusModify(battle.foe(player).stored().status, &battle.foe(player).active.stats);
|
|
2444
2524
|
}
|
|
2445
2525
|
|
|
2446
|
-
fn unboost(battle: anytype, player: Player, move: Move.Data,
|
|
2526
|
+
fn unboost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
2527
|
+
var log = options.log;
|
|
2447
2528
|
var foe = battle.foe(player);
|
|
2448
2529
|
const foe_ident = battle.active(player.foe());
|
|
2530
|
+
const secondary = move.effect.isStatDownChance();
|
|
2449
2531
|
|
|
2450
|
-
if (foe.active.volatiles.Substitute)
|
|
2451
|
-
return if (!move.effect.isStatDownChance()) log.fail(foe_ident, .None);
|
|
2452
|
-
}
|
|
2532
|
+
if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(foe_ident, .None);
|
|
2453
2533
|
|
|
2454
|
-
if (
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2534
|
+
if (secondary) {
|
|
2535
|
+
const proc = try Rolls.unboost(battle, player, options);
|
|
2536
|
+
if (!proc or foe.active.volatiles.Invulnerable) return;
|
|
2537
|
+
} else {
|
|
2538
|
+
// checkHit already checks for Invulnerable
|
|
2539
|
+
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2540
|
+
try options.chance.commit(player, .hit);
|
|
2458
2541
|
}
|
|
2459
2542
|
|
|
2460
2543
|
var stats = &foe.active.stats;
|
|
2461
2544
|
var boosts = &foe.active.boosts;
|
|
2545
|
+
const fail = showdown or !secondary;
|
|
2462
2546
|
|
|
2463
2547
|
switch (move.effect) {
|
|
2464
2548
|
.AttackDown1, .AttackDownChance => {
|
|
2465
2549
|
assert(boosts.atk >= -6 and boosts.atk <= 6);
|
|
2466
|
-
if (boosts.atk == -6) return try log.fail(foe_ident, .None);
|
|
2467
|
-
boosts.atk = @intCast(
|
|
2550
|
+
if (boosts.atk == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2551
|
+
boosts.atk = @intCast(@max(-6, @as(i8, boosts.atk) - 1));
|
|
2468
2552
|
if (stats.atk == 1) {
|
|
2469
2553
|
boosts.atk += 1;
|
|
2470
2554
|
if (showdown) {
|
|
@@ -2473,16 +2557,16 @@ pub const Effects = struct {
|
|
|
2473
2557
|
}
|
|
2474
2558
|
return try log.fail(foe_ident, .None);
|
|
2475
2559
|
}
|
|
2476
|
-
var mod = BOOSTS[@
|
|
2560
|
+
var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
|
|
2477
2561
|
const stat = unmodifiedStats(battle, foe).atk;
|
|
2478
2562
|
stats.atk = @max(1, stat * mod[0] / mod[1]);
|
|
2479
2563
|
try log.boost(foe_ident, .Attack, -1);
|
|
2480
2564
|
},
|
|
2481
2565
|
.DefenseDown1, .DefenseDown2, .DefenseDownChance => {
|
|
2482
2566
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
2483
|
-
if (boosts.def == -6) return try log.fail(foe_ident, .None);
|
|
2567
|
+
if (boosts.def == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2484
2568
|
const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
|
|
2485
|
-
boosts.def = @intCast(
|
|
2569
|
+
boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
|
|
2486
2570
|
if (stats.def == 1) {
|
|
2487
2571
|
boosts.def += 1;
|
|
2488
2572
|
if (showdown) {
|
|
@@ -2491,15 +2575,15 @@ pub const Effects = struct {
|
|
|
2491
2575
|
}
|
|
2492
2576
|
return try log.fail(foe_ident, .None);
|
|
2493
2577
|
}
|
|
2494
|
-
var mod = BOOSTS[@
|
|
2578
|
+
var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
|
|
2495
2579
|
const stat = unmodifiedStats(battle, foe).def;
|
|
2496
2580
|
stats.def = @max(1, stat * mod[0] / mod[1]);
|
|
2497
2581
|
try log.boost(foe_ident, .Defense, -@as(i8, n));
|
|
2498
2582
|
},
|
|
2499
2583
|
.SpeedDown1, .SpeedDownChance => {
|
|
2500
2584
|
assert(boosts.spe >= -6 and boosts.spe <= 6);
|
|
2501
|
-
if (boosts.spe == -6) return try log.fail(foe_ident, .None);
|
|
2502
|
-
boosts.spe = @intCast(
|
|
2585
|
+
if (boosts.spe == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2586
|
+
boosts.spe = @intCast(@max(-6, @as(i8, boosts.spe) - 1));
|
|
2503
2587
|
if (stats.spe == 1) {
|
|
2504
2588
|
boosts.spe += 1;
|
|
2505
2589
|
if (showdown) {
|
|
@@ -2508,7 +2592,7 @@ pub const Effects = struct {
|
|
|
2508
2592
|
}
|
|
2509
2593
|
return try log.fail(foe_ident, .None);
|
|
2510
2594
|
}
|
|
2511
|
-
var mod = BOOSTS[@
|
|
2595
|
+
var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
|
|
2512
2596
|
const stat = unmodifiedStats(battle, foe).spe;
|
|
2513
2597
|
stats.spe = @max(1, stat * mod[0] / mod[1]);
|
|
2514
2598
|
try log.boost(foe_ident, .Speed, -1);
|
|
@@ -2516,8 +2600,8 @@ pub const Effects = struct {
|
|
|
2516
2600
|
},
|
|
2517
2601
|
.SpecialDownChance => {
|
|
2518
2602
|
assert(boosts.spc >= -6 and boosts.spc <= 6);
|
|
2519
|
-
if (boosts.spc == -6) return try log.fail(foe_ident, .None);
|
|
2520
|
-
boosts.spc = @intCast(
|
|
2603
|
+
if (boosts.spc == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2604
|
+
boosts.spc = @intCast(@max(-6, @as(i8, boosts.spc) - 1));
|
|
2521
2605
|
if (stats.spc == 1) {
|
|
2522
2606
|
boosts.spc += 1;
|
|
2523
2607
|
if (showdown) {
|
|
@@ -2528,7 +2612,7 @@ pub const Effects = struct {
|
|
|
2528
2612
|
}
|
|
2529
2613
|
return try log.fail(foe_ident, .None);
|
|
2530
2614
|
}
|
|
2531
|
-
var mod = BOOSTS[@
|
|
2615
|
+
var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
|
|
2532
2616
|
const stat = unmodifiedStats(battle, foe).spc;
|
|
2533
2617
|
stats.spc = @max(1, stat * mod[0] / mod[1]);
|
|
2534
2618
|
try log.boost(foe_ident, .SpecialAttack, -1);
|
|
@@ -2536,8 +2620,8 @@ pub const Effects = struct {
|
|
|
2536
2620
|
},
|
|
2537
2621
|
.AccuracyDown1 => {
|
|
2538
2622
|
assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
|
|
2539
|
-
if (boosts.accuracy == -6) return try log.fail(foe_ident, .None);
|
|
2540
|
-
boosts.accuracy = @intCast(
|
|
2623
|
+
if (boosts.accuracy == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2624
|
+
boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
|
|
2541
2625
|
try log.boost(foe_ident, .Accuracy, -1);
|
|
2542
2626
|
},
|
|
2543
2627
|
else => unreachable,
|
|
@@ -2562,19 +2646,22 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
|
|
|
2562
2646
|
}
|
|
2563
2647
|
}
|
|
2564
2648
|
|
|
2565
|
-
inline fn isForced(active:
|
|
2649
|
+
inline fn isForced(active: anytype) bool {
|
|
2566
2650
|
return active.volatiles.Recharging or active.volatiles.Rage or
|
|
2567
2651
|
active.volatiles.Thrashing or active.volatiles.Charging;
|
|
2568
2652
|
}
|
|
2569
2653
|
|
|
2570
|
-
fn clearVolatiles(battle: anytype, who: Player,
|
|
2654
|
+
fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !void {
|
|
2655
|
+
var log = options.log;
|
|
2571
2656
|
var side = battle.side(who);
|
|
2572
2657
|
var volatiles = &side.active.volatiles;
|
|
2573
2658
|
const ident = battle.active(who);
|
|
2574
2659
|
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2660
|
+
options.chance.clearDurations(who, clear);
|
|
2661
|
+
|
|
2662
|
+
if (volatiles.disable_move != 0) {
|
|
2663
|
+
volatiles.disable_move = 0;
|
|
2664
|
+
volatiles.disable_duration = 0;
|
|
2578
2665
|
try log.end(ident, .DisableSilent);
|
|
2579
2666
|
}
|
|
2580
2667
|
if (volatiles.Confusion) {
|
|
@@ -2584,15 +2671,15 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
|
|
|
2584
2671
|
}
|
|
2585
2672
|
if (volatiles.Mist) {
|
|
2586
2673
|
volatiles.Mist = false;
|
|
2587
|
-
try log.end(ident, .
|
|
2674
|
+
try log.end(ident, .MistSilent);
|
|
2588
2675
|
}
|
|
2589
2676
|
if (volatiles.FocusEnergy) {
|
|
2590
2677
|
volatiles.FocusEnergy = false;
|
|
2591
|
-
try log.end(ident, .
|
|
2678
|
+
try log.end(ident, .FocusEnergySilent);
|
|
2592
2679
|
}
|
|
2593
2680
|
if (volatiles.LeechSeed) {
|
|
2594
2681
|
volatiles.LeechSeed = false;
|
|
2595
|
-
try log.end(ident, .
|
|
2682
|
+
try log.end(ident, .LeechSeedSilent);
|
|
2596
2683
|
}
|
|
2597
2684
|
if (!showdown and volatiles.Toxic) {
|
|
2598
2685
|
volatiles.Toxic = false;
|
|
@@ -2600,152 +2687,313 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
|
|
|
2600
2687
|
}
|
|
2601
2688
|
if (volatiles.LightScreen) {
|
|
2602
2689
|
volatiles.LightScreen = false;
|
|
2603
|
-
try log.end(ident, .
|
|
2690
|
+
try log.end(ident, .LightScreenSilent);
|
|
2604
2691
|
}
|
|
2605
2692
|
if (volatiles.Reflect) {
|
|
2606
2693
|
volatiles.Reflect = false;
|
|
2607
|
-
try log.end(ident, .
|
|
2694
|
+
try log.end(ident, .ReflectSilent);
|
|
2608
2695
|
}
|
|
2609
2696
|
if (showdown and volatiles.Toxic) {
|
|
2610
2697
|
volatiles.Toxic = false;
|
|
2611
2698
|
// Pokémon Showdown erroneously clears the toxic counter
|
|
2612
2699
|
volatiles.toxic = 0;
|
|
2613
|
-
try log.end(ident, .
|
|
2700
|
+
try log.end(ident, .ToxicSilent);
|
|
2614
2701
|
}
|
|
2615
2702
|
}
|
|
2616
2703
|
|
|
2617
2704
|
pub const Rolls = struct {
|
|
2618
|
-
inline fn speedTie(battle: anytype) bool {
|
|
2619
|
-
if (
|
|
2620
|
-
|
|
2621
|
-
|
|
2705
|
+
inline fn speedTie(battle: anytype, options: anytype) !bool {
|
|
2706
|
+
const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
|
|
2707
|
+
player == .P1
|
|
2708
|
+
else if (showdown)
|
|
2709
|
+
battle.rng.range(u8, 0, 2) == 0
|
|
2710
|
+
else
|
|
2711
|
+
battle.rng.next() < Gen12.percent(50) + 1;
|
|
2622
2712
|
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
return std.math.rotl(u8, battle.rng.next(), 3) < chance;
|
|
2713
|
+
try options.chance.speedTie(p1);
|
|
2714
|
+
return p1;
|
|
2626
2715
|
}
|
|
2627
2716
|
|
|
2628
|
-
inline fn
|
|
2629
|
-
if (
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2717
|
+
inline fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
|
|
2718
|
+
const crit = if (options.calc.overridden(player, .critical_hit)) |val|
|
|
2719
|
+
val == .true
|
|
2720
|
+
else if (showdown)
|
|
2721
|
+
battle.rng.chance(u8, rate, 256)
|
|
2722
|
+
else
|
|
2723
|
+
std.math.rotl(u8, battle.rng.next(), 3) < rate;
|
|
2635
2724
|
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
return battle.rng.next() < accuracy;
|
|
2725
|
+
try options.chance.criticalHit(player, crit, rate);
|
|
2726
|
+
return crit;
|
|
2639
2727
|
}
|
|
2640
2728
|
|
|
2641
|
-
inline fn
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2729
|
+
inline fn damage(battle: anytype, player: anytype, options: anytype) !u8 {
|
|
2730
|
+
const roll = if (options.calc.overridden(player, .damage)) |val|
|
|
2731
|
+
val
|
|
2732
|
+
else roll: {
|
|
2733
|
+
if (showdown) break :roll battle.rng.range(u8, 217, 256);
|
|
2734
|
+
while (true) {
|
|
2735
|
+
const r = std.math.rotr(u8, battle.rng.next(), 1);
|
|
2736
|
+
if (r >= 217) break :roll r;
|
|
2737
|
+
}
|
|
2738
|
+
};
|
|
2645
2739
|
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
return
|
|
2740
|
+
assert(roll >= 217 and roll <= 255);
|
|
2741
|
+
try options.chance.damage(player, roll);
|
|
2742
|
+
return roll;
|
|
2649
2743
|
}
|
|
2650
2744
|
|
|
2651
|
-
inline fn
|
|
2652
|
-
if (
|
|
2653
|
-
|
|
2745
|
+
inline fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) bool {
|
|
2746
|
+
const ok = if (options.calc.overridden(player, .hit)) |val|
|
|
2747
|
+
val == .true
|
|
2748
|
+
else if (showdown)
|
|
2749
|
+
battle.rng.chance(u8, accuracy, 256)
|
|
2750
|
+
else
|
|
2751
|
+
battle.rng.next() < accuracy;
|
|
2752
|
+
|
|
2753
|
+
options.chance.hit(ok, accuracy);
|
|
2754
|
+
return ok;
|
|
2654
2755
|
}
|
|
2655
2756
|
|
|
2656
|
-
inline fn
|
|
2657
|
-
if (
|
|
2658
|
-
|
|
2757
|
+
inline fn confused(battle: anytype, player: Player, options: anytype) !bool {
|
|
2758
|
+
const cfz = if (options.calc.overridden(player, .confused)) |val|
|
|
2759
|
+
val == .true
|
|
2760
|
+
else if (showdown)
|
|
2761
|
+
!battle.rng.chance(u8, 128, 256)
|
|
2762
|
+
else
|
|
2763
|
+
battle.rng.next() >= Gen12.percent(50) + 1;
|
|
2764
|
+
|
|
2765
|
+
try options.chance.confused(player, cfz);
|
|
2766
|
+
return cfz;
|
|
2659
2767
|
}
|
|
2660
2768
|
|
|
2661
|
-
inline fn
|
|
2662
|
-
if (
|
|
2663
|
-
|
|
2769
|
+
inline fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
|
|
2770
|
+
const par = if (options.calc.overridden(player, .paralyzed)) |val|
|
|
2771
|
+
val == .true
|
|
2772
|
+
else if (showdown)
|
|
2773
|
+
battle.rng.chance(u8, 63, 256)
|
|
2774
|
+
else
|
|
2775
|
+
battle.rng.next() < Gen12.percent(25);
|
|
2776
|
+
|
|
2777
|
+
try options.chance.paralyzed(player, par);
|
|
2778
|
+
return par;
|
|
2664
2779
|
}
|
|
2665
2780
|
|
|
2666
|
-
inline fn
|
|
2667
|
-
if (
|
|
2668
|
-
|
|
2781
|
+
inline fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
|
|
2782
|
+
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2783
|
+
val == .true
|
|
2784
|
+
else if (showdown)
|
|
2785
|
+
battle.rng.chance(u8, 25, 256)
|
|
2786
|
+
else
|
|
2787
|
+
battle.rng.next() < Gen12.percent(10);
|
|
2788
|
+
|
|
2789
|
+
try options.chance.secondaryChance(player, proc, 25);
|
|
2790
|
+
return proc;
|
|
2669
2791
|
}
|
|
2670
2792
|
|
|
2671
|
-
inline fn
|
|
2672
|
-
if (
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2793
|
+
inline fn secondaryChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
|
|
2794
|
+
const rate: u8 = if (low) 26 else 77;
|
|
2795
|
+
|
|
2796
|
+
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2797
|
+
val == .true
|
|
2798
|
+
else if (showdown)
|
|
2799
|
+
battle.rng.chance(u8, rate, 256)
|
|
2800
|
+
else
|
|
2801
|
+
battle.rng.next() < 1 + (if (low) Gen12.percent(10) else Gen12.percent(30));
|
|
2802
|
+
|
|
2803
|
+
try options.chance.secondaryChance(player, proc, rate);
|
|
2804
|
+
return proc;
|
|
2677
2805
|
}
|
|
2678
2806
|
|
|
2679
|
-
inline fn
|
|
2680
|
-
if (
|
|
2681
|
-
|
|
2807
|
+
inline fn poisonChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
|
|
2808
|
+
const rate: u8 = if (low) 52 else 103;
|
|
2809
|
+
|
|
2810
|
+
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2811
|
+
val == .true
|
|
2812
|
+
else if (showdown)
|
|
2813
|
+
battle.rng.chance(u8, rate, 256)
|
|
2814
|
+
else
|
|
2815
|
+
battle.rng.next() < 1 + (if (low) Gen12.percent(20) else Gen12.percent(40));
|
|
2816
|
+
|
|
2817
|
+
try options.chance.secondaryChance(player, proc, rate);
|
|
2818
|
+
return proc;
|
|
2682
2819
|
}
|
|
2683
2820
|
|
|
2684
|
-
fn
|
|
2685
|
-
if (
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2821
|
+
inline fn unboost(battle: anytype, player: Player, options: anytype) !bool {
|
|
2822
|
+
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2823
|
+
val == .true
|
|
2824
|
+
else if (showdown)
|
|
2825
|
+
battle.rng.chance(u8, 85, 256)
|
|
2826
|
+
else
|
|
2827
|
+
battle.rng.next() < Gen12.percent(33) + 1;
|
|
2828
|
+
|
|
2829
|
+
try options.chance.secondaryChance(player, proc, 85);
|
|
2830
|
+
return proc;
|
|
2831
|
+
}
|
|
2832
|
+
|
|
2833
|
+
inline fn metronome(battle: anytype, player: Player, options: anytype) !Move {
|
|
2834
|
+
const move: Move = if (options.calc.overridden(player, .metronome)) |val|
|
|
2835
|
+
val
|
|
2836
|
+
else if (showdown)
|
|
2837
|
+
Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
|
|
2838
|
+
else move: {
|
|
2839
|
+
while (true) {
|
|
2840
|
+
const r = battle.rng.next();
|
|
2841
|
+
if (r == 0 or r == @intFromEnum(Move.Metronome)) continue;
|
|
2842
|
+
if (r >= @intFromEnum(Move.Struggle)) continue;
|
|
2843
|
+
break :move @enumFromInt(r);
|
|
2704
2844
|
}
|
|
2705
|
-
}
|
|
2845
|
+
};
|
|
2706
2846
|
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
|
|
2710
|
-
}
|
|
2847
|
+
try options.chance.metronome(player, move);
|
|
2848
|
+
return move;
|
|
2711
2849
|
}
|
|
2712
2850
|
|
|
2713
|
-
inline fn
|
|
2714
|
-
|
|
2715
|
-
|
|
2851
|
+
inline fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
|
|
2852
|
+
const power = if (options.calc.overridden(player, .psywave)) |val|
|
|
2853
|
+
val - 1
|
|
2854
|
+
else if (showdown)
|
|
2855
|
+
battle.rng.range(u8, 0, max)
|
|
2856
|
+
else power: {
|
|
2857
|
+
while (true) {
|
|
2858
|
+
const r = battle.rng.next();
|
|
2859
|
+
if (r < max) break :power r;
|
|
2860
|
+
}
|
|
2861
|
+
};
|
|
2862
|
+
|
|
2863
|
+
assert(power < max);
|
|
2864
|
+
try options.chance.psywave(player, power, max);
|
|
2865
|
+
return power;
|
|
2716
2866
|
}
|
|
2717
2867
|
|
|
2718
|
-
inline fn
|
|
2719
|
-
if (
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2868
|
+
inline fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2869
|
+
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2870
|
+
@intCast(val)
|
|
2871
|
+
else if (showdown)
|
|
2872
|
+
battle.rng.range(u3, 1, 8)
|
|
2873
|
+
else duration: {
|
|
2874
|
+
while (true) {
|
|
2875
|
+
const r = battle.rng.next() & 7;
|
|
2876
|
+
if (r != 0) break :duration @intCast(r);
|
|
2877
|
+
}
|
|
2878
|
+
};
|
|
2879
|
+
|
|
2880
|
+
assert(duration >= 1 and duration <= 7);
|
|
2881
|
+
options.chance.duration(.sleep, player, player.foe(), duration);
|
|
2882
|
+
return duration;
|
|
2730
2883
|
}
|
|
2731
2884
|
|
|
2732
|
-
inline fn
|
|
2733
|
-
if (
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2885
|
+
inline fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
|
|
2886
|
+
const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
|
|
2887
|
+
val
|
|
2888
|
+
else if (showdown)
|
|
2889
|
+
battle.rng.range(u4, 1, 9)
|
|
2890
|
+
else
|
|
2891
|
+
@intCast((battle.rng.next() & 7) + 1);
|
|
2892
|
+
|
|
2893
|
+
assert(duration >= 1 and duration <= 8);
|
|
2894
|
+
options.chance.duration(.disable, player, player.foe(), duration);
|
|
2895
|
+
return duration;
|
|
2738
2896
|
}
|
|
2739
2897
|
|
|
2740
|
-
inline fn
|
|
2741
|
-
if (
|
|
2742
|
-
|
|
2743
|
-
|
|
2898
|
+
inline fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
|
|
2899
|
+
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2900
|
+
@intCast(val)
|
|
2901
|
+
else if (showdown)
|
|
2902
|
+
battle.rng.range(u3, 2, 6)
|
|
2903
|
+
else
|
|
2904
|
+
@intCast((battle.rng.next() & 3) + 2);
|
|
2905
|
+
|
|
2906
|
+
assert(duration >= 2 and duration <= 5);
|
|
2907
|
+
options.chance.duration(.confusion, player, if (self) player else player.foe(), duration);
|
|
2908
|
+
return duration;
|
|
2744
2909
|
}
|
|
2745
2910
|
|
|
2746
|
-
inline fn
|
|
2747
|
-
if (
|
|
2748
|
-
|
|
2911
|
+
inline fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2912
|
+
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2913
|
+
@intCast(val)
|
|
2914
|
+
else if (showdown)
|
|
2915
|
+
battle.rng.range(u3, 2, 4)
|
|
2916
|
+
else
|
|
2917
|
+
@intCast((battle.rng.next() & 1) + 2);
|
|
2918
|
+
|
|
2919
|
+
assert(duration >= 2 and duration <= 3);
|
|
2920
|
+
options.chance.duration(.attacking, player, player, duration);
|
|
2921
|
+
return duration;
|
|
2922
|
+
}
|
|
2923
|
+
|
|
2924
|
+
const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
|
|
2925
|
+
|
|
2926
|
+
inline fn distribution(
|
|
2927
|
+
battle: anytype,
|
|
2928
|
+
comptime effect: Move.Effect,
|
|
2929
|
+
player: Player,
|
|
2930
|
+
options: anytype,
|
|
2931
|
+
) !u3 {
|
|
2932
|
+
const roll = if (effect == .Binding) .duration else .multi_hit;
|
|
2933
|
+
const n: u3 = if (options.calc.overridden(player, roll)) |val|
|
|
2934
|
+
@intCast(val)
|
|
2935
|
+
else if (showdown)
|
|
2936
|
+
DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
|
|
2937
|
+
else n: {
|
|
2938
|
+
const r = (battle.rng.next() & 3);
|
|
2939
|
+
break :n @intCast((if (r < 2) r else battle.rng.next() & 3) + 2);
|
|
2940
|
+
};
|
|
2941
|
+
|
|
2942
|
+
assert(n >= 2 and n <= 5);
|
|
2943
|
+
if (effect == .Binding) {
|
|
2944
|
+
options.chance.duration(.binding, player, player, n);
|
|
2945
|
+
} else {
|
|
2946
|
+
try options.chance.multiHit(player, n);
|
|
2947
|
+
}
|
|
2948
|
+
return n;
|
|
2949
|
+
}
|
|
2950
|
+
|
|
2951
|
+
fn moveSlot(
|
|
2952
|
+
battle: anytype,
|
|
2953
|
+
player: Player,
|
|
2954
|
+
moves: []MoveSlot,
|
|
2955
|
+
check_pp: u4,
|
|
2956
|
+
options: anytype,
|
|
2957
|
+
) !u4 {
|
|
2958
|
+
// TODO: consider throwing error instead of rerolling?
|
|
2959
|
+
const overridden = if (options.calc.overridden(player, .move_slot)) |val|
|
|
2960
|
+
if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
|
|
2961
|
+
val
|
|
2962
|
+
else
|
|
2963
|
+
null
|
|
2964
|
+
else
|
|
2965
|
+
null;
|
|
2966
|
+
|
|
2967
|
+
const slot: u4 = overridden orelse slot: {
|
|
2968
|
+
if (showdown) {
|
|
2969
|
+
if (check_pp == 0) {
|
|
2970
|
+
var i: usize = moves.len;
|
|
2971
|
+
while (i > 0) {
|
|
2972
|
+
i -= 1;
|
|
2973
|
+
if (moves[i].id != .None) {
|
|
2974
|
+
break :slot battle.rng.range(u4, 0, @as(u4, @intCast(i + 1))) + 1;
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
} else {
|
|
2978
|
+
var r = battle.rng.range(u4, 0, check_pp) + 1;
|
|
2979
|
+
var i: usize = 0;
|
|
2980
|
+
while (i < moves.len and r > 0) : (i += 1) {
|
|
2981
|
+
if (moves[i].pp > 0) {
|
|
2982
|
+
r -= 1;
|
|
2983
|
+
if (r == 0) break :slot @intCast(i + 1);
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
|
|
2989
|
+
while (true) {
|
|
2990
|
+
const r: u4 = @intCast(battle.rng.next() & 3);
|
|
2991
|
+
if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) break :slot r + 1;
|
|
2992
|
+
}
|
|
2993
|
+
};
|
|
2994
|
+
|
|
2995
|
+
try options.chance.moveSlot(player, slot, moves, check_pp);
|
|
2996
|
+
return slot;
|
|
2749
2997
|
}
|
|
2750
2998
|
};
|
|
2751
2999
|
|
|
@@ -2753,14 +3001,14 @@ test "RNG agreement" {
|
|
|
2753
3001
|
if (!showdown) return;
|
|
2754
3002
|
var expected: [256]u32 = undefined;
|
|
2755
3003
|
for (0..expected.len) |i| {
|
|
2756
|
-
expected[i] = @intCast(
|
|
3004
|
+
expected[i] = @intCast(i * 0x1000000);
|
|
2757
3005
|
}
|
|
2758
3006
|
|
|
2759
|
-
var spe
|
|
2760
|
-
var cfz
|
|
2761
|
-
var par
|
|
2762
|
-
var brn
|
|
2763
|
-
var eff
|
|
3007
|
+
var spe: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
|
|
3008
|
+
var cfz: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
|
|
3009
|
+
var par: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
|
|
3010
|
+
var brn: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
|
|
3011
|
+
var eff: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
|
|
2764
3012
|
|
|
2765
3013
|
for (0..expected.len) |i| {
|
|
2766
3014
|
try expectEqual(spe.range(u8, 0, 2) == 0, i < Gen12.percent(50) + 1);
|
|
@@ -2796,7 +3044,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
2796
3044
|
const side = battle.side(player);
|
|
2797
3045
|
const foe = battle.foe(player);
|
|
2798
3046
|
|
|
2799
|
-
var active = side.active;
|
|
3047
|
+
var active = &side.active;
|
|
2800
3048
|
const stored = side.stored();
|
|
2801
3049
|
|
|
2802
3050
|
// While players are not given any input options on the cartridge in these cases,
|
|
@@ -2810,7 +3058,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
2810
3058
|
// strictly correct as the player would not have been presented the option to move or
|
|
2811
3059
|
// switch at all.
|
|
2812
3060
|
if (isForced(active)) {
|
|
2813
|
-
out[n] = .{ .type = .Move, .data = @
|
|
3061
|
+
out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
|
|
2814
3062
|
n += 1;
|
|
2815
3063
|
return n;
|
|
2816
3064
|
}
|
|
@@ -2850,7 +3098,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
2850
3098
|
if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
|
|
2851
3099
|
// Pokémon Showdown displays Struggle if limited to Bide but unable to pick
|
|
2852
3100
|
const struggle =
|
|
2853
|
-
m.id == .Bide and (m.pp == 0 or active.volatiles.
|
|
3101
|
+
m.id == .Bide and (m.pp == 0 or active.volatiles.disable_move == slot);
|
|
2854
3102
|
const s = if (struggle) 0 else slot;
|
|
2855
3103
|
out[n] = .{ .type = .Move, .data = s };
|
|
2856
3104
|
n += 1;
|
|
@@ -2865,7 +3113,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
2865
3113
|
const m = active.moves[slot - 1];
|
|
2866
3114
|
if (m.id == .None) break;
|
|
2867
3115
|
if (m.pp == 0) continue;
|
|
2868
|
-
if (active.volatiles.
|
|
3116
|
+
if (active.volatiles.disable_move == slot) continue;
|
|
2869
3117
|
out[n] = .{ .type = .Move, .data = slot };
|
|
2870
3118
|
n += 1;
|
|
2871
3119
|
}
|