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