@pkmn/engine 0.1.0-dev.a324c50f → 0.1.0-dev.a77ab8e2
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 +87 -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 +3 -2
- 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 +259 -117
- package/package.json +57 -25
- package/src/bin/install-pkmn-engine +31 -21
- package/src/lib/common/array.zig +77 -0
- 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 +120 -0
- package/src/lib/common/optional.zig +71 -0
- package/src/lib/common/options.zig +2 -0
- package/src/lib/common/protocol.zig +810 -327
- package/src/lib/common/rational.zig +447 -0
- package/src/lib/common/rng.zig +22 -13
- package/src/lib/common/util.zig +61 -0
- package/src/lib/gen1/calc.zig +806 -0
- package/src/lib/gen1/chance.zig +1167 -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 +58 -64
- package/src/lib/gen1/helpers.zig +95 -397
- package/src/lib/gen1/mechanics.zig +587 -550
- package/src/lib/gen2/calc.zig +161 -0
- package/src/lib/gen2/chance.zig +905 -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 +107 -79
- package/src/lib/gen2/generated.zig +806 -0
- package/src/lib/gen2/helpers.zig +49 -21
- package/src/lib/gen2/mechanics.zig +3563 -9
- package/src/lib/{binding/node.zig → node.zig} +10 -13
- package/src/lib/pkmn.zig +32 -15
- 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 {
|
|
@@ -522,36 +486,41 @@ fn beforeMove(
|
|
|
522
486
|
|
|
523
487
|
if (Status.is(stored.status, .SLP)) {
|
|
524
488
|
const before = stored.status;
|
|
489
|
+
const slf = Status.is(stored.status, .EXT);
|
|
525
490
|
// Even if the EXT bit is set this will still correctly modify the sleep duration
|
|
526
|
-
if (options.calc.
|
|
527
|
-
|
|
491
|
+
if (options.calc.overridden(player, .sleep)) |obs| switch (obs) {
|
|
492
|
+
.started, .ended => stored.status = 0,
|
|
493
|
+
.continuing => if (Status.duration(stored.status) > 1) {
|
|
494
|
+
stored.status -= 1;
|
|
495
|
+
},
|
|
496
|
+
else => unreachable,
|
|
528
497
|
} else {
|
|
529
498
|
stored.status -= 1;
|
|
530
499
|
}
|
|
531
500
|
|
|
532
501
|
const duration = Status.duration(stored.status);
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
502
|
+
try options.chance.sleep(player, if (slf)
|
|
503
|
+
.None
|
|
504
|
+
else if (duration == 0) .ended else .continuing);
|
|
536
505
|
|
|
537
506
|
if (duration == 0) {
|
|
538
|
-
try log.curestatus(ident, before, .Message);
|
|
507
|
+
try log.curestatus(.{ ident, before, .Message });
|
|
539
508
|
stored.status = 0; // clears EXT if present
|
|
540
509
|
} else {
|
|
541
|
-
try log.cant(ident, .Sleep);
|
|
510
|
+
try log.cant(.{ ident, .Sleep });
|
|
542
511
|
}
|
|
543
512
|
side.last_used_move = .None;
|
|
544
513
|
return .done;
|
|
545
514
|
}
|
|
546
515
|
|
|
547
516
|
if (Status.is(stored.status, .FRZ)) {
|
|
548
|
-
try log.cant(ident, .Freeze);
|
|
517
|
+
try log.cant(.{ ident, .Freeze });
|
|
549
518
|
side.last_used_move = .None;
|
|
550
519
|
return .done;
|
|
551
520
|
}
|
|
552
521
|
|
|
553
522
|
if (skip or foe.active.volatiles.Binding) {
|
|
554
|
-
try log.cant(ident, .Bound);
|
|
523
|
+
try log.cant(.{ ident, .Bound });
|
|
555
524
|
return .done;
|
|
556
525
|
}
|
|
557
526
|
|
|
@@ -559,27 +528,27 @@ fn beforeMove(
|
|
|
559
528
|
// Pokémon Showdown doesn't clear Flinch until its imaginary "residual" phase, meaning
|
|
560
529
|
// Pokémon can sometimes flinch multiple times from the same original hit
|
|
561
530
|
if (!showdown) volatiles.Flinch = false;
|
|
562
|
-
try log.cant(ident, .Flinch);
|
|
531
|
+
try log.cant(.{ ident, .Flinch });
|
|
563
532
|
return .done;
|
|
564
533
|
}
|
|
565
534
|
|
|
566
535
|
if (volatiles.Recharging) {
|
|
567
536
|
volatiles.Recharging = false;
|
|
568
|
-
try log.cant(ident, .Recharge);
|
|
537
|
+
try log.cant(.{ ident, .Recharge });
|
|
569
538
|
return .done;
|
|
570
539
|
}
|
|
571
540
|
|
|
572
|
-
if (volatiles.
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
541
|
+
if (volatiles.disable_duration > 0) {
|
|
542
|
+
volatiles.disable_duration =
|
|
543
|
+
decrement(.disable, player, options, volatiles.disable_duration);
|
|
544
|
+
try options.chance.disable(
|
|
545
|
+
player,
|
|
546
|
+
if (volatiles.disable_duration == 0) .ended else .continuing,
|
|
547
|
+
);
|
|
579
548
|
|
|
580
|
-
if (volatiles.
|
|
581
|
-
volatiles.
|
|
582
|
-
try log.end(ident, .Disable);
|
|
549
|
+
if (volatiles.disable_duration == 0) {
|
|
550
|
+
volatiles.disable_move = 0;
|
|
551
|
+
try log.end(.{ ident, .Disable });
|
|
583
552
|
}
|
|
584
553
|
}
|
|
585
554
|
// Pokémon Showdown's disable condition has a single onBeforeMove handler
|
|
@@ -594,19 +563,25 @@ fn beforeMove(
|
|
|
594
563
|
|
|
595
564
|
if (volatiles.Confusion) {
|
|
596
565
|
assert(volatiles.confusion > 0);
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
if (
|
|
566
|
+
if (options.calc.overridden(player, .confusion)) |obs| switch (obs) {
|
|
567
|
+
.started => volatiles.confusion -= 1,
|
|
568
|
+
.continuing => if (volatiles.confusion > 1) {
|
|
569
|
+
volatiles.confusion -= 1;
|
|
570
|
+
},
|
|
571
|
+
.ended => volatiles.confusion = 0,
|
|
572
|
+
.overwritten => {},
|
|
573
|
+
else => unreachable,
|
|
600
574
|
} else {
|
|
601
575
|
volatiles.confusion -= 1;
|
|
602
576
|
}
|
|
603
|
-
|
|
577
|
+
|
|
578
|
+
try options.chance.confusion(player, if (volatiles.confusion == 0) .ended else .continuing);
|
|
604
579
|
|
|
605
580
|
if (volatiles.confusion == 0) {
|
|
606
581
|
volatiles.Confusion = false;
|
|
607
|
-
try log.end(ident, .Confusion);
|
|
582
|
+
try log.end(.{ ident, .Confusion });
|
|
608
583
|
} else {
|
|
609
|
-
try log.activate(ident, .Confusion);
|
|
584
|
+
try log.activate(.{ ident, .Confusion });
|
|
610
585
|
|
|
611
586
|
if (try Rolls.confused(battle, player, options)) {
|
|
612
587
|
assert(!volatiles.MultiHit);
|
|
@@ -618,7 +593,9 @@ fn beforeMove(
|
|
|
618
593
|
volatiles.Charging = false;
|
|
619
594
|
volatiles.Binding = false;
|
|
620
595
|
volatiles.Invulnerable = false;
|
|
621
|
-
|
|
596
|
+
|
|
597
|
+
options.chance.observe(.attacking, 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,12 @@ fn beforeMove(
|
|
|
655
634
|
volatiles.Thrashing = false;
|
|
656
635
|
volatiles.Charging = false;
|
|
657
636
|
volatiles.Binding = false;
|
|
658
|
-
|
|
637
|
+
|
|
638
|
+
options.chance.observe(.attacking, player, .None);
|
|
639
|
+
options.chance.observe(.binding, player, .None);
|
|
640
|
+
|
|
659
641
|
// GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
|
|
660
|
-
try log.cant(ident, .Paralysis);
|
|
642
|
+
try log.cant(.{ ident, .Paralysis });
|
|
661
643
|
return .done;
|
|
662
644
|
}
|
|
663
645
|
|
|
@@ -673,28 +655,22 @@ fn beforeMove(
|
|
|
673
655
|
volatiles.state +%= battle.last_damage;
|
|
674
656
|
}
|
|
675
657
|
|
|
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);
|
|
658
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
659
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
684
660
|
|
|
685
661
|
if (volatiles.attacks != 0) {
|
|
686
|
-
try log.activate(ident, .Bide);
|
|
662
|
+
try log.activate(.{ ident, .Bide });
|
|
687
663
|
return .done;
|
|
688
664
|
}
|
|
689
665
|
|
|
690
666
|
volatiles.Bide = false;
|
|
691
|
-
try log.end(ident, .Bide);
|
|
667
|
+
try log.end(.{ ident, .Bide });
|
|
692
668
|
|
|
693
669
|
battle.last_damage = volatiles.state *% 2;
|
|
694
670
|
volatiles.state = 0;
|
|
695
671
|
|
|
696
672
|
if (battle.last_damage == 0) {
|
|
697
|
-
try log.fail(ident, .None);
|
|
673
|
+
try log.fail(.{ ident, .None });
|
|
698
674
|
return .done;
|
|
699
675
|
}
|
|
700
676
|
|
|
@@ -714,47 +690,38 @@ fn beforeMove(
|
|
|
714
690
|
}
|
|
715
691
|
|
|
716
692
|
if (volatiles.Thrashing) {
|
|
717
|
-
|
|
718
|
-
assert(from != null);
|
|
693
|
+
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
719
694
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
} else {
|
|
723
|
-
volatiles.attacks -= 1;
|
|
724
|
-
}
|
|
725
|
-
try options.chance.attacking(player, volatiles.attacks);
|
|
695
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
696
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
726
697
|
|
|
727
|
-
if (
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
698
|
+
if (volatiles.attacks == 0) {
|
|
699
|
+
const overwritten = volatiles.Confusion;
|
|
700
|
+
|
|
701
|
+
volatiles.Thrashing = false;
|
|
702
|
+
volatiles.Confusion = true;
|
|
703
|
+
volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
704
|
+
|
|
705
|
+
// On Pokémon Showdown this could leak information to the opponent that thrashing has
|
|
706
|
+
// ended (the engine's protocol is always from the omniscient perspective and clients
|
|
707
|
+
// can be expected to recognize this pattern and pass the information only to the
|
|
708
|
+
// correct player)
|
|
709
|
+
try log.start(.{ battle.active(player), .ConfusionSilent });
|
|
710
|
+
options.chance.observe(.confusion, player, if (overwritten) .overwritten else .started);
|
|
738
711
|
}
|
|
712
|
+
|
|
713
|
+
// This shouldn't actually set last_used_move, but Pokémon Showdown sets last
|
|
714
|
+
// used in useMove and doesn't have the notion of skipping canMove semantics
|
|
715
|
+
if (showdown) side.last_used_move = side.last_selected_move;
|
|
716
|
+
|
|
739
717
|
return .skip_can;
|
|
740
718
|
}
|
|
741
719
|
|
|
742
720
|
if (volatiles.Binding) {
|
|
743
|
-
|
|
721
|
+
volatiles.attacks = decrement(.binding, player, options, volatiles.attacks);
|
|
722
|
+
try options.chance.binding(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
744
723
|
|
|
745
|
-
|
|
746
|
-
if (!extend) volatiles.attacks = 0;
|
|
747
|
-
} else {
|
|
748
|
-
volatiles.attacks -= 1;
|
|
749
|
-
}
|
|
750
|
-
try options.chance.binding(player, volatiles.attacks);
|
|
751
|
-
|
|
752
|
-
try log.move(
|
|
753
|
-
battle.active(player),
|
|
754
|
-
side.last_selected_move,
|
|
755
|
-
battle.active(player.foe()),
|
|
756
|
-
side.last_selected_move,
|
|
757
|
-
);
|
|
724
|
+
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
758
725
|
if (showdown or battle.last_damage != 0) {
|
|
759
726
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
760
727
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
@@ -774,7 +741,7 @@ fn canMove(
|
|
|
774
741
|
mslot: u4,
|
|
775
742
|
auto: bool,
|
|
776
743
|
skip_pp: bool,
|
|
777
|
-
from:
|
|
744
|
+
from: Move,
|
|
778
745
|
residual: *bool,
|
|
779
746
|
options: anytype,
|
|
780
747
|
) !bool {
|
|
@@ -782,33 +749,21 @@ fn canMove(
|
|
|
782
749
|
const player_ident = battle.active(player);
|
|
783
750
|
const move = Move.get(side.last_selected_move);
|
|
784
751
|
|
|
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
752
|
if (side.active.volatiles.Charging) {
|
|
791
753
|
side.active.volatiles.Charging = false;
|
|
792
754
|
side.active.volatiles.Invulnerable = false;
|
|
793
755
|
} else if (move.effect == .Charge) {
|
|
794
|
-
try options.log.move(player_ident, side.last_selected_move,
|
|
756
|
+
try options.log.move(.{ player_ident, side.last_selected_move, ID{}, from });
|
|
795
757
|
setCounterable(battle, player, side, move);
|
|
796
758
|
try Effects.charge(battle, player, options);
|
|
797
759
|
return false;
|
|
798
760
|
}
|
|
799
761
|
|
|
800
|
-
|
|
801
|
-
if (!
|
|
762
|
+
side.last_used_move = side.last_selected_move;
|
|
763
|
+
if (!skip_pp) decrementPP(side, mslot, auto);
|
|
802
764
|
|
|
803
765
|
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);
|
|
766
|
+
try options.log.move(.{ player_ident, side.last_selected_move, battle.active(target), from });
|
|
812
767
|
setCounterable(battle, player, side, move);
|
|
813
768
|
|
|
814
769
|
if (move.effect.onBegin()) {
|
|
@@ -834,11 +789,7 @@ fn setCounterable(battle: anytype, player: Player, side: *Side, move: Move.Data)
|
|
|
834
789
|
// as the desync results from actually having two separate battle states that subtly disagree
|
|
835
790
|
const counterable = side.last_selected_move != .Counter and move.bp > 0 and
|
|
836
791
|
(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
|
-
}
|
|
792
|
+
battle.lastMove(player).counterable = @intFromBool(counterable);
|
|
842
793
|
}
|
|
843
794
|
|
|
844
795
|
fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
|
|
@@ -850,16 +801,18 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
|
|
|
850
801
|
assert(!volatiles.Rage and !volatiles.Thrashing and !volatiles.MultiHit);
|
|
851
802
|
if (volatiles.Bide) return;
|
|
852
803
|
|
|
853
|
-
|
|
854
|
-
|
|
804
|
+
var move_slot = active.move(mslot);
|
|
805
|
+
assert(move_slot.pp > 0 or auto);
|
|
806
|
+
move_slot.pp = @as(u6, @intCast(move_slot.pp)) -% 1;
|
|
855
807
|
if (volatiles.Transform) return;
|
|
856
808
|
|
|
857
|
-
|
|
858
|
-
|
|
809
|
+
move_slot = side.stored().move(mslot);
|
|
810
|
+
assert(move_slot.pp > 0 or auto);
|
|
811
|
+
move_slot.pp = @as(u6, @intCast(move_slot.pp)) -% 1;
|
|
859
812
|
assert(active.move(mslot).pp == side.stored().move(mslot).pp);
|
|
860
813
|
}
|
|
861
814
|
|
|
862
|
-
fn incrementPP(side: *Side, mslot: u4) void {
|
|
815
|
+
fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
|
|
863
816
|
var active = &side.active;
|
|
864
817
|
const volatiles = &active.volatiles;
|
|
865
818
|
|
|
@@ -867,8 +820,11 @@ fn incrementPP(side: *Side, mslot: u4) void {
|
|
|
867
820
|
// GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
|
|
868
821
|
if (showdown and volatiles.Transform) return;
|
|
869
822
|
|
|
823
|
+
const n = options.calc.overridden(player, .pp) orelse 1;
|
|
824
|
+
if (volatiles.Transform and !options.chance.pp(player, n)) return;
|
|
825
|
+
|
|
870
826
|
assert(mslot > 0 and mslot <= 4);
|
|
871
|
-
side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +%
|
|
827
|
+
side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% n;
|
|
872
828
|
}
|
|
873
829
|
|
|
874
830
|
// Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
|
|
@@ -916,13 +872,13 @@ fn doMove(
|
|
|
916
872
|
break :miss true;
|
|
917
873
|
}
|
|
918
874
|
break :miss (move.target != .Self and
|
|
919
|
-
!moveHit(battle, player, move, &immune, &mist, options));
|
|
875
|
+
!try moveHit(battle, player, move, &immune, &mist, options));
|
|
920
876
|
};
|
|
921
877
|
assert(!immune or miss or (showdown and move.effect == .Binding));
|
|
922
878
|
|
|
923
879
|
var late = showdown and move.effect != .Explode;
|
|
924
880
|
const skip = status or immune;
|
|
925
|
-
if ((!showdown or (!skip or counter)) and !miss)
|
|
881
|
+
if ((!showdown or (!skip or counter)) and !miss) damage: {
|
|
926
882
|
if (showdown and move.effect.isMulti()) {
|
|
927
883
|
try Effects.multiHit(battle, player, move, options);
|
|
928
884
|
hits = side.active.volatiles.attacks;
|
|
@@ -944,8 +900,9 @@ fn doMove(
|
|
|
944
900
|
ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
|
|
945
901
|
// This can overflow after adjustDamage, but will still be sufficient to OHKO
|
|
946
902
|
battle.last_damage = if (ohko) 65535 else 0;
|
|
947
|
-
if (showdown) break :
|
|
948
|
-
} else if (!calcDamage(battle, player, player.foe(), move, crit)) {
|
|
903
|
+
if (showdown) break :damage; // skip adjustDamage / randomizeDamage
|
|
904
|
+
} else if (!calcDamage(battle, player, player.foe(), move, crit, options)) {
|
|
905
|
+
if (!showdown) try options.chance.commit(player, .err);
|
|
949
906
|
return @as(?Result, Result.Error);
|
|
950
907
|
}
|
|
951
908
|
if (battle.last_damage == 0) {
|
|
@@ -959,11 +916,12 @@ fn doMove(
|
|
|
959
916
|
}
|
|
960
917
|
}
|
|
961
918
|
|
|
962
|
-
var
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
919
|
+
var missed: bool = undefined;
|
|
920
|
+
const zero = battle.last_damage == 0;
|
|
921
|
+
if (!showdown and !skip) {
|
|
922
|
+
missed = !try moveHit(battle, player, move, &immune, &mist, options);
|
|
923
|
+
miss = missed or zero;
|
|
924
|
+
}
|
|
967
925
|
|
|
968
926
|
assert(showdown or miss or battle.last_damage > 0 or skip);
|
|
969
927
|
assert((!showdown and miss) or !(ohko and immune));
|
|
@@ -987,16 +945,37 @@ fn doMove(
|
|
|
987
945
|
ohko = (!showdown or (!immune and !invulnerable)) and
|
|
988
946
|
move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
|
|
989
947
|
if (ohko) {
|
|
990
|
-
try log.immune(foe_ident, .OHKO);
|
|
948
|
+
try log.immune(.{ foe_ident, .OHKO });
|
|
991
949
|
} else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
|
|
992
|
-
try log.immune(foe_ident, .None);
|
|
950
|
+
try log.immune(.{ foe_ident, .None });
|
|
951
|
+
if (!showdown) {
|
|
952
|
+
if (move.effect == .Binding) {
|
|
953
|
+
// Rather obnoxiously, the cartridge sets the Binding volatile in canMove and
|
|
954
|
+
// then simply clears it later on if the move happens to miss in moveHit.
|
|
955
|
+
// However, moveHit can return true and we'll still end up in the outer miss
|
|
956
|
+
// handling block because attacking into an immune Pokemon does zero damage
|
|
957
|
+
// (considered missing)
|
|
958
|
+
assert(zero);
|
|
959
|
+
// Thus we still need to actually save the hit result (but not the crit roll).
|
|
960
|
+
// Using .miss for this commit would accomplish what we want, but is somewhat
|
|
961
|
+
// misleading as its entirely possible the move actually did hit (and the target
|
|
962
|
+
// is now trapped) so we use the .binding value instead which is effectively
|
|
963
|
+
// identical, just doesn't claim to know about the actual results of the hit
|
|
964
|
+
// roll)
|
|
965
|
+
try options.chance.commit(player, .binding);
|
|
966
|
+
} else {
|
|
967
|
+
// The .glitch option allows for conditionally committing if we are in a
|
|
968
|
+
// division-by-zero freeze alternative timeline scenario
|
|
969
|
+
try options.chance.commit(player, .glitch);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
993
972
|
} else if (mist) {
|
|
994
|
-
if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
|
|
995
|
-
try log.fail(foe_ident, .None);
|
|
973
|
+
if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
|
|
974
|
+
try log.fail(.{ foe_ident, .None });
|
|
996
975
|
} else {
|
|
997
|
-
if (showdown
|
|
998
|
-
try log.lastmiss();
|
|
999
|
-
try log.miss(battle.active(player));
|
|
976
|
+
if (!showdown) try options.chance.commit(player, if (zero and !missed) .hit else .miss);
|
|
977
|
+
try log.lastmiss(.{});
|
|
978
|
+
try log.miss(.{battle.active(player)});
|
|
1000
979
|
}
|
|
1001
980
|
if (move.effect == .JumpKick) {
|
|
1002
981
|
// Recoil is supposed to be damage/8 but damage will always be 0 here
|
|
@@ -1009,17 +988,15 @@ fn doMove(
|
|
|
1009
988
|
try buildRage(battle, player.foe(), options);
|
|
1010
989
|
} else if (showdown and move.effect == .Disable) {
|
|
1011
990
|
try buildRage(battle, player.foe(), options);
|
|
1012
|
-
} else if (!showdown and immune and move.effect == .Binding) {
|
|
1013
|
-
try options.chance.commit(player, .binding);
|
|
1014
991
|
}
|
|
1015
992
|
return null;
|
|
1016
|
-
} else {
|
|
993
|
+
} else if (!showdown) {
|
|
1017
994
|
try options.chance.commit(player, .hit);
|
|
1018
995
|
}
|
|
1019
996
|
|
|
1020
997
|
// On the cartridge MultiHit doesn't get set up until after damage has been applied for the
|
|
1021
998
|
// first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
|
|
1022
|
-
// it up above before damage calculation)
|
|
999
|
+
// it up above before damage calculation)
|
|
1023
1000
|
if (!showdown and move.effect.isMulti()) {
|
|
1024
1001
|
try Effects.multiHit(battle, player, move, options);
|
|
1025
1002
|
hits = side.active.volatiles.attacks;
|
|
@@ -1032,15 +1009,15 @@ fn doMove(
|
|
|
1032
1009
|
var hit: u4 = 0;
|
|
1033
1010
|
while (hit < hits) {
|
|
1034
1011
|
if (hit == 0) {
|
|
1035
|
-
if (crit) try log.crit(battle.active(player.foe()));
|
|
1012
|
+
if (crit) try log.crit(.{battle.active(player.foe())});
|
|
1036
1013
|
if (effectiveness > Effectiveness.neutral) {
|
|
1037
|
-
try log.supereffective(battle.active(player.foe()));
|
|
1014
|
+
try log.supereffective(.{battle.active(player.foe())});
|
|
1038
1015
|
} else if (effectiveness < Effectiveness.neutral) {
|
|
1039
|
-
try log.resisted(battle.active(player.foe()));
|
|
1016
|
+
try log.resisted(.{battle.active(player.foe())});
|
|
1040
1017
|
}
|
|
1041
1018
|
}
|
|
1042
1019
|
if (!skip) nullified = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
1043
|
-
if (hit == 0 and ohko) try log.ohko();
|
|
1020
|
+
if (hit == 0 and ohko) try log.ohko(.{});
|
|
1044
1021
|
hit += 1;
|
|
1045
1022
|
if (foe.stored().hp == 0) break;
|
|
1046
1023
|
if (!late and (!sub or move.effect == .Explode)) {
|
|
@@ -1057,7 +1034,7 @@ fn doMove(
|
|
|
1057
1034
|
if (showdown and move.effect == .Twineedle and !nullified and foe.stored().hp > 0) {
|
|
1058
1035
|
try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
1059
1036
|
}
|
|
1060
|
-
try log.hitcount(battle.active(player.foe()), hit);
|
|
1037
|
+
try log.hitcount(.{ battle.active(player.foe()), hit });
|
|
1061
1038
|
} else if (showdown) {
|
|
1062
1039
|
// This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
|
|
1063
1040
|
if (move.effect == .Binding) {
|
|
@@ -1067,9 +1044,9 @@ fn doMove(
|
|
|
1067
1044
|
assert(foe.stored().hp > 0);
|
|
1068
1045
|
// Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
|
|
1069
1046
|
if (sub) {
|
|
1070
|
-
try log.activate(battle.active(player.foe()), .Substitute);
|
|
1047
|
+
try log.activate(.{ battle.active(player.foe()), .Substitute });
|
|
1071
1048
|
} else {
|
|
1072
|
-
try log.damage(battle.active(player.foe()), foe.stored(), .None);
|
|
1049
|
+
try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
|
|
1073
1050
|
try buildRage(battle, player.foe(), options);
|
|
1074
1051
|
}
|
|
1075
1052
|
return null;
|
|
@@ -1078,12 +1055,12 @@ fn doMove(
|
|
|
1078
1055
|
}
|
|
1079
1056
|
|
|
1080
1057
|
// 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
|
|
1058
|
+
// if an effect was intended to "always happen" it will still get skipped
|
|
1082
1059
|
if (nullified) return null;
|
|
1083
1060
|
|
|
1084
1061
|
// On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
|
|
1085
1062
|
// 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
|
|
1063
|
+
// setup before the loop means we can avoid having to waste time doing no-op handler searches
|
|
1087
1064
|
if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
|
|
1088
1065
|
|
|
1089
1066
|
if (foe.stored().hp == 0) return null;
|
|
@@ -1098,7 +1075,7 @@ fn doMove(
|
|
|
1098
1075
|
// below). For Twineedle we change the data to that of one with PoisonChance1 given its
|
|
1099
1076
|
// MultiHit behavior is complete after the loop above, though Pokémon Showdown handles the
|
|
1100
1077
|
// Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
|
|
1101
|
-
// puts the |-status| message before |-hitcount| instead of after
|
|
1078
|
+
// puts the |-status| message before |-hitcount| instead of after
|
|
1102
1079
|
if (move.effect == .Twineedle) {
|
|
1103
1080
|
if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
1104
1081
|
} else if (move.effect == .Disable) {
|
|
@@ -1117,12 +1094,14 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
|
|
|
1117
1094
|
const side = battle.side(player);
|
|
1118
1095
|
|
|
1119
1096
|
// Base speed is used for the critical hit calculation, even when Transform-ed
|
|
1120
|
-
var rate =
|
|
1097
|
+
var rate: u16 = Species.chance(side.stored().species);
|
|
1121
1098
|
// GLITCH: Focus Energy reduces critical hit rate instead of increasing it
|
|
1122
1099
|
rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
|
|
1123
1100
|
rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
|
|
1124
1101
|
|
|
1125
|
-
|
|
1102
|
+
const spurious =
|
|
1103
|
+
!showdown and ((move.bp == 0 and move.effect != .OHKO) or move.target == .Depends);
|
|
1104
|
+
return Rolls.criticalHit(battle, player, @intCast(rate), spurious, options);
|
|
1126
1105
|
}
|
|
1127
1106
|
|
|
1128
1107
|
fn calcDamage(
|
|
@@ -1131,6 +1110,7 @@ fn calcDamage(
|
|
|
1131
1110
|
target_player: Player,
|
|
1132
1111
|
m: ?Move.Data,
|
|
1133
1112
|
crit: bool,
|
|
1113
|
+
options: anytype,
|
|
1134
1114
|
) bool {
|
|
1135
1115
|
// Confusion (indicated when m == null) just needs a 40 BP physical move
|
|
1136
1116
|
const cfz = m == null;
|
|
@@ -1142,28 +1122,8 @@ fn calcDamage(
|
|
|
1142
1122
|
|
|
1143
1123
|
const special = move.type.special();
|
|
1144
1124
|
|
|
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
|
|
1125
|
+
var atk = attack(side, crit, special);
|
|
1126
|
+
var def = defense(target, crit, special, cfz);
|
|
1167
1127
|
|
|
1168
1128
|
// Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
|
|
1169
1129
|
// will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
|
|
@@ -1174,9 +1134,9 @@ fn calcDamage(
|
|
|
1174
1134
|
def = @max((def / 4) & 255, if (showdown) 1 else 0);
|
|
1175
1135
|
}
|
|
1176
1136
|
|
|
1177
|
-
const lvl =
|
|
1137
|
+
const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
|
|
1178
1138
|
|
|
1179
|
-
def =
|
|
1139
|
+
def = if (move.effect == .Explode) @max(def / 2, 1) else def;
|
|
1180
1140
|
|
|
1181
1141
|
if (def == 0) return false;
|
|
1182
1142
|
|
|
@@ -1188,11 +1148,41 @@ fn calcDamage(
|
|
|
1188
1148
|
d = @min(997, d);
|
|
1189
1149
|
d += 2;
|
|
1190
1150
|
|
|
1151
|
+
// Whether or not a critical hit occurred can change whether a division-by-zero freeze is
|
|
1152
|
+
// encountered so we need to make sure that in the event of a miss we commit the status of the
|
|
1153
|
+
// crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
|
|
1154
|
+
// that results in counter intuitive probabilities in the common case
|
|
1155
|
+
if (pkmn.options.chance and !showdown) {
|
|
1156
|
+
atk = attack(side, !crit, special);
|
|
1157
|
+
def = defense(target, !crit, special, false);
|
|
1158
|
+
if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1191
1161
|
battle.last_damage = @intCast(d);
|
|
1192
1162
|
|
|
1193
1163
|
return true;
|
|
1194
1164
|
}
|
|
1195
1165
|
|
|
1166
|
+
fn attack(side: *Side, crit: bool, special: bool) u32 {
|
|
1167
|
+
return if (crit)
|
|
1168
|
+
if (special) side.stored().stats.spc else side.stored().stats.atk
|
|
1169
|
+
else if (special) side.active.stats.spc else side.active.stats.atk;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
fn defense(target: *Side, crit: bool, special: bool, cfz: bool) u32 {
|
|
1173
|
+
return if (crit)
|
|
1174
|
+
if (special) target.stored().stats.spc else target.stored().stats.def
|
|
1175
|
+
else
|
|
1176
|
+
// GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
|
|
1177
|
+
if (special)
|
|
1178
|
+
target.active.stats.spc *
|
|
1179
|
+
@as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
|
|
1180
|
+
else
|
|
1181
|
+
// Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
|
|
1182
|
+
target.active.stats.def *
|
|
1183
|
+
@as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1196
1186
|
fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
1197
1187
|
const side = battle.side(player);
|
|
1198
1188
|
const foe = battle.foe(player);
|
|
@@ -1237,7 +1227,6 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1237
1227
|
const foe = battle.foe(player);
|
|
1238
1228
|
|
|
1239
1229
|
if (!try checkHit(battle, player, move, options)) return null;
|
|
1240
|
-
try options.chance.commit(player, .hit);
|
|
1241
1230
|
|
|
1242
1231
|
battle.last_damage = switch (side.last_selected_move) {
|
|
1243
1232
|
.SuperFang => @max(foe.stored().hp / 2, 1),
|
|
@@ -1267,7 +1256,7 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1267
1256
|
const foe = battle.foe(player);
|
|
1268
1257
|
|
|
1269
1258
|
if (battle.last_damage == 0) {
|
|
1270
|
-
try options.log.fail(battle.active(player), .None);
|
|
1259
|
+
try options.log.fail(.{ battle.active(player), .None });
|
|
1271
1260
|
return null;
|
|
1272
1261
|
}
|
|
1273
1262
|
|
|
@@ -1278,33 +1267,28 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1278
1267
|
else
|
|
1279
1268
|
foe.last_selected_move);
|
|
1280
1269
|
|
|
1281
|
-
const used = (
|
|
1282
|
-
battle.last_moves.p1_counterable
|
|
1283
|
-
else
|
|
1284
|
-
battle.last_moves.p2_counterable) != 0;
|
|
1285
|
-
|
|
1270
|
+
const used = battle.lastMove(player.foe()).counterable != 0;
|
|
1286
1271
|
const selected = foe_last_selected_move.bp > 0 and
|
|
1287
1272
|
foe.last_selected_move != .Counter and
|
|
1288
1273
|
(foe_last_selected_move.type == .Normal or
|
|
1289
1274
|
foe_last_selected_move.type == .Fighting);
|
|
1290
1275
|
|
|
1291
1276
|
if (!used and !selected) {
|
|
1292
|
-
try options.log.fail(battle.active(player), .None);
|
|
1277
|
+
try options.log.fail(.{ battle.active(player), .None });
|
|
1293
1278
|
return null;
|
|
1294
1279
|
}
|
|
1295
1280
|
|
|
1296
1281
|
if (!used or !selected) {
|
|
1297
1282
|
// GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
|
|
1298
1283
|
if (!showdown) return Result.Error;
|
|
1299
|
-
try options.log.fail(battle.active(player), .None);
|
|
1284
|
+
try options.log.fail(.{ battle.active(player), .None });
|
|
1300
1285
|
return null;
|
|
1301
1286
|
}
|
|
1302
1287
|
|
|
1303
1288
|
battle.last_damage *|= 2;
|
|
1304
1289
|
|
|
1305
|
-
// Pokémon Showdown calls
|
|
1290
|
+
// Pokémon Showdown calls moveHit before Counter
|
|
1306
1291
|
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
1307
|
-
try options.chance.commit(player, .hit);
|
|
1308
1292
|
|
|
1309
1293
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1310
1294
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
@@ -1332,20 +1316,20 @@ fn applyDamage(
|
|
|
1332
1316
|
subbed.active.volatiles.substitute = 0;
|
|
1333
1317
|
subbed.active.volatiles.Substitute = false;
|
|
1334
1318
|
// 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(
|
|
1319
|
+
try options.log.end(.{ battle.active(sub_player), .Substitute });
|
|
1320
|
+
options.calc.capped(sub_player);
|
|
1337
1321
|
return true;
|
|
1338
1322
|
} else {
|
|
1339
1323
|
// Safe to truncate since less than subbed.volatiles.substitute which is a u8
|
|
1340
1324
|
subbed.active.volatiles.substitute -= @intCast(battle.last_damage);
|
|
1341
|
-
try options.log.activate(battle.active(sub_player), .Substitute);
|
|
1325
|
+
try options.log.activate(.{ battle.active(sub_player), .Substitute });
|
|
1342
1326
|
return false;
|
|
1343
1327
|
}
|
|
1344
1328
|
}
|
|
1345
1329
|
|
|
1346
1330
|
if (battle.last_damage > target.stored().hp) battle.last_damage = target.stored().hp;
|
|
1347
1331
|
target.stored().hp -= battle.last_damage;
|
|
1348
|
-
try options.log.damage(battle.active(target_player), target.stored(), reason);
|
|
1332
|
+
try options.log.damage(.{ battle.active(target_player), target.stored(), reason });
|
|
1349
1333
|
return false;
|
|
1350
1334
|
}
|
|
1351
1335
|
|
|
@@ -1357,17 +1341,17 @@ fn mirrorMove(
|
|
|
1357
1341
|
auto: bool,
|
|
1358
1342
|
residual: *bool,
|
|
1359
1343
|
options: anytype,
|
|
1360
|
-
) !?Result {
|
|
1344
|
+
) anyerror!?Result {
|
|
1361
1345
|
var side = battle.side(player);
|
|
1362
1346
|
const foe = battle.foe(player);
|
|
1363
1347
|
|
|
1364
1348
|
side.last_selected_move = foe.last_used_move;
|
|
1365
1349
|
|
|
1366
1350
|
if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
|
|
1367
|
-
try options.log.fail(battle.active(player), .None);
|
|
1351
|
+
try options.log.fail(.{ battle.active(player), .None });
|
|
1368
1352
|
return null;
|
|
1369
1353
|
} else if (!showdown or foe.last_used_move != .Struggle) {
|
|
1370
|
-
incrementPP(side, mslot);
|
|
1354
|
+
incrementPP(side, player, mslot, options);
|
|
1371
1355
|
}
|
|
1372
1356
|
|
|
1373
1357
|
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
|
|
@@ -1384,11 +1368,11 @@ fn metronome(
|
|
|
1384
1368
|
auto: bool,
|
|
1385
1369
|
residual: *bool,
|
|
1386
1370
|
options: anytype,
|
|
1387
|
-
) !?Result {
|
|
1371
|
+
) anyerror!?Result {
|
|
1388
1372
|
var side = battle.side(player);
|
|
1389
1373
|
|
|
1390
1374
|
side.last_selected_move = try Rolls.metronome(battle, player, options);
|
|
1391
|
-
incrementPP(side, mslot);
|
|
1375
|
+
incrementPP(side, player, mslot, options);
|
|
1392
1376
|
|
|
1393
1377
|
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
|
|
1394
1378
|
return null;
|
|
@@ -1400,20 +1384,23 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
|
|
|
1400
1384
|
var immune = false;
|
|
1401
1385
|
var mist = false;
|
|
1402
1386
|
|
|
1403
|
-
if (moveHit(battle, player, move, &immune, &mist, options))
|
|
1387
|
+
if (try moveHit(battle, player, move, &immune, &mist, options)) {
|
|
1388
|
+
if (!showdown) try options.chance.commit(player, .hit);
|
|
1389
|
+
return true;
|
|
1390
|
+
}
|
|
1404
1391
|
|
|
1405
1392
|
assert(!immune);
|
|
1406
1393
|
if (mist) {
|
|
1407
1394
|
assert(!showdown);
|
|
1408
1395
|
const foe_ident = battle.active(player.foe());
|
|
1409
|
-
try options.log.activate(foe_ident, .Mist);
|
|
1410
|
-
try options.log.fail(foe_ident, .None);
|
|
1396
|
+
try options.log.activate(.{ foe_ident, .Mist });
|
|
1397
|
+
try options.log.fail(.{ foe_ident, .None });
|
|
1411
1398
|
} else {
|
|
1412
|
-
try options.
|
|
1413
|
-
try options.log.
|
|
1414
|
-
try options.log.miss(battle.active(player));
|
|
1399
|
+
try options.log.lastmiss(.{});
|
|
1400
|
+
try options.log.miss(.{battle.active(player)});
|
|
1415
1401
|
}
|
|
1416
1402
|
|
|
1403
|
+
if (!showdown) try options.chance.commit(player, .miss);
|
|
1417
1404
|
return false;
|
|
1418
1405
|
}
|
|
1419
1406
|
|
|
@@ -1424,7 +1411,7 @@ fn moveHit(
|
|
|
1424
1411
|
immune: *bool,
|
|
1425
1412
|
mist: *bool,
|
|
1426
1413
|
options: anytype,
|
|
1427
|
-
) bool {
|
|
1414
|
+
) !bool {
|
|
1428
1415
|
var side = battle.side(player);
|
|
1429
1416
|
const foe = battle.foe(player);
|
|
1430
1417
|
|
|
@@ -1463,7 +1450,7 @@ fn moveHit(
|
|
|
1463
1450
|
const overwritten = overwrite and state > 0;
|
|
1464
1451
|
assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
|
|
1465
1452
|
|
|
1466
|
-
var accuracy = if (overwritten) state else
|
|
1453
|
+
var accuracy: u16 = if (overwritten) state else move.accuracy;
|
|
1467
1454
|
var boost = BOOSTS[@as(u4, @intCast(@as(i8, side.active.boosts.accuracy) + 6))];
|
|
1468
1455
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1469
1456
|
boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
|
|
@@ -1475,7 +1462,7 @@ fn moveHit(
|
|
|
1475
1462
|
if (overwrite) side.active.volatiles.state = accuracy;
|
|
1476
1463
|
|
|
1477
1464
|
// GLITCH: max accuracy is 255 so 1/256 chance of miss
|
|
1478
|
-
break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
|
|
1465
|
+
break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
|
|
1479
1466
|
};
|
|
1480
1467
|
|
|
1481
1468
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1486,13 +1473,17 @@ fn moveHit(
|
|
|
1486
1473
|
|
|
1487
1474
|
if (!miss) return true;
|
|
1488
1475
|
if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
|
|
1476
|
+
|
|
1489
1477
|
side.active.volatiles.Binding = false;
|
|
1478
|
+
options.chance.observe(.binding, player, .None);
|
|
1479
|
+
|
|
1490
1480
|
return false;
|
|
1491
1481
|
}
|
|
1492
1482
|
|
|
1493
1483
|
fn checkFaint(
|
|
1494
1484
|
battle: anytype,
|
|
1495
1485
|
player: Player,
|
|
1486
|
+
cap: bool,
|
|
1496
1487
|
options: anytype,
|
|
1497
1488
|
) @TypeOf(options.log).Error!?Result {
|
|
1498
1489
|
const side = battle.side(player);
|
|
@@ -1502,33 +1493,32 @@ fn checkFaint(
|
|
|
1502
1493
|
const foe_fainted = foe.stored().hp == 0;
|
|
1503
1494
|
|
|
1504
1495
|
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;
|
|
1496
|
+
const foe_out = foe_fainted and findFirstAlive(foe) == 0;
|
|
1497
|
+
const more = player_out or foe_out;
|
|
1508
1498
|
|
|
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;
|
|
1499
|
+
if (try faint(battle, player, !(more or foe_fainted), cap, options)) |r| return r;
|
|
1500
|
+
if (foe_fainted) if (try faint(battle, player.foe(), !more, false, options)) |r| return r;
|
|
1511
1501
|
|
|
1512
1502
|
assert(!side.active.volatiles.MultiHit);
|
|
1513
1503
|
assert(!foe.active.volatiles.MultiHit);
|
|
1514
1504
|
|
|
1515
|
-
if (
|
|
1516
|
-
try options.log.tie();
|
|
1505
|
+
if (player_out and foe_out) {
|
|
1506
|
+
try options.log.tie(.{});
|
|
1517
1507
|
return Result.Tie;
|
|
1518
1508
|
} else if (player_out) {
|
|
1519
|
-
try options.log.win(player.foe());
|
|
1509
|
+
try options.log.win(.{player.foe()});
|
|
1520
1510
|
return if (player == .P1) Result.Lose else Result.Win;
|
|
1521
1511
|
} else if (foe_out) {
|
|
1522
|
-
try options.log.win(player);
|
|
1512
|
+
try options.log.win(.{player});
|
|
1523
1513
|
return if (player == .P1) Result.Win else Result.Lose;
|
|
1524
1514
|
}
|
|
1525
1515
|
|
|
1526
1516
|
const foe_choice: Choice.Type = if (foe_fainted) .Switch else .Pass;
|
|
1527
|
-
if (player == .P1) return
|
|
1528
|
-
return
|
|
1517
|
+
if (player == .P1) return .{ .p1 = .Switch, .p2 = foe_choice };
|
|
1518
|
+
return .{ .p1 = foe_choice, .p2 = .Switch };
|
|
1529
1519
|
}
|
|
1530
1520
|
|
|
1531
|
-
fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
|
|
1521
|
+
fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytype) !?Result {
|
|
1532
1522
|
var side = battle.side(player);
|
|
1533
1523
|
var foe = battle.foe(player);
|
|
1534
1524
|
assert(side.stored().hp == 0);
|
|
@@ -1561,8 +1551,10 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
|
|
|
1561
1551
|
side.stored().stats.spe;
|
|
1562
1552
|
}
|
|
1563
1553
|
|
|
1564
|
-
try options.log.faint(battle.active(player), done);
|
|
1565
|
-
|
|
1554
|
+
try options.log.faint(.{ battle.active(player), done });
|
|
1555
|
+
// We can only safely set capped if the faint occured as the direct result of an opponent's
|
|
1556
|
+
// move (as opposed to due to residual damage, recoil, Explosion, etc)
|
|
1557
|
+
if (cap) options.calc.capped(player);
|
|
1566
1558
|
return null;
|
|
1567
1559
|
}
|
|
1568
1560
|
|
|
@@ -1573,21 +1565,19 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
|
|
|
1573
1565
|
var volatiles = &side.active.volatiles;
|
|
1574
1566
|
|
|
1575
1567
|
const brn = Status.is(stored.status, .BRN);
|
|
1576
|
-
if (brn or Status.is(stored.status, .PSN))
|
|
1568
|
+
if (brn or Status.is(stored.status, .PSN)) {
|
|
1577
1569
|
var damage = @max(stored.stats.hp / 16, 1);
|
|
1578
|
-
|
|
1579
1570
|
if (volatiles.Toxic) {
|
|
1580
1571
|
volatiles.toxic += 1;
|
|
1581
1572
|
damage *= volatiles.toxic;
|
|
1582
1573
|
}
|
|
1574
|
+
damage = @min(damage, stored.hp);
|
|
1583
1575
|
|
|
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);
|
|
1576
|
+
if (!showdown or damage > 0) {
|
|
1577
|
+
stored.hp -= damage;
|
|
1578
|
+
// Pokémon Showdown uses damage [of] here but its not relevant in Generation I
|
|
1579
|
+
try options.log.damage(.{ ident, stored, if (brn) Damage.Burn else Damage.Poison });
|
|
1580
|
+
}
|
|
1591
1581
|
}
|
|
1592
1582
|
|
|
1593
1583
|
if (volatiles.LeechSeed) {
|
|
@@ -1611,14 +1601,14 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
|
|
|
1611
1601
|
const amount = @min(damage, stored.hp);
|
|
1612
1602
|
stored.hp -= amount;
|
|
1613
1603
|
|
|
1614
|
-
// As above, Pokémon Showdown uses
|
|
1615
|
-
if (amount > 0) try options.log.damage(ident, stored, .LeechSeed);
|
|
1604
|
+
// As above, Pokémon Showdown uses damage [of] but its not relevant
|
|
1605
|
+
if (amount > 0) try options.log.damage(.{ ident, stored, .LeechSeed });
|
|
1616
1606
|
|
|
1617
1607
|
const before = foe_stored.hp;
|
|
1618
1608
|
// Uncapped damage is added back to the foe
|
|
1619
1609
|
foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
|
|
1620
1610
|
// 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);
|
|
1611
|
+
if (foe_stored.hp > before) try options.log.heal(.{ foe_ident, foe_stored, .Silent });
|
|
1622
1612
|
}
|
|
1623
1613
|
}
|
|
1624
1614
|
|
|
@@ -1629,18 +1619,18 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
|
|
|
1629
1619
|
battle.turn += 1;
|
|
1630
1620
|
|
|
1631
1621
|
if (showdown and pkmn.options.ebc and checkEBC(battle)) {
|
|
1632
|
-
try options.log.tie();
|
|
1622
|
+
try options.log.tie(.{});
|
|
1633
1623
|
return Result.Tie;
|
|
1634
1624
|
}
|
|
1635
1625
|
|
|
1636
1626
|
if (showdown and battle.turn >= 1000) {
|
|
1637
|
-
try options.log.tie();
|
|
1627
|
+
try options.log.tie(.{});
|
|
1638
1628
|
return Result.Tie;
|
|
1639
1629
|
} else if (battle.turn >= 65535) {
|
|
1640
1630
|
return Result.Error;
|
|
1641
1631
|
}
|
|
1642
1632
|
|
|
1643
|
-
try options.log.turn(battle.turn);
|
|
1633
|
+
try options.log.turn(.{battle.turn});
|
|
1644
1634
|
|
|
1645
1635
|
return Result.Default;
|
|
1646
1636
|
}
|
|
@@ -1700,9 +1690,9 @@ fn checkEBC(battle: anytype) bool {
|
|
|
1700
1690
|
}
|
|
1701
1691
|
|
|
1702
1692
|
fn disabled(side: *Side, ident: ID, options: anytype) !bool {
|
|
1703
|
-
if (side.active.volatiles.
|
|
1693
|
+
if (side.active.volatiles.disable_move != 0) {
|
|
1704
1694
|
// A Pokémon that transforms after being disabled may end up with less move slots
|
|
1705
|
-
const m = side.active.moves[side.active.volatiles.
|
|
1695
|
+
const m = side.active.moves[side.active.volatiles.disable_move - 1].id;
|
|
1706
1696
|
// side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
|
|
1707
1697
|
const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
|
|
1708
1698
|
m
|
|
@@ -1710,32 +1700,21 @@ fn disabled(side: *Side, ident: ID, options: anytype) !bool {
|
|
|
1710
1700
|
side.last_selected_move;
|
|
1711
1701
|
if (m != .None and m == last) {
|
|
1712
1702
|
side.active.volatiles.Charging = false;
|
|
1713
|
-
try options.log.
|
|
1703
|
+
try options.log.cant(.{ ident, .Disable, last });
|
|
1714
1704
|
return true;
|
|
1715
1705
|
}
|
|
1716
1706
|
}
|
|
1717
1707
|
return false;
|
|
1718
1708
|
}
|
|
1719
1709
|
|
|
1720
|
-
|
|
1710
|
+
fn buildRage(battle: anytype, who: Player, options: anytype) !void {
|
|
1721
1711
|
const side = battle.side(who);
|
|
1722
1712
|
if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
|
|
1723
1713
|
try Effects.boost(battle, who, Move.get(.Rage), options);
|
|
1724
1714
|
}
|
|
1725
1715
|
}
|
|
1726
1716
|
|
|
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(
|
|
1717
|
+
fn onBegin(
|
|
1739
1718
|
battle: anytype,
|
|
1740
1719
|
player: Player,
|
|
1741
1720
|
move: Move.Data,
|
|
@@ -1743,7 +1722,6 @@ inline fn onBegin(
|
|
|
1743
1722
|
residual: *bool,
|
|
1744
1723
|
options: anytype,
|
|
1745
1724
|
) !void {
|
|
1746
|
-
assert(move.effect.onBegin());
|
|
1747
1725
|
return switch (move.effect) {
|
|
1748
1726
|
.Confusion => Effects.confusion(battle, player, move, options),
|
|
1749
1727
|
.Conversion => Effects.conversion(battle, player, options),
|
|
@@ -1765,8 +1743,7 @@ inline fn onBegin(
|
|
|
1765
1743
|
};
|
|
1766
1744
|
}
|
|
1767
1745
|
|
|
1768
|
-
|
|
1769
|
-
assert(move.effect.onEnd());
|
|
1746
|
+
fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1770
1747
|
return switch (move.effect) {
|
|
1771
1748
|
.Bide => Effects.bide(battle, player, options),
|
|
1772
1749
|
// zig fmt: off
|
|
@@ -1781,14 +1758,13 @@ inline fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anyty
|
|
|
1781
1758
|
};
|
|
1782
1759
|
}
|
|
1783
1760
|
|
|
1784
|
-
|
|
1761
|
+
fn alwaysHappens(
|
|
1785
1762
|
battle: anytype,
|
|
1786
1763
|
player: Player,
|
|
1787
1764
|
move: Move.Data,
|
|
1788
1765
|
residual: *bool,
|
|
1789
1766
|
options: anytype,
|
|
1790
1767
|
) !void {
|
|
1791
|
-
assert(move.effect.alwaysHappens());
|
|
1792
1768
|
return switch (move.effect) {
|
|
1793
1769
|
.DrainHP, .DreamEater => Effects.drainHP(battle, player, options),
|
|
1794
1770
|
.Explode => Effects.explode(battle, player),
|
|
@@ -1800,7 +1776,7 @@ inline fn alwaysHappens(
|
|
|
1800
1776
|
};
|
|
1801
1777
|
}
|
|
1802
1778
|
|
|
1803
|
-
|
|
1779
|
+
fn moveEffect(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1804
1780
|
return switch (move.effect) {
|
|
1805
1781
|
.BurnChance1, .BurnChance2 => Effects.burnChance(battle, player, move, options),
|
|
1806
1782
|
.ConfusionChance => Effects.confusion(battle, player, move, options),
|
|
@@ -1824,10 +1800,12 @@ pub const Effects = struct {
|
|
|
1824
1800
|
|
|
1825
1801
|
side.active.volatiles.Bide = true;
|
|
1826
1802
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1803
|
+
|
|
1827
1804
|
side.active.volatiles.state = 0;
|
|
1828
1805
|
side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
1806
|
+
options.chance.observe(.attacking, player, .started);
|
|
1829
1807
|
|
|
1830
|
-
try options.log.start(battle.active(player), .Bide);
|
|
1808
|
+
try options.log.start(.{ battle.active(player), .Bide });
|
|
1831
1809
|
}
|
|
1832
1810
|
|
|
1833
1811
|
fn burnChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -1841,11 +1819,11 @@ pub const Effects = struct {
|
|
|
1841
1819
|
// GLITCH: Freeze top move selection desync can occur if thawed Pokémon is slower
|
|
1842
1820
|
if (Status.is(foe_stored.status, .FRZ)) {
|
|
1843
1821
|
assert(move.type == .Fire);
|
|
1844
|
-
try options.log.curestatus(
|
|
1822
|
+
try options.log.curestatus(.{
|
|
1845
1823
|
battle.active(player.foe()),
|
|
1846
1824
|
foe_stored.status,
|
|
1847
1825
|
.Message,
|
|
1848
|
-
);
|
|
1826
|
+
});
|
|
1849
1827
|
foe_stored.status = 0;
|
|
1850
1828
|
}
|
|
1851
1829
|
return;
|
|
@@ -1859,7 +1837,7 @@ pub const Effects = struct {
|
|
|
1859
1837
|
foe_stored.status = Status.init(.BRN);
|
|
1860
1838
|
foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
|
|
1861
1839
|
|
|
1862
|
-
try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
|
|
1840
|
+
try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
|
|
1863
1841
|
}
|
|
1864
1842
|
|
|
1865
1843
|
fn charge(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -1869,8 +1847,8 @@ pub const Effects = struct {
|
|
|
1869
1847
|
volatiles.Charging = true;
|
|
1870
1848
|
const move = side.last_selected_move;
|
|
1871
1849
|
if (move == .Fly or move == .Dig) volatiles.Invulnerable = true;
|
|
1872
|
-
try options.log.laststill();
|
|
1873
|
-
try options.log.prepare(battle.active(player), move);
|
|
1850
|
+
try options.log.laststill(.{});
|
|
1851
|
+
try options.log.prepare(.{ battle.active(player), move });
|
|
1874
1852
|
}
|
|
1875
1853
|
|
|
1876
1854
|
fn confusion(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -1881,42 +1859,38 @@ pub const Effects = struct {
|
|
|
1881
1859
|
if (!try Rolls.confusionChance(battle, player, options)) return;
|
|
1882
1860
|
} else {
|
|
1883
1861
|
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
|
-
}
|
|
1862
|
+
if (!try checkHit(battle, player, move, options)) return;
|
|
1863
|
+
if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
1889
1864
|
} else {
|
|
1890
|
-
if (sub) {
|
|
1891
|
-
|
|
1892
|
-
} else if (!try checkHit(battle, player, move, options)) {
|
|
1893
|
-
return;
|
|
1894
|
-
}
|
|
1865
|
+
if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
1866
|
+
if (!try checkHit(battle, player, move, options)) return;
|
|
1895
1867
|
}
|
|
1896
|
-
try options.chance.commit(player, .hit);
|
|
1897
1868
|
}
|
|
1898
1869
|
|
|
1899
1870
|
if (foe.active.volatiles.Confusion) return;
|
|
1900
1871
|
foe.active.volatiles.Confusion = true;
|
|
1901
|
-
foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
|
|
1902
1872
|
|
|
1903
|
-
|
|
1873
|
+
foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
1874
|
+
options.chance.observe(.confusion, player.foe(), .started);
|
|
1875
|
+
|
|
1876
|
+
try options.log.start(.{ battle.active(player.foe()), .Confusion });
|
|
1904
1877
|
}
|
|
1905
1878
|
|
|
1906
1879
|
fn conversion(battle: anytype, player: Player, options: anytype) !void {
|
|
1907
1880
|
const foe = battle.foe(player);
|
|
1908
1881
|
|
|
1909
1882
|
if (foe.active.volatiles.Invulnerable) {
|
|
1910
|
-
try options.log.lastmiss();
|
|
1911
|
-
return options.log.miss(battle.active(player));
|
|
1883
|
+
try options.log.lastmiss(.{});
|
|
1884
|
+
return options.log.miss(.{battle.active(player)});
|
|
1912
1885
|
}
|
|
1913
1886
|
|
|
1914
1887
|
battle.side(player).active.types = foe.active.types;
|
|
1915
|
-
return options.log.
|
|
1888
|
+
return options.log.start(.{
|
|
1916
1889
|
battle.active(player),
|
|
1890
|
+
.TypeChange,
|
|
1917
1891
|
foe.active.types,
|
|
1918
1892
|
battle.active(player.foe()),
|
|
1919
|
-
);
|
|
1893
|
+
});
|
|
1920
1894
|
}
|
|
1921
1895
|
|
|
1922
1896
|
fn disable(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
|
|
@@ -1925,13 +1899,10 @@ pub const Effects = struct {
|
|
|
1925
1899
|
const foe_ident = battle.active(player.foe());
|
|
1926
1900
|
|
|
1927
1901
|
// 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
|
-
}
|
|
1902
|
+
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
1932
1903
|
|
|
1933
|
-
if (volatiles.
|
|
1934
|
-
try options.log.fail(foe_ident, .None);
|
|
1904
|
+
if (volatiles.disable_move != 0) {
|
|
1905
|
+
try options.log.fail(.{ foe_ident, .None });
|
|
1935
1906
|
return null;
|
|
1936
1907
|
}
|
|
1937
1908
|
|
|
@@ -1947,7 +1918,7 @@ pub const Effects = struct {
|
|
|
1947
1918
|
// Technically this is still considered simply a "miss" on the cartridge,
|
|
1948
1919
|
// but diverging from Pokémon Showdown here would mostly just be pedantic
|
|
1949
1920
|
if (n == 0) {
|
|
1950
|
-
try options.log.fail(foe_ident, .None);
|
|
1921
|
+
try options.log.fail(.{ foe_ident, .None });
|
|
1951
1922
|
return null;
|
|
1952
1923
|
} else if (err) {
|
|
1953
1924
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
@@ -1955,18 +1926,19 @@ pub const Effects = struct {
|
|
|
1955
1926
|
return Result.Error;
|
|
1956
1927
|
}
|
|
1957
1928
|
|
|
1958
|
-
volatiles.
|
|
1929
|
+
volatiles.disable_move =
|
|
1959
1930
|
@intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
|
|
1960
|
-
volatiles.
|
|
1931
|
+
volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
|
|
1932
|
+
options.chance.observe(.disable, player.foe(), .started);
|
|
1961
1933
|
|
|
1962
|
-
const id = foe.active.move(volatiles.
|
|
1963
|
-
try options.log.
|
|
1934
|
+
const id = foe.active.move(volatiles.disable_move).id;
|
|
1935
|
+
try options.log.start(.{ foe_ident, .Disable, id });
|
|
1964
1936
|
return null;
|
|
1965
1937
|
}
|
|
1966
1938
|
|
|
1967
1939
|
fn drainHP(battle: anytype, player: Player, options: anytype) !void {
|
|
1968
|
-
var
|
|
1969
|
-
|
|
1940
|
+
var stored = battle.side(player).stored();
|
|
1941
|
+
const ident = battle.active(player);
|
|
1970
1942
|
|
|
1971
1943
|
if (battle.last_damage == 0) {
|
|
1972
1944
|
assert(showdown);
|
|
@@ -1979,7 +1951,7 @@ pub const Effects = struct {
|
|
|
1979
1951
|
if (stored.hp == stored.stats.hp) return;
|
|
1980
1952
|
stored.hp = @min(stored.stats.hp, stored.hp + drain);
|
|
1981
1953
|
|
|
1982
|
-
try options.log.
|
|
1954
|
+
try options.log.heal(.{ ident, stored, .Drain, battle.active(player.foe()) });
|
|
1983
1955
|
}
|
|
1984
1956
|
|
|
1985
1957
|
fn explode(battle: anytype, player: Player) !void {
|
|
@@ -2011,7 +1983,7 @@ pub const Effects = struct {
|
|
|
2011
1983
|
if (side.active.volatiles.FocusEnergy) return;
|
|
2012
1984
|
side.active.volatiles.FocusEnergy = true;
|
|
2013
1985
|
|
|
2014
|
-
try options.log.start(battle.active(player), .FocusEnergy);
|
|
1986
|
+
try options.log.start(.{ battle.active(player), .FocusEnergy });
|
|
2015
1987
|
}
|
|
2016
1988
|
|
|
2017
1989
|
fn freezeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2033,7 +2005,7 @@ pub const Effects = struct {
|
|
|
2033
2005
|
foe_stored.status = Status.init(.FRZ);
|
|
2034
2006
|
// GLITCH: Hyper Beam recharging status is not cleared
|
|
2035
2007
|
|
|
2036
|
-
try options.log.status(foe_ident, foe_stored.status, .None);
|
|
2008
|
+
try options.log.status(.{ foe_ident, foe_stored.status, .None });
|
|
2037
2009
|
}
|
|
2038
2010
|
|
|
2039
2011
|
fn haze(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -2041,7 +2013,7 @@ pub const Effects = struct {
|
|
|
2041
2013
|
var side = battle.side(player);
|
|
2042
2014
|
var foe = battle.foe(player);
|
|
2043
2015
|
|
|
2044
|
-
|
|
2016
|
+
const side_stored = side.stored();
|
|
2045
2017
|
var foe_stored = foe.stored();
|
|
2046
2018
|
|
|
2047
2019
|
const player_ident = battle.active(player);
|
|
@@ -2053,8 +2025,8 @@ pub const Effects = struct {
|
|
|
2053
2025
|
side.active.stats = side_stored.stats;
|
|
2054
2026
|
foe.active.stats = foe_stored.stats;
|
|
2055
2027
|
|
|
2056
|
-
try log.activate(player_ident, .Haze);
|
|
2057
|
-
try log.clearallboost();
|
|
2028
|
+
try log.activate(.{ player_ident, .Haze });
|
|
2029
|
+
try log.clearallboost(.{});
|
|
2058
2030
|
|
|
2059
2031
|
// Pokémon Showdown clears P1 then P2 instead of status -> side -> foe
|
|
2060
2032
|
if (showdown) {
|
|
@@ -2062,24 +2034,26 @@ pub const Effects = struct {
|
|
|
2062
2034
|
const p: Player = @enumFromInt(i);
|
|
2063
2035
|
// Pokémon Showdown incorrectly does not prevent sleep/freeze from moving
|
|
2064
2036
|
if (p != player and Status.any(s.stored().status)) {
|
|
2065
|
-
try log.curestatus(foe_ident, foe_stored.status, .Silent);
|
|
2037
|
+
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
2066
2038
|
s.stored().status = 0;
|
|
2039
|
+
options.chance.observe(.sleep, p, .None);
|
|
2067
2040
|
} else if (showdown and s.stored().status == Status.TOX) {
|
|
2068
2041
|
s.stored().status = Status.init(.PSN);
|
|
2069
|
-
try log.status(battle.active(p), s.stored().status, .None);
|
|
2042
|
+
try log.status(.{ battle.active(p), s.stored().status, .None });
|
|
2070
2043
|
}
|
|
2071
|
-
try clearVolatiles(battle, p,
|
|
2044
|
+
try clearVolatiles(battle, p, options);
|
|
2072
2045
|
}
|
|
2073
2046
|
} else {
|
|
2074
2047
|
if (Status.any(foe_stored.status)) {
|
|
2075
2048
|
if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
|
|
2076
2049
|
foe.last_selected_move = .SKIP_TURN;
|
|
2050
|
+
options.chance.observe(.sleep, player.foe(), .None);
|
|
2077
2051
|
}
|
|
2078
|
-
try log.curestatus(foe_ident, foe_stored.status, .Silent);
|
|
2052
|
+
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
2079
2053
|
foe_stored.status = 0;
|
|
2080
2054
|
}
|
|
2081
|
-
try clearVolatiles(battle, player,
|
|
2082
|
-
try clearVolatiles(battle, player.foe(),
|
|
2055
|
+
try clearVolatiles(battle, player, options);
|
|
2056
|
+
try clearVolatiles(battle, player.foe(), options);
|
|
2083
2057
|
}
|
|
2084
2058
|
}
|
|
2085
2059
|
|
|
@@ -2091,7 +2065,7 @@ pub const Effects = struct {
|
|
|
2091
2065
|
// GLITCH: HP recovery move failure glitches
|
|
2092
2066
|
const delta = stored.stats.hp - stored.hp;
|
|
2093
2067
|
if (delta == 0 or (delta & 255 == 255 and stored.hp % 256 != 0)) {
|
|
2094
|
-
return
|
|
2068
|
+
return options.log.fail(.{ ident, .None });
|
|
2095
2069
|
}
|
|
2096
2070
|
|
|
2097
2071
|
const rest = side.last_selected_move == .Rest;
|
|
@@ -2099,17 +2073,17 @@ pub const Effects = struct {
|
|
|
2099
2073
|
// Adding the sleep status runs the sleep condition handler to roll duration
|
|
2100
2074
|
if (showdown) battle.rng.advance(1);
|
|
2101
2075
|
stored.status = Status.slf(2);
|
|
2102
|
-
try options.log.
|
|
2076
|
+
try options.log.status(.{ ident, stored.status, .From, Move.Rest });
|
|
2103
2077
|
stored.hp = stored.stats.hp;
|
|
2104
2078
|
} else {
|
|
2105
2079
|
stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
|
|
2106
2080
|
}
|
|
2107
|
-
try options.log.heal(ident, stored, if (rest) Heal.Silent else Heal.None);
|
|
2081
|
+
try options.log.heal(.{ ident, stored, if (rest) Heal.Silent else Heal.None });
|
|
2108
2082
|
}
|
|
2109
2083
|
|
|
2110
2084
|
fn hyperBeam(battle: anytype, player: Player, options: anytype) !void {
|
|
2111
2085
|
battle.side(player).active.volatiles.Recharging = true;
|
|
2112
|
-
try options.log.mustrecharge(battle.active(player));
|
|
2086
|
+
try options.log.mustrecharge(.{battle.active(player)});
|
|
2113
2087
|
}
|
|
2114
2088
|
|
|
2115
2089
|
fn leechSeed(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2118,33 +2092,31 @@ pub const Effects = struct {
|
|
|
2118
2092
|
if (showdown) {
|
|
2119
2093
|
// Invulnerability trumps type immunity on Pokémon Showdown
|
|
2120
2094
|
if (!foe.active.volatiles.Invulnerable and foe.active.types.includes(.Grass)) {
|
|
2121
|
-
return options.log.immune(battle.active(player.foe()), .None);
|
|
2095
|
+
return options.log.immune(.{ battle.active(player.foe()), .None });
|
|
2122
2096
|
}
|
|
2123
2097
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2124
2098
|
if (foe.active.volatiles.LeechSeed) return;
|
|
2125
2099
|
} else {
|
|
2126
2100
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2127
2101
|
if (foe.active.types.includes(.Grass) or foe.active.volatiles.LeechSeed) {
|
|
2128
|
-
try options.log.lastmiss();
|
|
2129
|
-
return options.log.miss(battle.active(player));
|
|
2102
|
+
try options.log.lastmiss(.{});
|
|
2103
|
+
return options.log.miss(.{battle.active(player)});
|
|
2130
2104
|
}
|
|
2131
2105
|
}
|
|
2132
2106
|
|
|
2133
|
-
try options.chance.commit(player, .hit);
|
|
2134
2107
|
foe.active.volatiles.LeechSeed = true;
|
|
2135
2108
|
|
|
2136
|
-
try options.log.start(battle.active(player.foe()), .LeechSeed);
|
|
2109
|
+
try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
|
|
2137
2110
|
}
|
|
2138
2111
|
|
|
2139
2112
|
fn lightScreen(battle: anytype, player: Player, options: anytype) !void {
|
|
2140
2113
|
var side = battle.side(player);
|
|
2114
|
+
const ident = battle.active(player);
|
|
2141
2115
|
|
|
2142
|
-
if (side.active.volatiles.LightScreen) {
|
|
2143
|
-
return options.log.fail(battle.active(player), .None);
|
|
2144
|
-
}
|
|
2116
|
+
if (side.active.volatiles.LightScreen) return options.log.fail(.{ ident, .None });
|
|
2145
2117
|
side.active.volatiles.LightScreen = true;
|
|
2146
2118
|
|
|
2147
|
-
try options.log.start(
|
|
2119
|
+
try options.log.start(.{ ident, .LightScreen });
|
|
2148
2120
|
}
|
|
2149
2121
|
|
|
2150
2122
|
fn mimic(battle: anytype, player: Player, move: Move.Data, mslot: u8, options: anytype) !void {
|
|
@@ -2172,17 +2144,15 @@ pub const Effects = struct {
|
|
|
2172
2144
|
if (!has_mimic) {
|
|
2173
2145
|
// Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
|
|
2174
2146
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2175
|
-
|
|
2176
|
-
return try options.log.fail(battle.active(player.foe()), .None);
|
|
2147
|
+
return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
2177
2148
|
}
|
|
2178
2149
|
}
|
|
2179
2150
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2180
|
-
try options.chance.commit(player, .hit);
|
|
2181
2151
|
|
|
2182
2152
|
const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
|
|
2183
2153
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
2184
2154
|
|
|
2185
|
-
try options.log.
|
|
2155
|
+
try options.log.start(.{ battle.active(player), .Mimic, side.active.move(oslot).id });
|
|
2186
2156
|
}
|
|
2187
2157
|
|
|
2188
2158
|
fn mist(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -2191,7 +2161,7 @@ pub const Effects = struct {
|
|
|
2191
2161
|
if (side.active.volatiles.Mist) return;
|
|
2192
2162
|
side.active.volatiles.Mist = true;
|
|
2193
2163
|
|
|
2194
|
-
try options.log.start(battle.active(player), .Mist);
|
|
2164
|
+
try options.log.start(.{ battle.active(player), .Mist });
|
|
2195
2165
|
}
|
|
2196
2166
|
|
|
2197
2167
|
fn multiHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2217,25 +2187,25 @@ pub const Effects = struct {
|
|
|
2217
2187
|
|
|
2218
2188
|
if (showdown) {
|
|
2219
2189
|
// Invulnerability trumps type immunity on Pokémon Showdown
|
|
2220
|
-
if (immune and !foe.active.volatiles.Invulnerable)
|
|
2221
|
-
|
|
2190
|
+
if (immune and !foe.active.volatiles.Invulnerable) {
|
|
2191
|
+
return log.immune(.{ foe_ident, .None });
|
|
2192
|
+
} else if (!try checkHit(battle, player, move, options)) return;
|
|
2222
2193
|
}
|
|
2223
2194
|
if (Status.any(foe_stored.status)) {
|
|
2224
|
-
return log.fail(
|
|
2195
|
+
return log.fail(.{
|
|
2225
2196
|
foe_ident,
|
|
2226
|
-
if (Status.is(foe_stored.status, .PAR)) .Paralysis else .None,
|
|
2227
|
-
);
|
|
2197
|
+
if (Status.is(foe_stored.status, .PAR)) Fail.Paralysis else Fail.None,
|
|
2198
|
+
});
|
|
2228
2199
|
}
|
|
2229
2200
|
if (!showdown) {
|
|
2230
|
-
if (immune) return log.immune(foe_ident, .None);
|
|
2201
|
+
if (immune) return log.immune(.{ foe_ident, .None });
|
|
2231
2202
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2232
2203
|
}
|
|
2233
|
-
try options.chance.commit(player, .hit);
|
|
2234
2204
|
|
|
2235
2205
|
foe_stored.status = Status.init(.PAR);
|
|
2236
2206
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
2237
2207
|
|
|
2238
|
-
try log.status(foe_ident, foe_stored.status, .None);
|
|
2208
|
+
try log.status(.{ foe_ident, foe_stored.status, .None });
|
|
2239
2209
|
}
|
|
2240
2210
|
|
|
2241
2211
|
fn paralyzeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2257,12 +2227,12 @@ pub const Effects = struct {
|
|
|
2257
2227
|
foe_stored.status = Status.init(.PAR);
|
|
2258
2228
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
2259
2229
|
|
|
2260
|
-
try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
|
|
2230
|
+
try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
|
|
2261
2231
|
}
|
|
2262
2232
|
|
|
2263
2233
|
fn payDay(battle: anytype, player: Player, options: anytype) !void {
|
|
2264
2234
|
if (!showdown or !battle.foe(player).active.volatiles.Substitute) {
|
|
2265
|
-
try options.log.fieldactivate();
|
|
2235
|
+
try options.log.fieldactivate(.{});
|
|
2266
2236
|
}
|
|
2267
2237
|
}
|
|
2268
2238
|
|
|
@@ -2277,28 +2247,27 @@ pub const Effects = struct {
|
|
|
2277
2247
|
return;
|
|
2278
2248
|
} else if (foe.active.volatiles.Substitute) {
|
|
2279
2249
|
if (move.effect != .Poison) return;
|
|
2280
|
-
return log.fail(foe_ident, .None);
|
|
2250
|
+
return log.fail(.{ foe_ident, .None });
|
|
2281
2251
|
} else if (Status.any(foe_stored.status)) {
|
|
2282
2252
|
if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
|
|
2283
2253
|
// Pokémon Showdown considers Toxic to be a status even in Generation I and so
|
|
2284
2254
|
// 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))
|
|
2255
|
+
return log.fail(.{ foe_ident, if (Status.is(foe_stored.status, .PSN))
|
|
2286
2256
|
if (!showdown)
|
|
2287
|
-
.Poison
|
|
2257
|
+
Fail.Poison
|
|
2288
2258
|
else if (toxic == (foe_stored.status == Status.TOX))
|
|
2289
|
-
if (toxic) .Toxic else .Poison
|
|
2259
|
+
if (toxic) Fail.Toxic else Fail.Poison
|
|
2290
2260
|
else
|
|
2291
|
-
.None
|
|
2261
|
+
Fail.None
|
|
2292
2262
|
else
|
|
2293
|
-
.None);
|
|
2263
|
+
Fail.None });
|
|
2294
2264
|
} else if (foe.active.types.includes(.Poison)) {
|
|
2295
2265
|
if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
|
|
2296
|
-
return log.immune(foe_ident, .None);
|
|
2266
|
+
return log.immune(.{ foe_ident, .None });
|
|
2297
2267
|
}
|
|
2298
2268
|
|
|
2299
2269
|
if (move.effect == .Poison) {
|
|
2300
2270
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2301
|
-
try options.chance.commit(player, .hit);
|
|
2302
2271
|
} else {
|
|
2303
2272
|
if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
|
|
2304
2273
|
return;
|
|
@@ -2312,7 +2281,7 @@ pub const Effects = struct {
|
|
|
2312
2281
|
foe.active.volatiles.toxic = 0;
|
|
2313
2282
|
}
|
|
2314
2283
|
|
|
2315
|
-
try log.status(foe_ident, foe_stored.status, .None);
|
|
2284
|
+
try log.status(.{ foe_ident, foe_stored.status, .None });
|
|
2316
2285
|
}
|
|
2317
2286
|
|
|
2318
2287
|
fn rage(battle: anytype, player: Player) !void {
|
|
@@ -2332,22 +2301,23 @@ pub const Effects = struct {
|
|
|
2332
2301
|
@as(u8, if (side.last_selected_move == .Struggle) 2 else 4), 1));
|
|
2333
2302
|
stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
|
|
2334
2303
|
|
|
2335
|
-
try options.log.
|
|
2304
|
+
try options.log.damage(.{
|
|
2336
2305
|
battle.active(player),
|
|
2337
2306
|
stored,
|
|
2338
2307
|
.RecoilOf,
|
|
2339
2308
|
battle.active(player.foe()),
|
|
2340
|
-
);
|
|
2309
|
+
});
|
|
2341
2310
|
if (showdown and stored.hp == 0) residual.* = false;
|
|
2342
2311
|
}
|
|
2343
2312
|
|
|
2344
2313
|
fn reflect(battle: anytype, player: Player, options: anytype) !void {
|
|
2345
2314
|
var side = battle.side(player);
|
|
2315
|
+
const ident = battle.active(player);
|
|
2346
2316
|
|
|
2347
|
-
if (side.active.volatiles.Reflect) return options.log.fail(
|
|
2317
|
+
if (side.active.volatiles.Reflect) return options.log.fail(.{ ident, .None });
|
|
2348
2318
|
side.active.volatiles.Reflect = true;
|
|
2349
2319
|
|
|
2350
|
-
try options.log.start(
|
|
2320
|
+
try options.log.start(.{ ident, .Reflect });
|
|
2351
2321
|
}
|
|
2352
2322
|
|
|
2353
2323
|
fn sleep(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2361,14 +2331,13 @@ pub const Effects = struct {
|
|
|
2361
2331
|
// the Sleep Clause Mod might activate, causing us to not actually bypass
|
|
2362
2332
|
} else {
|
|
2363
2333
|
if (Status.any(foe_stored.status)) {
|
|
2364
|
-
return options.log.fail(
|
|
2334
|
+
return options.log.fail(.{
|
|
2365
2335
|
foe_ident,
|
|
2366
|
-
if (Status.is(foe_stored.status, .SLP)) .Sleep else .None,
|
|
2367
|
-
);
|
|
2336
|
+
if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
|
|
2337
|
+
});
|
|
2368
2338
|
}
|
|
2369
|
-
// If
|
|
2339
|
+
// If moveHit in doMove didn't return true Pokémon Showdown wouldn't be in here
|
|
2370
2340
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2371
|
-
try options.chance.commit(player, .hit);
|
|
2372
2341
|
}
|
|
2373
2342
|
|
|
2374
2343
|
// Sleep Clause Mod
|
|
@@ -2380,19 +2349,20 @@ pub const Effects = struct {
|
|
|
2380
2349
|
foe.active.volatiles.Recharging = false;
|
|
2381
2350
|
|
|
2382
2351
|
foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
|
|
2352
|
+
options.chance.observe(.sleep, player.foe(), .started);
|
|
2353
|
+
|
|
2383
2354
|
const last = battle.side(player).last_selected_move;
|
|
2384
|
-
try options.log.
|
|
2355
|
+
try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
|
|
2385
2356
|
}
|
|
2386
2357
|
|
|
2387
2358
|
fn splash(battle: anytype, player: Player, options: anytype) !void {
|
|
2388
|
-
try options.log.activate(battle.active(player), .Splash);
|
|
2359
|
+
try options.log.activate(.{ battle.active(player), .Splash });
|
|
2389
2360
|
}
|
|
2390
2361
|
|
|
2391
2362
|
fn substitute(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
|
|
2392
2363
|
var side = battle.side(player);
|
|
2393
2364
|
if (side.active.volatiles.Substitute) {
|
|
2394
|
-
|
|
2395
|
-
return;
|
|
2365
|
+
return options.log.fail(.{ battle.active(player), .Substitute });
|
|
2396
2366
|
}
|
|
2397
2367
|
|
|
2398
2368
|
assert(side.stored().stats.hp <= 1023);
|
|
@@ -2404,28 +2374,30 @@ pub const Effects = struct {
|
|
|
2404
2374
|
// avoid having to convert to floating point)
|
|
2405
2375
|
const required_hp = if (showdown) @divFloor(side.stored().stats.hp - 1, 4) + 1 else hp;
|
|
2406
2376
|
if (side.stored().hp < required_hp) {
|
|
2407
|
-
|
|
2408
|
-
return;
|
|
2377
|
+
return options.log.fail(.{ battle.active(player), .Weak });
|
|
2409
2378
|
}
|
|
2410
2379
|
|
|
2411
2380
|
// GLITCH: can leave the user with 0 HP (faints later) because didn't check '<=' above
|
|
2412
2381
|
side.stored().hp -= hp;
|
|
2413
2382
|
side.active.volatiles.substitute = hp + 1;
|
|
2414
2383
|
side.active.volatiles.Substitute = true;
|
|
2415
|
-
try options.log.start(battle.active(player), .Substitute);
|
|
2384
|
+
try options.log.start(.{ battle.active(player), .Substitute });
|
|
2416
2385
|
if (hp > 0) {
|
|
2417
|
-
try options.log.damage(battle.active(player), side.stored(), .None);
|
|
2386
|
+
try options.log.damage(.{ battle.active(player), side.stored(), .None });
|
|
2418
2387
|
if (showdown and side.stored().hp == 0) residual.* = false;
|
|
2419
2388
|
}
|
|
2420
2389
|
}
|
|
2421
2390
|
|
|
2422
2391
|
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
2392
|
// Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
|
|
2426
|
-
if (
|
|
2427
|
-
|
|
2428
|
-
|
|
2393
|
+
if (showdown) {
|
|
2394
|
+
if (battle.side(player).last_selected_move == .Teleport) return;
|
|
2395
|
+
_ = try checkHit(battle, player, move, options);
|
|
2396
|
+
battle.last_damage = 0;
|
|
2397
|
+
} else {
|
|
2398
|
+
try options.log.fail(.{ battle.active(player), .None });
|
|
2399
|
+
try options.log.laststill(.{});
|
|
2400
|
+
}
|
|
2429
2401
|
}
|
|
2430
2402
|
|
|
2431
2403
|
fn thrashing(battle: anytype, player: Player, options: anytype) void {
|
|
@@ -2435,6 +2407,8 @@ pub const Effects = struct {
|
|
|
2435
2407
|
|
|
2436
2408
|
volatiles.Thrashing = true;
|
|
2437
2409
|
volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
2410
|
+
|
|
2411
|
+
options.chance.observe(.attacking, player, .started);
|
|
2438
2412
|
}
|
|
2439
2413
|
|
|
2440
2414
|
fn transform(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -2463,7 +2437,7 @@ pub const Effects = struct {
|
|
|
2463
2437
|
side.active.moves[i].pp = if (m.id != .None) 5 else 0;
|
|
2464
2438
|
}
|
|
2465
2439
|
|
|
2466
|
-
try options.log.transform(battle.active(player), foe_ident);
|
|
2440
|
+
try options.log.transform(.{ battle.active(player), foe_ident });
|
|
2467
2441
|
}
|
|
2468
2442
|
|
|
2469
2443
|
fn binding(battle: anytype, player: Player, rewrap: bool, options: anytype) !void {
|
|
@@ -2480,6 +2454,7 @@ pub const Effects = struct {
|
|
|
2480
2454
|
|
|
2481
2455
|
side.active.volatiles.attacks =
|
|
2482
2456
|
try Rolls.distribution(battle, .Binding, player, options) - 1;
|
|
2457
|
+
options.chance.observe(.binding, player, .started);
|
|
2483
2458
|
}
|
|
2484
2459
|
|
|
2485
2460
|
fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2493,87 +2468,87 @@ pub const Effects = struct {
|
|
|
2493
2468
|
switch (move.effect) {
|
|
2494
2469
|
.AttackUp1, .AttackUp2, .Rage => {
|
|
2495
2470
|
assert(boosts.atk >= -6 and boosts.atk <= 6);
|
|
2496
|
-
if (boosts.atk == 6) return
|
|
2471
|
+
if (boosts.atk == 6) return log.fail(.{ ident, .None });
|
|
2497
2472
|
const n: u2 = if (move.effect == .AttackUp2) 2 else 1;
|
|
2498
2473
|
boosts.atk = @as(i4, @intCast(@min(6, @as(i8, boosts.atk) + n)));
|
|
2499
|
-
const reason = if (move.effect == .Rage)
|
|
2474
|
+
const reason: Boost = if (move.effect == .Rage) .Rage else .Attack;
|
|
2500
2475
|
if (stats.atk == MAX_STAT_VALUE) {
|
|
2501
2476
|
boosts.atk -= 1;
|
|
2502
2477
|
if (showdown) {
|
|
2503
|
-
try log.boost(ident, reason, n);
|
|
2504
|
-
try log.boost(ident,
|
|
2478
|
+
try log.boost(.{ ident, reason, n });
|
|
2479
|
+
try log.boost(.{ ident, .Attack, -1 });
|
|
2505
2480
|
if (move.effect == .Rage) return;
|
|
2506
2481
|
}
|
|
2507
|
-
return
|
|
2482
|
+
return log.fail(.{ ident, .None });
|
|
2508
2483
|
}
|
|
2509
|
-
|
|
2484
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
|
|
2510
2485
|
const stat = unmodifiedStats(battle, side).atk;
|
|
2511
2486
|
stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2512
|
-
try log.boost(ident, reason, n);
|
|
2487
|
+
try log.boost(.{ ident, reason, n });
|
|
2513
2488
|
// Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
|
|
2514
2489
|
if (showdown and move.effect == .Rage) return;
|
|
2515
2490
|
},
|
|
2516
2491
|
.DefenseUp1, .DefenseUp2 => {
|
|
2517
2492
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
2518
|
-
if (boosts.def == 6) return
|
|
2493
|
+
if (boosts.def == 6) return log.fail(.{ ident, .None });
|
|
2519
2494
|
const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
|
|
2520
2495
|
boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
|
|
2521
2496
|
if (stats.def == MAX_STAT_VALUE) {
|
|
2522
2497
|
boosts.def -= 1;
|
|
2523
2498
|
if (showdown) {
|
|
2524
|
-
try log.boost(ident, .Defense, n);
|
|
2525
|
-
try log.boost(ident, .Defense, -1);
|
|
2499
|
+
try log.boost(.{ ident, .Defense, n });
|
|
2500
|
+
try log.boost(.{ ident, .Defense, -1 });
|
|
2526
2501
|
}
|
|
2527
|
-
return
|
|
2502
|
+
return log.fail(.{ ident, .None });
|
|
2528
2503
|
}
|
|
2529
|
-
|
|
2504
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
|
|
2530
2505
|
const stat = unmodifiedStats(battle, side).def;
|
|
2531
2506
|
stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2532
|
-
try log.boost(ident, .Defense, n);
|
|
2507
|
+
try log.boost(.{ ident, .Defense, n });
|
|
2533
2508
|
},
|
|
2534
2509
|
.SpeedUp2 => {
|
|
2535
2510
|
assert(boosts.spe >= -6 and boosts.spe <= 6);
|
|
2536
|
-
if (boosts.spe == 6) return
|
|
2511
|
+
if (boosts.spe == 6) return log.fail(.{ ident, .None });
|
|
2537
2512
|
boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
|
|
2538
2513
|
if (stats.spe == MAX_STAT_VALUE) {
|
|
2539
2514
|
boosts.spe -= 1;
|
|
2540
2515
|
if (showdown) {
|
|
2541
|
-
try log.boost(ident, .Speed, 2);
|
|
2542
|
-
try log.boost(ident, .Speed, -1);
|
|
2516
|
+
try log.boost(.{ ident, .Speed, 2 });
|
|
2517
|
+
try log.boost(.{ ident, .Speed, -1 });
|
|
2543
2518
|
}
|
|
2544
|
-
return
|
|
2519
|
+
return log.fail(.{ ident, .None });
|
|
2545
2520
|
}
|
|
2546
|
-
|
|
2521
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
|
|
2547
2522
|
const stat = unmodifiedStats(battle, side).spe;
|
|
2548
2523
|
stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2549
|
-
try log.boost(ident, .Speed, 2);
|
|
2524
|
+
try log.boost(.{ ident, .Speed, 2 });
|
|
2550
2525
|
},
|
|
2551
2526
|
.SpecialUp1, .SpecialUp2 => {
|
|
2552
2527
|
assert(boosts.spc >= -6 and boosts.spc <= 6);
|
|
2553
|
-
if (boosts.spc == 6) return
|
|
2528
|
+
if (boosts.spc == 6) return log.fail(.{ ident, .None });
|
|
2554
2529
|
const n: u2 = if (move.effect == .SpecialUp2) 2 else 1;
|
|
2555
2530
|
boosts.spc = @intCast(@min(6, @as(i8, boosts.spc) + n));
|
|
2556
2531
|
if (stats.spc == MAX_STAT_VALUE) {
|
|
2557
2532
|
boosts.spc -= 1;
|
|
2558
2533
|
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);
|
|
2534
|
+
try log.boost(.{ ident, .SpecialAttack, n });
|
|
2535
|
+
try log.boost(.{ ident, .SpecialAttack, -1 });
|
|
2536
|
+
try log.boost(.{ ident, .SpecialDefense, n });
|
|
2537
|
+
try log.boost(.{ ident, .SpecialDefense, -1 });
|
|
2563
2538
|
}
|
|
2564
|
-
return
|
|
2539
|
+
return log.fail(.{ ident, .None });
|
|
2565
2540
|
}
|
|
2566
|
-
|
|
2541
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
|
|
2567
2542
|
const stat = unmodifiedStats(battle, side).spc;
|
|
2568
2543
|
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);
|
|
2544
|
+
try log.boost(.{ ident, .SpecialAttack, n });
|
|
2545
|
+
try log.boost(.{ ident, .SpecialDefense, n });
|
|
2571
2546
|
},
|
|
2572
2547
|
.EvasionUp1 => {
|
|
2573
2548
|
assert(boosts.evasion >= -6 and boosts.evasion <= 6);
|
|
2574
|
-
if (boosts.evasion == 6) return
|
|
2549
|
+
if (boosts.evasion == 6) return log.fail(.{ ident, .None });
|
|
2575
2550
|
boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
|
|
2576
|
-
try log.boost(ident, .Evasion, 1);
|
|
2551
|
+
try log.boost(.{ ident, .Evasion, 1 });
|
|
2577
2552
|
},
|
|
2578
2553
|
else => unreachable,
|
|
2579
2554
|
}
|
|
@@ -2588,7 +2563,7 @@ pub const Effects = struct {
|
|
|
2588
2563
|
const foe_ident = battle.active(player.foe());
|
|
2589
2564
|
const secondary = move.effect.isStatDownChance();
|
|
2590
2565
|
|
|
2591
|
-
if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(foe_ident, .None);
|
|
2566
|
+
if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(.{ foe_ident, .None });
|
|
2592
2567
|
|
|
2593
2568
|
if (secondary) {
|
|
2594
2569
|
const proc = try Rolls.unboost(battle, player, options);
|
|
@@ -2596,7 +2571,6 @@ pub const Effects = struct {
|
|
|
2596
2571
|
} else {
|
|
2597
2572
|
// checkHit already checks for Invulnerable
|
|
2598
2573
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2599
|
-
try options.chance.commit(player, .hit);
|
|
2600
2574
|
}
|
|
2601
2575
|
|
|
2602
2576
|
var stats = &foe.active.stats;
|
|
@@ -2606,82 +2580,82 @@ pub const Effects = struct {
|
|
|
2606
2580
|
switch (move.effect) {
|
|
2607
2581
|
.AttackDown1, .AttackDownChance => {
|
|
2608
2582
|
assert(boosts.atk >= -6 and boosts.atk <= 6);
|
|
2609
|
-
if (boosts.atk == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2583
|
+
if (boosts.atk == -6) return if (fail) try log.fail(.{ foe_ident, .None });
|
|
2610
2584
|
boosts.atk = @intCast(@max(-6, @as(i8, boosts.atk) - 1));
|
|
2611
2585
|
if (stats.atk == 1) {
|
|
2612
2586
|
boosts.atk += 1;
|
|
2613
2587
|
if (showdown) {
|
|
2614
|
-
try log.boost(foe_ident, .Attack, -1);
|
|
2615
|
-
try log.boost(foe_ident, .Attack, 1);
|
|
2588
|
+
try log.boost(.{ foe_ident, .Attack, -1 });
|
|
2589
|
+
try log.boost(.{ foe_ident, .Attack, 1 });
|
|
2616
2590
|
}
|
|
2617
|
-
return
|
|
2591
|
+
return log.fail(.{ foe_ident, .None });
|
|
2618
2592
|
}
|
|
2619
|
-
|
|
2593
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
|
|
2620
2594
|
const stat = unmodifiedStats(battle, foe).atk;
|
|
2621
2595
|
stats.atk = @max(1, stat * mod[0] / mod[1]);
|
|
2622
|
-
try log.boost(foe_ident, .Attack, -1);
|
|
2596
|
+
try log.boost(.{ foe_ident, .Attack, -1 });
|
|
2623
2597
|
},
|
|
2624
2598
|
.DefenseDown1, .DefenseDown2, .DefenseDownChance => {
|
|
2625
2599
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
2626
|
-
if (boosts.def == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2600
|
+
if (boosts.def == -6) return if (fail) try log.fail(.{ foe_ident, .None });
|
|
2627
2601
|
const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
|
|
2628
2602
|
boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
|
|
2629
2603
|
if (stats.def == 1) {
|
|
2630
2604
|
boosts.def += 1;
|
|
2631
2605
|
if (showdown) {
|
|
2632
|
-
try log.boost(foe_ident, .Defense, -@as(i8, n));
|
|
2633
|
-
try log.boost(foe_ident, .Defense, 1);
|
|
2606
|
+
try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
|
|
2607
|
+
try log.boost(.{ foe_ident, .Defense, 1 });
|
|
2634
2608
|
}
|
|
2635
|
-
return
|
|
2609
|
+
return log.fail(.{ foe_ident, .None });
|
|
2636
2610
|
}
|
|
2637
|
-
|
|
2611
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
|
|
2638
2612
|
const stat = unmodifiedStats(battle, foe).def;
|
|
2639
2613
|
stats.def = @max(1, stat * mod[0] / mod[1]);
|
|
2640
|
-
try log.boost(foe_ident, .Defense, -@as(i8, n));
|
|
2614
|
+
try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
|
|
2641
2615
|
},
|
|
2642
2616
|
.SpeedDown1, .SpeedDownChance => {
|
|
2643
2617
|
assert(boosts.spe >= -6 and boosts.spe <= 6);
|
|
2644
|
-
if (boosts.spe == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2618
|
+
if (boosts.spe == -6) return if (fail) try log.fail(.{ foe_ident, .None });
|
|
2645
2619
|
boosts.spe = @intCast(@max(-6, @as(i8, boosts.spe) - 1));
|
|
2646
2620
|
if (stats.spe == 1) {
|
|
2647
2621
|
boosts.spe += 1;
|
|
2648
2622
|
if (showdown) {
|
|
2649
|
-
try log.boost(foe_ident, .Speed, -1);
|
|
2650
|
-
try log.boost(foe_ident, .Speed, 1);
|
|
2623
|
+
try log.boost(.{ foe_ident, .Speed, -1 });
|
|
2624
|
+
try log.boost(.{ foe_ident, .Speed, 1 });
|
|
2651
2625
|
}
|
|
2652
|
-
return
|
|
2626
|
+
return log.fail(.{ foe_ident, .None });
|
|
2653
2627
|
}
|
|
2654
|
-
|
|
2628
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
|
|
2655
2629
|
const stat = unmodifiedStats(battle, foe).spe;
|
|
2656
2630
|
stats.spe = @max(1, stat * mod[0] / mod[1]);
|
|
2657
|
-
try log.boost(foe_ident, .Speed, -1);
|
|
2631
|
+
try log.boost(.{ foe_ident, .Speed, -1 });
|
|
2658
2632
|
assert(boosts.spe >= -6);
|
|
2659
2633
|
},
|
|
2660
2634
|
.SpecialDownChance => {
|
|
2661
2635
|
assert(boosts.spc >= -6 and boosts.spc <= 6);
|
|
2662
|
-
if (boosts.spc == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2636
|
+
if (boosts.spc == -6) return if (fail) try log.fail(.{ foe_ident, .None });
|
|
2663
2637
|
boosts.spc = @intCast(@max(-6, @as(i8, boosts.spc) - 1));
|
|
2664
2638
|
if (stats.spc == 1) {
|
|
2665
2639
|
boosts.spc += 1;
|
|
2666
2640
|
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);
|
|
2641
|
+
try log.boost(.{ foe_ident, .SpecialAttack, -1 });
|
|
2642
|
+
try log.boost(.{ foe_ident, .SpecialAttack, 1 });
|
|
2643
|
+
try log.boost(.{ foe_ident, .SpecialDefense, -1 });
|
|
2644
|
+
try log.boost(.{ foe_ident, .SpecialDefense, 1 });
|
|
2671
2645
|
}
|
|
2672
|
-
return
|
|
2646
|
+
return log.fail(.{ foe_ident, .None });
|
|
2673
2647
|
}
|
|
2674
|
-
|
|
2648
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
|
|
2675
2649
|
const stat = unmodifiedStats(battle, foe).spc;
|
|
2676
2650
|
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);
|
|
2651
|
+
try log.boost(.{ foe_ident, .SpecialAttack, -1 });
|
|
2652
|
+
try log.boost(.{ foe_ident, .SpecialDefense, -1 });
|
|
2679
2653
|
},
|
|
2680
2654
|
.AccuracyDown1 => {
|
|
2681
2655
|
assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
|
|
2682
|
-
if (boosts.accuracy == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2656
|
+
if (boosts.accuracy == -6) return if (fail) try log.fail(.{ foe_ident, .None });
|
|
2683
2657
|
boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
|
|
2684
|
-
try log.boost(foe_ident, .Accuracy, -1);
|
|
2658
|
+
try log.boost(.{ foe_ident, .Accuracy, -1 });
|
|
2685
2659
|
},
|
|
2686
2660
|
else => unreachable,
|
|
2687
2661
|
}
|
|
@@ -2705,40 +2679,40 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
|
|
|
2705
2679
|
}
|
|
2706
2680
|
}
|
|
2707
2681
|
|
|
2708
|
-
|
|
2682
|
+
fn isForced(active: anytype) bool {
|
|
2709
2683
|
return active.volatiles.Recharging or active.volatiles.Rage or
|
|
2710
2684
|
active.volatiles.Thrashing or active.volatiles.Charging;
|
|
2711
2685
|
}
|
|
2712
2686
|
|
|
2713
|
-
fn clearVolatiles(battle: anytype, who: Player,
|
|
2687
|
+
fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
|
|
2714
2688
|
var log = options.log;
|
|
2715
2689
|
var side = battle.side(who);
|
|
2716
2690
|
var volatiles = &side.active.volatiles;
|
|
2717
2691
|
const ident = battle.active(who);
|
|
2718
2692
|
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
try log.end(ident, .DisableSilent);
|
|
2693
|
+
if (volatiles.disable_move != 0) {
|
|
2694
|
+
volatiles.disable_move = 0;
|
|
2695
|
+
volatiles.disable_duration = 0;
|
|
2696
|
+
options.chance.observe(.disable, who, .None);
|
|
2697
|
+
try log.end(.{ ident, .DisableSilent });
|
|
2725
2698
|
}
|
|
2726
2699
|
if (volatiles.Confusion) {
|
|
2727
2700
|
// volatiles.confusion is left unchanged
|
|
2728
2701
|
volatiles.Confusion = false;
|
|
2729
|
-
|
|
2702
|
+
options.chance.observe(.confusion, who, .None);
|
|
2703
|
+
try log.end(.{ ident, .ConfusionSilent });
|
|
2730
2704
|
}
|
|
2731
2705
|
if (volatiles.Mist) {
|
|
2732
2706
|
volatiles.Mist = false;
|
|
2733
|
-
try log.end(ident, .
|
|
2707
|
+
try log.end(.{ ident, .MistSilent });
|
|
2734
2708
|
}
|
|
2735
2709
|
if (volatiles.FocusEnergy) {
|
|
2736
2710
|
volatiles.FocusEnergy = false;
|
|
2737
|
-
try log.end(ident, .
|
|
2711
|
+
try log.end(.{ ident, .FocusEnergySilent });
|
|
2738
2712
|
}
|
|
2739
2713
|
if (volatiles.LeechSeed) {
|
|
2740
2714
|
volatiles.LeechSeed = false;
|
|
2741
|
-
try log.end(ident, .
|
|
2715
|
+
try log.end(.{ ident, .LeechSeedSilent });
|
|
2742
2716
|
}
|
|
2743
2717
|
if (!showdown and volatiles.Toxic) {
|
|
2744
2718
|
volatiles.Toxic = false;
|
|
@@ -2746,24 +2720,39 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
|
|
|
2746
2720
|
}
|
|
2747
2721
|
if (volatiles.LightScreen) {
|
|
2748
2722
|
volatiles.LightScreen = false;
|
|
2749
|
-
try log.end(ident, .
|
|
2723
|
+
try log.end(.{ ident, .LightScreenSilent });
|
|
2750
2724
|
}
|
|
2751
2725
|
if (volatiles.Reflect) {
|
|
2752
2726
|
volatiles.Reflect = false;
|
|
2753
|
-
try log.end(ident, .
|
|
2727
|
+
try log.end(.{ ident, .ReflectSilent });
|
|
2754
2728
|
}
|
|
2755
2729
|
if (showdown and volatiles.Toxic) {
|
|
2756
2730
|
volatiles.Toxic = false;
|
|
2757
2731
|
// Pokémon Showdown erroneously clears the toxic counter
|
|
2758
2732
|
volatiles.toxic = 0;
|
|
2759
|
-
try log.end(ident, .
|
|
2733
|
+
try log.end(.{ ident, .ToxicSilent });
|
|
2760
2734
|
}
|
|
2761
2735
|
}
|
|
2762
2736
|
|
|
2737
|
+
fn decrement(
|
|
2738
|
+
comptime field: chance.Action.Field,
|
|
2739
|
+
player: Player,
|
|
2740
|
+
options: anytype,
|
|
2741
|
+
n: anytype,
|
|
2742
|
+
) @TypeOf(n) {
|
|
2743
|
+
return if (options.calc.overridden(player, field)) |obs| switch (obs) {
|
|
2744
|
+
.started, .ended => 0,
|
|
2745
|
+
.continuing => if (n > 1) n - 1 else n,
|
|
2746
|
+
else => unreachable,
|
|
2747
|
+
} else n - 1;
|
|
2748
|
+
}
|
|
2749
|
+
|
|
2763
2750
|
pub const Rolls = struct {
|
|
2764
|
-
|
|
2751
|
+
fn speedTie(battle: anytype, options: anytype) !bool {
|
|
2765
2752
|
const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
|
|
2766
2753
|
player == .P1
|
|
2754
|
+
else if (@hasDecl(@TypeOf(battle.rng), "speedTie"))
|
|
2755
|
+
battle.rng.speedTie()
|
|
2767
2756
|
else if (showdown)
|
|
2768
2757
|
battle.rng.range(u8, 0, 2) == 0
|
|
2769
2758
|
else
|
|
@@ -2773,21 +2762,31 @@ pub const Rolls = struct {
|
|
|
2773
2762
|
return p1;
|
|
2774
2763
|
}
|
|
2775
2764
|
|
|
2776
|
-
|
|
2765
|
+
fn criticalHit(
|
|
2766
|
+
battle: anytype,
|
|
2767
|
+
player: Player,
|
|
2768
|
+
rate: u8,
|
|
2769
|
+
spurious: bool,
|
|
2770
|
+
options: anytype,
|
|
2771
|
+
) !bool {
|
|
2777
2772
|
const crit = if (options.calc.overridden(player, .critical_hit)) |val|
|
|
2778
2773
|
val == .true
|
|
2774
|
+
else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
|
|
2775
|
+
battle.rng.criticalHit(player, rate)
|
|
2779
2776
|
else if (showdown)
|
|
2780
2777
|
battle.rng.chance(u8, rate, 256)
|
|
2781
2778
|
else
|
|
2782
2779
|
std.math.rotl(u8, battle.rng.next(), 3) < rate;
|
|
2783
2780
|
|
|
2784
|
-
try options.chance.criticalHit(player, crit, rate);
|
|
2781
|
+
if (!spurious) try options.chance.criticalHit(player, crit, rate);
|
|
2785
2782
|
return crit;
|
|
2786
2783
|
}
|
|
2787
2784
|
|
|
2788
|
-
|
|
2785
|
+
fn damage(battle: anytype, player: Player, options: anytype) !u8 {
|
|
2789
2786
|
const roll = if (options.calc.overridden(player, .damage)) |val|
|
|
2790
2787
|
val
|
|
2788
|
+
else if (@hasDecl(@TypeOf(battle.rng), "damage"))
|
|
2789
|
+
battle.rng.damage(player)
|
|
2791
2790
|
else roll: {
|
|
2792
2791
|
if (showdown) break :roll battle.rng.range(u8, 217, 256);
|
|
2793
2792
|
while (true) {
|
|
@@ -2796,25 +2795,32 @@ pub const Rolls = struct {
|
|
|
2796
2795
|
}
|
|
2797
2796
|
};
|
|
2798
2797
|
|
|
2798
|
+
assert(roll >= 217 and roll <= 255);
|
|
2799
2799
|
try options.chance.damage(player, roll);
|
|
2800
2800
|
return roll;
|
|
2801
2801
|
}
|
|
2802
2802
|
|
|
2803
|
-
|
|
2803
|
+
const uN = if (pkmn.options.miss) u8 else u9;
|
|
2804
|
+
fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) !bool {
|
|
2805
|
+
const acc: uN = if (pkmn.options.miss) accuracy else @as(uN, accuracy) + 1;
|
|
2804
2806
|
const ok = if (options.calc.overridden(player, .hit)) |val|
|
|
2805
2807
|
val == .true
|
|
2808
|
+
else if (@hasDecl(@TypeOf(battle.rng), "hit"))
|
|
2809
|
+
battle.rng.hit(player, acc)
|
|
2806
2810
|
else if (showdown)
|
|
2807
|
-
battle.rng.chance(
|
|
2811
|
+
battle.rng.chance(uN, acc, 256)
|
|
2808
2812
|
else
|
|
2809
|
-
battle.rng.next() <
|
|
2813
|
+
battle.rng.next() < acc;
|
|
2810
2814
|
|
|
2811
|
-
options.chance.hit(ok,
|
|
2815
|
+
try options.chance.hit(player, ok, acc);
|
|
2812
2816
|
return ok;
|
|
2813
2817
|
}
|
|
2814
2818
|
|
|
2815
|
-
|
|
2819
|
+
fn confused(battle: anytype, player: Player, options: anytype) !bool {
|
|
2816
2820
|
const cfz = if (options.calc.overridden(player, .confused)) |val|
|
|
2817
2821
|
val == .true
|
|
2822
|
+
else if (@hasDecl(@TypeOf(battle.rng), "confused"))
|
|
2823
|
+
battle.rng.confused(player)
|
|
2818
2824
|
else if (showdown)
|
|
2819
2825
|
!battle.rng.chance(u8, 128, 256)
|
|
2820
2826
|
else
|
|
@@ -2824,9 +2830,11 @@ pub const Rolls = struct {
|
|
|
2824
2830
|
return cfz;
|
|
2825
2831
|
}
|
|
2826
2832
|
|
|
2827
|
-
|
|
2833
|
+
fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
|
|
2828
2834
|
const par = if (options.calc.overridden(player, .paralyzed)) |val|
|
|
2829
2835
|
val == .true
|
|
2836
|
+
else if (@hasDecl(@TypeOf(battle.rng), "paralyzed"))
|
|
2837
|
+
battle.rng.paralyzed(player)
|
|
2830
2838
|
else if (showdown)
|
|
2831
2839
|
battle.rng.chance(u8, 63, 256)
|
|
2832
2840
|
else
|
|
@@ -2836,9 +2844,11 @@ pub const Rolls = struct {
|
|
|
2836
2844
|
return par;
|
|
2837
2845
|
}
|
|
2838
2846
|
|
|
2839
|
-
|
|
2847
|
+
fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
|
|
2840
2848
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2841
2849
|
val == .true
|
|
2850
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2851
|
+
battle.rng.secondaryChance(player, 25)
|
|
2842
2852
|
else if (showdown)
|
|
2843
2853
|
battle.rng.chance(u8, 25, 256)
|
|
2844
2854
|
else
|
|
@@ -2848,11 +2858,13 @@ pub const Rolls = struct {
|
|
|
2848
2858
|
return proc;
|
|
2849
2859
|
}
|
|
2850
2860
|
|
|
2851
|
-
|
|
2861
|
+
fn secondaryChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
|
|
2852
2862
|
const rate: u8 = if (low) 26 else 77;
|
|
2853
2863
|
|
|
2854
2864
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2855
2865
|
val == .true
|
|
2866
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2867
|
+
battle.rng.secondaryChance(player, rate)
|
|
2856
2868
|
else if (showdown)
|
|
2857
2869
|
battle.rng.chance(u8, rate, 256)
|
|
2858
2870
|
else
|
|
@@ -2862,11 +2874,13 @@ pub const Rolls = struct {
|
|
|
2862
2874
|
return proc;
|
|
2863
2875
|
}
|
|
2864
2876
|
|
|
2865
|
-
|
|
2877
|
+
fn poisonChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
|
|
2866
2878
|
const rate: u8 = if (low) 52 else 103;
|
|
2867
2879
|
|
|
2868
2880
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2869
2881
|
val == .true
|
|
2882
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2883
|
+
battle.rng.secondaryChance(player, rate)
|
|
2870
2884
|
else if (showdown)
|
|
2871
2885
|
battle.rng.chance(u8, rate, 256)
|
|
2872
2886
|
else
|
|
@@ -2876,9 +2890,11 @@ pub const Rolls = struct {
|
|
|
2876
2890
|
return proc;
|
|
2877
2891
|
}
|
|
2878
2892
|
|
|
2879
|
-
|
|
2893
|
+
fn unboost(battle: anytype, player: Player, options: anytype) !bool {
|
|
2880
2894
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2881
2895
|
val == .true
|
|
2896
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2897
|
+
battle.rng.secondaryChance(player, 85)
|
|
2882
2898
|
else if (showdown)
|
|
2883
2899
|
battle.rng.chance(u8, 85, 256)
|
|
2884
2900
|
else
|
|
@@ -2888,14 +2904,14 @@ pub const Rolls = struct {
|
|
|
2888
2904
|
return proc;
|
|
2889
2905
|
}
|
|
2890
2906
|
|
|
2891
|
-
|
|
2907
|
+
fn metronome(battle: anytype, player: Player, options: anytype) !Move {
|
|
2892
2908
|
const move: Move = if (options.calc.overridden(player, .metronome)) |val|
|
|
2893
2909
|
val
|
|
2894
|
-
else if (
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2910
|
+
else if (@hasDecl(@TypeOf(battle.rng), "metronome"))
|
|
2911
|
+
battle.rng.metronome(player)
|
|
2912
|
+
else if (showdown)
|
|
2913
|
+
Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
|
|
2914
|
+
else move: {
|
|
2899
2915
|
while (true) {
|
|
2900
2916
|
const r = battle.rng.next();
|
|
2901
2917
|
if (r == 0 or r == @intFromEnum(Move.Metronome)) continue;
|
|
@@ -2908,9 +2924,11 @@ pub const Rolls = struct {
|
|
|
2908
2924
|
return move;
|
|
2909
2925
|
}
|
|
2910
2926
|
|
|
2911
|
-
|
|
2927
|
+
fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
|
|
2912
2928
|
const power = if (options.calc.overridden(player, .psywave)) |val|
|
|
2913
2929
|
val - 1
|
|
2930
|
+
else if (@hasDecl(@TypeOf(battle.rng), "psywave"))
|
|
2931
|
+
battle.rng.psywave(player)
|
|
2914
2932
|
else if (showdown)
|
|
2915
2933
|
battle.rng.range(u8, 0, max)
|
|
2916
2934
|
else power: {
|
|
@@ -2920,15 +2938,18 @@ pub const Rolls = struct {
|
|
|
2920
2938
|
}
|
|
2921
2939
|
};
|
|
2922
2940
|
|
|
2941
|
+
assert(power < max);
|
|
2923
2942
|
try options.chance.psywave(player, power, max);
|
|
2924
2943
|
return power;
|
|
2925
2944
|
}
|
|
2926
2945
|
|
|
2927
|
-
|
|
2946
|
+
fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2928
2947
|
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2929
2948
|
@intCast(val)
|
|
2949
|
+
else if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
|
|
2950
|
+
battle.rng.sleepDuration(player)
|
|
2930
2951
|
else if (showdown)
|
|
2931
|
-
|
|
2952
|
+
battle.rng.range(u3, 1, 8)
|
|
2932
2953
|
else duration: {
|
|
2933
2954
|
while (true) {
|
|
2934
2955
|
const r = battle.rng.next() & 7;
|
|
@@ -2936,47 +2957,59 @@ pub const Rolls = struct {
|
|
|
2936
2957
|
}
|
|
2937
2958
|
};
|
|
2938
2959
|
|
|
2939
|
-
|
|
2960
|
+
assert(duration >= 1 and duration <= 7);
|
|
2961
|
+
options.chance.duration(player, duration);
|
|
2940
2962
|
return duration;
|
|
2941
2963
|
}
|
|
2942
2964
|
|
|
2943
|
-
|
|
2965
|
+
fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
|
|
2944
2966
|
const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
|
|
2945
2967
|
val
|
|
2968
|
+
else if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
|
|
2969
|
+
battle.rng.disableDuration(player)
|
|
2946
2970
|
else if (showdown)
|
|
2947
|
-
|
|
2971
|
+
battle.rng.range(u4, 1, 9)
|
|
2948
2972
|
else
|
|
2949
2973
|
@intCast((battle.rng.next() & 7) + 1);
|
|
2950
2974
|
|
|
2951
|
-
|
|
2975
|
+
assert(duration >= 1 and duration <= 8);
|
|
2976
|
+
options.chance.duration(player, duration);
|
|
2952
2977
|
return duration;
|
|
2953
2978
|
}
|
|
2954
2979
|
|
|
2955
|
-
|
|
2980
|
+
fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2956
2981
|
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2957
2982
|
@intCast(val)
|
|
2983
|
+
else if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
|
|
2984
|
+
battle.rng.confusionDuration(player)
|
|
2958
2985
|
else if (showdown)
|
|
2959
|
-
|
|
2986
|
+
battle.rng.range(u3, 2, 6)
|
|
2960
2987
|
else
|
|
2961
2988
|
@intCast((battle.rng.next() & 3) + 2);
|
|
2962
2989
|
|
|
2963
|
-
|
|
2990
|
+
assert(duration >= 2 and duration <= 5);
|
|
2991
|
+
options.chance.duration(player, duration);
|
|
2964
2992
|
return duration;
|
|
2965
2993
|
}
|
|
2966
2994
|
|
|
2967
|
-
|
|
2995
|
+
fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2968
2996
|
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2969
2997
|
@intCast(val)
|
|
2998
|
+
else if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
|
|
2999
|
+
battle.rng.attackingDuration(player)
|
|
2970
3000
|
else if (showdown)
|
|
2971
|
-
|
|
3001
|
+
battle.rng.range(u3, 2, 4)
|
|
2972
3002
|
else
|
|
2973
3003
|
@intCast((battle.rng.next() & 1) + 2);
|
|
2974
3004
|
|
|
2975
|
-
|
|
3005
|
+
assert(duration >= 2 and duration <= 3);
|
|
3006
|
+
options.chance.duration(player, duration);
|
|
2976
3007
|
return duration;
|
|
2977
3008
|
}
|
|
2978
3009
|
|
|
2979
|
-
|
|
3010
|
+
const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
|
|
3011
|
+
|
|
3012
|
+
fn distribution(
|
|
2980
3013
|
battle: anytype,
|
|
2981
3014
|
comptime effect: Move.Effect,
|
|
2982
3015
|
player: Player,
|
|
@@ -2985,15 +3018,18 @@ pub const Rolls = struct {
|
|
|
2985
3018
|
const roll = if (effect == .Binding) .duration else .multi_hit;
|
|
2986
3019
|
const n: u3 = if (options.calc.overridden(player, roll)) |val|
|
|
2987
3020
|
@intCast(val)
|
|
3021
|
+
else if (@hasDecl(@TypeOf(battle.rng), "distribution"))
|
|
3022
|
+
battle.rng.distribution(player)
|
|
2988
3023
|
else if (showdown)
|
|
2989
|
-
DISTRIBUTION[battle.rng.range(
|
|
3024
|
+
DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
|
|
2990
3025
|
else n: {
|
|
2991
3026
|
const r = (battle.rng.next() & 3);
|
|
2992
3027
|
break :n @intCast((if (r < 2) r else battle.rng.next() & 3) + 2);
|
|
2993
3028
|
};
|
|
2994
3029
|
|
|
3030
|
+
assert(n >= 2 and n <= 5);
|
|
2995
3031
|
if (effect == .Binding) {
|
|
2996
|
-
options.chance.duration(
|
|
3032
|
+
options.chance.duration(player, n);
|
|
2997
3033
|
} else {
|
|
2998
3034
|
try options.chance.multiHit(player, n);
|
|
2999
3035
|
}
|
|
@@ -3016,7 +3052,9 @@ pub const Rolls = struct {
|
|
|
3016
3052
|
else
|
|
3017
3053
|
null;
|
|
3018
3054
|
|
|
3019
|
-
const slot: u4 = overridden orelse
|
|
3055
|
+
const slot: u4 = overridden orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
|
|
3056
|
+
battle.rng.moveSlot(player, moves, check_pp)
|
|
3057
|
+
else slot: {
|
|
3020
3058
|
if (showdown) {
|
|
3021
3059
|
if (check_pp == 0) {
|
|
3022
3060
|
var i: usize = moves.len;
|
|
@@ -3032,10 +3070,9 @@ pub const Rolls = struct {
|
|
|
3032
3070
|
while (i < moves.len and r > 0) : (i += 1) {
|
|
3033
3071
|
if (moves[i].pp > 0) {
|
|
3034
3072
|
r -= 1;
|
|
3035
|
-
if (r == 0) break;
|
|
3073
|
+
if (r == 0) break :slot @intCast(i + 1);
|
|
3036
3074
|
}
|
|
3037
3075
|
}
|
|
3038
|
-
break :slot @intCast(i + 1);
|
|
3039
3076
|
}
|
|
3040
3077
|
}
|
|
3041
3078
|
|
|
@@ -3057,11 +3094,11 @@ test "RNG agreement" {
|
|
|
3057
3094
|
expected[i] = @intCast(i * 0x1000000);
|
|
3058
3095
|
}
|
|
3059
3096
|
|
|
3060
|
-
var spe
|
|
3061
|
-
var cfz
|
|
3062
|
-
var par
|
|
3063
|
-
var brn
|
|
3064
|
-
var eff
|
|
3097
|
+
var spe: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
|
|
3098
|
+
var cfz: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
|
|
3099
|
+
var par: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
|
|
3100
|
+
var brn: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
|
|
3101
|
+
var eff: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
|
|
3065
3102
|
|
|
3066
3103
|
for (0..expected.len) |i| {
|
|
3067
3104
|
try expectEqual(spe.range(u8, 0, 2) == 0, i < Gen12.percent(50) + 1);
|
|
@@ -3097,11 +3134,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3097
3134
|
const side = battle.side(player);
|
|
3098
3135
|
const foe = battle.foe(player);
|
|
3099
3136
|
|
|
3100
|
-
|
|
3137
|
+
const active = &side.active;
|
|
3101
3138
|
const stored = side.stored();
|
|
3102
3139
|
|
|
3103
3140
|
// 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
|
|
3141
|
+
// Pokémon Showdown instead produces a list with a single move that must be chosen
|
|
3105
3142
|
//
|
|
3106
3143
|
// Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
|
|
3107
3144
|
// here when not in compatibility mode, but the engine needs to be able to differentiate
|
|
@@ -3109,7 +3146,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3109
3146
|
// passing due to being forced into using a move. Instead of introducing another option
|
|
3110
3147
|
// we simply repurpose Move with no move slot, even though pedantically this is not
|
|
3111
3148
|
// strictly correct as the player would not have been presented the option to move or
|
|
3112
|
-
// switch at all
|
|
3149
|
+
// switch at all
|
|
3113
3150
|
if (isForced(active)) {
|
|
3114
3151
|
out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
|
|
3115
3152
|
n += 1;
|
|
@@ -3128,7 +3165,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3128
3165
|
// On the cartridge, all of these happen after "FIGHT" (indicating you are not
|
|
3129
3166
|
// switching) but before you are allowed to select a move. Pokémon Showdown instead
|
|
3130
3167
|
// either disables all other moves in the case of limited or requires you to select a
|
|
3131
|
-
// move normally if sleeping/frozen/bound
|
|
3168
|
+
// move normally if sleeping/frozen/bound
|
|
3132
3169
|
if (!showdown and (limited or foe.active.volatiles.Binding or
|
|
3133
3170
|
Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
|
|
3134
3171
|
{
|
|
@@ -3141,7 +3178,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3141
3178
|
// Pokémon Showdown handles Bide and Binding moves by checking if the move in question
|
|
3142
3179
|
// is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
|
|
3143
3180
|
// 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
|
|
3181
|
+
// proc-ed move in their moveset) and disabling all other moves
|
|
3145
3182
|
if (limited) {
|
|
3146
3183
|
assert(showdown);
|
|
3147
3184
|
assert(side.last_selected_move != .None);
|
|
@@ -3151,7 +3188,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3151
3188
|
if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
|
|
3152
3189
|
// Pokémon Showdown displays Struggle if limited to Bide but unable to pick
|
|
3153
3190
|
const struggle =
|
|
3154
|
-
m.id == .Bide and (m.pp == 0 or active.volatiles.
|
|
3191
|
+
m.id == .Bide and (m.pp == 0 or active.volatiles.disable_move == slot);
|
|
3155
3192
|
const s = if (struggle) 0 else slot;
|
|
3156
3193
|
out[n] = .{ .type = .Move, .data = s };
|
|
3157
3194
|
n += 1;
|
|
@@ -3166,7 +3203,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3166
3203
|
const m = active.moves[slot - 1];
|
|
3167
3204
|
if (m.id == .None) break;
|
|
3168
3205
|
if (m.pp == 0) continue;
|
|
3169
|
-
if (active.volatiles.
|
|
3206
|
+
if (active.volatiles.disable_move == slot) continue;
|
|
3170
3207
|
out[n] = .{ .type = .Move, .data = slot };
|
|
3171
3208
|
n += 1;
|
|
3172
3209
|
}
|