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