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