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