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