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