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