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