@pkmn/engine 0.1.0-dev.d716e940 → 0.1.0-dev.dd5b9081
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/README.md +55 -44
- package/build/pkg/addon.js +3 -2
- package/build/pkg/addon.js.map +1 -1
- package/build/pkg/data/layout.json +24 -2
- package/build/pkg/gen1.js +13 -16
- package/build/pkg/gen1.js.map +1 -1
- package/build/pkg/index.js.map +1 -1
- package/build.zig +86 -62
- package/package.json +25 -16
- package/src/bin/install-pkmn-engine +10 -5
- package/src/lib/binding/node.zig +45 -140
- package/src/lib/binding/wasm.zig +4 -2
- package/src/lib/common/battle.zig +17 -0
- package/src/lib/common/data.zig +14 -26
- package/src/lib/common/debug.zig +20 -11
- package/src/lib/common/js.zig +118 -0
- package/src/lib/common/optional.zig +60 -0
- package/src/lib/common/options.zig +9 -0
- package/src/lib/common/protocol.zig +277 -159
- package/src/lib/common/rational.zig +353 -0
- package/src/lib/common/rng.zig +11 -11
- package/src/lib/common/util.zig +12 -0
- package/src/lib/gen1/README.md +259 -177
- package/src/lib/gen1/calc.zig +426 -0
- package/src/lib/gen1/chance.zig +1036 -0
- package/src/lib/gen1/data/moves.zig +178 -180
- package/src/lib/gen1/data/species.zig +2 -2
- package/src/lib/gen1/data/types.zig +3 -3
- package/src/lib/gen1/data.zig +66 -54
- package/src/lib/gen1/helpers.zig +589 -18
- package/src/lib/gen1/mechanics.zig +791 -508
- package/src/lib/gen2/README.md +87 -13
- package/src/lib/gen2/data/items.zig +3 -3
- package/src/lib/gen2/data/moves.zig +624 -563
- package/src/lib/gen2/data/species.zig +1 -1
- package/src/lib/gen2/data/types.zig +3 -3
- package/src/lib/gen2/data.zig +100 -70
- package/src/lib/gen2/helpers.zig +16 -22
- package/src/lib/gen2/mechanics.zig +714 -10
- package/src/lib/pkmn.zig +61 -2
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
const std = @import("std");
|
|
2
2
|
|
|
3
|
+
const pkmn = @import("../pkmn.zig");
|
|
4
|
+
|
|
3
5
|
const common = @import("../common/data.zig");
|
|
4
6
|
const DEBUG = @import("../common/debug.zig").print;
|
|
5
|
-
const options = @import("../common/options.zig");
|
|
6
7
|
const protocol = @import("../common/protocol.zig");
|
|
7
8
|
const rng = @import("../common/rng.zig");
|
|
8
9
|
|
|
10
|
+
const Action = @import("chance.zig").Action;
|
|
11
|
+
|
|
9
12
|
const data = @import("data.zig");
|
|
10
13
|
|
|
11
14
|
const assert = std.debug.assert;
|
|
@@ -17,7 +20,7 @@ const ID = common.ID;
|
|
|
17
20
|
const Player = common.Player;
|
|
18
21
|
const Result = common.Result;
|
|
19
22
|
|
|
20
|
-
const showdown = options.showdown;
|
|
23
|
+
const showdown = pkmn.options.showdown;
|
|
21
24
|
|
|
22
25
|
const Boost = protocol.Boost;
|
|
23
26
|
const Damage = protocol.Damage;
|
|
@@ -58,9 +61,13 @@ const MAX_STAT_VALUE = 999;
|
|
|
58
61
|
|
|
59
62
|
const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
|
|
60
63
|
|
|
61
|
-
pub fn update(battle: anytype, c1: Choice, c2: Choice,
|
|
64
|
+
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("// ", .{});
|
|
62
69
|
assert(c1.type != .Pass or c2.type != .Pass or battle.turn == 0);
|
|
63
|
-
if (battle.turn == 0) return start(battle,
|
|
70
|
+
if (battle.turn == 0) return start(battle, options);
|
|
64
71
|
|
|
65
72
|
var s1 = false;
|
|
66
73
|
var s2 = false;
|
|
@@ -76,19 +83,19 @@ pub fn update(battle: anytype, c1: Choice, c2: Choice, log: anytype) !Result {
|
|
|
76
83
|
var r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
|
|
77
84
|
var r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
|
|
78
85
|
|
|
79
|
-
if (turnOrder(battle, c1, c2) == .P1) {
|
|
80
|
-
if (try doTurn(battle, .P1, c1, r1, s1, f1, .P2, c2, r2, s2, f2,
|
|
86
|
+
if (try turnOrder(battle, c1, c2, options) == .P1) {
|
|
87
|
+
if (try doTurn(battle, .P1, c1, r1, s1, f1, .P2, c2, r2, s2, f2, options)) |r| return r;
|
|
81
88
|
} else {
|
|
82
|
-
if (try doTurn(battle, .P2, c2, r2, s2, f2, .P1, c1, r1, s1, f1,
|
|
89
|
+
if (try doTurn(battle, .P2, c2, r2, s2, f2, .P1, c1, r1, s1, f1, options)) |r| return r;
|
|
83
90
|
}
|
|
84
91
|
|
|
85
92
|
if (p1.active.volatiles.attacks == 0) p1.active.volatiles.Binding = false;
|
|
86
93
|
if (p2.active.volatiles.attacks == 0) p2.active.volatiles.Binding = false;
|
|
87
94
|
|
|
88
|
-
return endTurn(battle,
|
|
95
|
+
return endTurn(battle, options);
|
|
89
96
|
}
|
|
90
97
|
|
|
91
|
-
fn start(battle: anytype,
|
|
98
|
+
fn start(battle: anytype, options: anytype) !Result {
|
|
92
99
|
const p1 = battle.side(.P1);
|
|
93
100
|
const p2 = battle.side(.P2);
|
|
94
101
|
|
|
@@ -100,10 +107,10 @@ fn start(battle: anytype, log: anytype) !Result {
|
|
|
100
107
|
assert(!showdown or p2_slot == 1);
|
|
101
108
|
if (p2_slot == 0) return Result.Win;
|
|
102
109
|
|
|
103
|
-
try switchIn(battle, .P1, p1_slot, true,
|
|
104
|
-
try switchIn(battle, .P2, p2_slot, true,
|
|
110
|
+
try switchIn(battle, .P1, p1_slot, true, options);
|
|
111
|
+
try switchIn(battle, .P2, p2_slot, true, options);
|
|
105
112
|
|
|
106
|
-
return endTurn(battle,
|
|
113
|
+
return endTurn(battle, options);
|
|
107
114
|
}
|
|
108
115
|
|
|
109
116
|
fn findFirstAlive(side: *const Side) u8 {
|
|
@@ -126,7 +133,7 @@ fn selectMove(
|
|
|
126
133
|
const stored = side.stored();
|
|
127
134
|
|
|
128
135
|
assert(!isForced(side.active) or
|
|
129
|
-
(choice.type == .Move and choice.data == @
|
|
136
|
+
(choice.type == .Move and choice.data == @intFromBool(showdown)));
|
|
130
137
|
|
|
131
138
|
// pre-battle menu
|
|
132
139
|
if (volatiles.Recharging) {
|
|
@@ -179,11 +186,7 @@ fn selectMove(
|
|
|
179
186
|
assert(choice.data == 0);
|
|
180
187
|
// GLITCH: https://glitchcity.wiki/Partial_trapping_move_Mirror_Move_link_battle_glitch
|
|
181
188
|
if (foe_choice.type == .Switch) {
|
|
182
|
-
const
|
|
183
|
-
battle.last_moves.p1_index
|
|
184
|
-
else
|
|
185
|
-
battle.last_moves.p2_index;
|
|
186
|
-
const last = side.active.move(@intCast(u4, slot));
|
|
189
|
+
const last = side.active.move(battle.lastMove(player).index);
|
|
187
190
|
if (last.id == .Metronome) side.last_selected_move = last.id;
|
|
188
191
|
if (last.id == .MirrorMove) return Result.Error;
|
|
189
192
|
}
|
|
@@ -211,12 +214,9 @@ fn selectMove(
|
|
|
211
214
|
}
|
|
212
215
|
break :ok true;
|
|
213
216
|
};
|
|
214
|
-
|
|
215
217
|
assert(struggle);
|
|
216
|
-
saveMove(battle, player, choice);
|
|
217
|
-
} else {
|
|
218
|
-
saveMove(battle, player, choice);
|
|
219
218
|
}
|
|
219
|
+
saveMove(battle, player, choice);
|
|
220
220
|
|
|
221
221
|
return null;
|
|
222
222
|
}
|
|
@@ -236,16 +236,12 @@ fn saveMove(battle: anytype, player: Player, choice: ?Choice) void {
|
|
|
236
236
|
assert(showdown or move.pp != 0);
|
|
237
237
|
|
|
238
238
|
side.last_selected_move = move.id;
|
|
239
|
-
|
|
240
|
-
battle.last_moves.p1_index = @intCast(u3, c.data);
|
|
241
|
-
} else {
|
|
242
|
-
battle.last_moves.p2_index = @intCast(u3, c.data);
|
|
243
|
-
}
|
|
239
|
+
battle.lastMove(player).index = @intCast(c.data);
|
|
244
240
|
}
|
|
245
241
|
}
|
|
246
242
|
}
|
|
247
243
|
|
|
248
|
-
fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool,
|
|
244
|
+
fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: anytype) !void {
|
|
249
245
|
var side = battle.side(player);
|
|
250
246
|
var foe = battle.foe(player);
|
|
251
247
|
var active = &side.active;
|
|
@@ -258,11 +254,7 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, log: anyty
|
|
|
258
254
|
side.order[0] = side.order[slot - 1];
|
|
259
255
|
side.order[slot - 1] = out;
|
|
260
256
|
|
|
261
|
-
|
|
262
|
-
battle.last_moves.p1_index = 1;
|
|
263
|
-
} else {
|
|
264
|
-
battle.last_moves.p2_index = 1;
|
|
265
|
-
}
|
|
257
|
+
battle.lastMove(player).index = 1;
|
|
266
258
|
|
|
267
259
|
side.last_used_move = .None;
|
|
268
260
|
foe.last_used_move = .None;
|
|
@@ -278,17 +270,18 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, log: anyty
|
|
|
278
270
|
|
|
279
271
|
foe.active.volatiles.Binding = false;
|
|
280
272
|
|
|
281
|
-
try log.switched(battle.active(player), incoming);
|
|
273
|
+
try options.log.switched(battle.active(player), incoming);
|
|
274
|
+
options.chance.switched(player, side.order[0], out);
|
|
282
275
|
|
|
283
276
|
if (showdown and incoming.status == Status.TOX) {
|
|
284
277
|
incoming.status = Status.init(.PSN);
|
|
285
278
|
// Technically, Pokémon Showdown adds these after *both* Pokémon have switched, but we'd
|
|
286
279
|
// rather not clutter up turnOrder just for this (incorrect) log message
|
|
287
|
-
try log.status(battle.active(player), incoming.status, .Silent);
|
|
280
|
+
try options.log.status(battle.active(player), incoming.status, .Silent);
|
|
288
281
|
}
|
|
289
282
|
}
|
|
290
283
|
|
|
291
|
-
fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
|
|
284
|
+
fn turnOrder(battle: anytype, c1: Choice, c2: Choice, options: anytype) !Player {
|
|
292
285
|
assert(c1.type != .Pass or c2.type != .Pass);
|
|
293
286
|
|
|
294
287
|
if (c1.type == .Pass) return .P2;
|
|
@@ -322,7 +315,7 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
|
|
|
322
315
|
battle.rng.advance(1);
|
|
323
316
|
}
|
|
324
317
|
|
|
325
|
-
const p1 = Rolls.speedTie(battle);
|
|
318
|
+
const p1 = try Rolls.speedTie(battle, options);
|
|
326
319
|
if (!showdown) return if (p1) .P1 else .P2;
|
|
327
320
|
|
|
328
321
|
// Pokémon Showdown's "lockedmove" volatile's onBeforeTurn uses BattleQueue#changeAction,
|
|
@@ -353,7 +346,7 @@ fn doTurn(
|
|
|
353
346
|
foe_rewrap: bool,
|
|
354
347
|
foe_skip: bool,
|
|
355
348
|
foe_from: ?Move,
|
|
356
|
-
|
|
349
|
+
options: anytype,
|
|
357
350
|
) !?Result {
|
|
358
351
|
assert(player_choice.type != .Pass);
|
|
359
352
|
|
|
@@ -367,19 +360,22 @@ fn doTurn(
|
|
|
367
360
|
player_skip,
|
|
368
361
|
player_from,
|
|
369
362
|
&residual,
|
|
370
|
-
|
|
363
|
+
options,
|
|
371
364
|
)) |r| return r;
|
|
372
365
|
if (!replace) {
|
|
373
366
|
if (player_choice.type != .Switch) {
|
|
374
|
-
if (try checkFaint(battle, foe_player,
|
|
367
|
+
if (try checkFaint(battle, foe_player, options)) |r| return r;
|
|
375
368
|
}
|
|
376
|
-
if (residual) try handleResidual(battle, player,
|
|
377
|
-
if (try checkFaint(battle, player,
|
|
369
|
+
if (residual) try handleResidual(battle, player, options);
|
|
370
|
+
if (try checkFaint(battle, player, options)) |r| return r;
|
|
378
371
|
} else if (foe_choice.type == .Pass) return null;
|
|
379
372
|
|
|
373
|
+
options.chance.clearPending();
|
|
374
|
+
|
|
380
375
|
residual = true;
|
|
381
376
|
replace = battle.side(foe_player).stored().hp == 0;
|
|
382
|
-
|
|
377
|
+
const calc = pkmn.options.calc and foe_choice.type == .Pass;
|
|
378
|
+
if (if (calc) null else try executeMove(
|
|
383
379
|
battle,
|
|
384
380
|
foe_player,
|
|
385
381
|
foe_choice,
|
|
@@ -387,14 +383,14 @@ fn doTurn(
|
|
|
387
383
|
foe_skip,
|
|
388
384
|
foe_from,
|
|
389
385
|
&residual,
|
|
390
|
-
|
|
386
|
+
options,
|
|
391
387
|
)) |r| return r;
|
|
392
388
|
if (!replace) {
|
|
393
|
-
if (foe_choice.type != .Switch) {
|
|
394
|
-
if (try checkFaint(battle, player,
|
|
389
|
+
if (!calc and foe_choice.type != .Switch) {
|
|
390
|
+
if (try checkFaint(battle, player, options)) |r| return r;
|
|
395
391
|
}
|
|
396
|
-
if (residual) try handleResidual(battle, foe_player,
|
|
397
|
-
if (try checkFaint(battle, foe_player,
|
|
392
|
+
if (residual) try handleResidual(battle, foe_player, options);
|
|
393
|
+
if (try checkFaint(battle, foe_player, options)) |r| return r;
|
|
398
394
|
}
|
|
399
395
|
|
|
400
396
|
// Flinch is bugged on Pokémon Showdown because it gets implemented with a duration which causes
|
|
@@ -415,12 +411,12 @@ fn executeMove(
|
|
|
415
411
|
skip: bool,
|
|
416
412
|
from: ?Move,
|
|
417
413
|
residual: *bool,
|
|
418
|
-
|
|
414
|
+
options: anytype,
|
|
419
415
|
) !?Result {
|
|
420
416
|
var side = battle.side(player);
|
|
421
417
|
|
|
422
418
|
if (choice.type == .Switch) {
|
|
423
|
-
try switchIn(battle, player,
|
|
419
|
+
try switchIn(battle, player, choice.data, false, options);
|
|
424
420
|
return null;
|
|
425
421
|
}
|
|
426
422
|
|
|
@@ -430,13 +426,13 @@ fn executeMove(
|
|
|
430
426
|
if (side.last_selected_move == .SKIP_TURN) {
|
|
431
427
|
assert(!showdown);
|
|
432
428
|
if (battle.foe(player).active.volatiles.Binding) {
|
|
433
|
-
try log.cant(battle.active(player), .Bound);
|
|
429
|
+
try options.log.cant(battle.active(player), .Bound);
|
|
434
430
|
}
|
|
435
431
|
return null;
|
|
436
432
|
}
|
|
437
433
|
|
|
438
434
|
assert(choice.type == .Move);
|
|
439
|
-
var mslot = @intCast(
|
|
435
|
+
var mslot: u4 = @intCast(choice.data);
|
|
440
436
|
// Sadly, we can't even check `Move.get(side.last_selected_move).effect == .Binding` here
|
|
441
437
|
// because Pokémon Showdown's Mirror Move implementation clobbers side.last_selected_move
|
|
442
438
|
var auto = showdown and side.last_selected_move != .None;
|
|
@@ -445,10 +441,7 @@ fn executeMove(
|
|
|
445
441
|
if (mslot == 0 and side.last_selected_move != .None and side.last_selected_move != .Struggle) {
|
|
446
442
|
// choice.data == 0 only happens with Struggle on Pokémon Showdown
|
|
447
443
|
assert(!showdown);
|
|
448
|
-
mslot = @intCast(
|
|
449
|
-
battle.last_moves.p1_index
|
|
450
|
-
else
|
|
451
|
-
battle.last_moves.p2_index);
|
|
444
|
+
mslot = @intCast(battle.lastMove(player).index);
|
|
452
445
|
const stored = side.stored();
|
|
453
446
|
// GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
|
|
454
447
|
auto = side.last_selected_move == .HyperBeam or side.active.volatiles.Bide or
|
|
@@ -469,15 +462,12 @@ fn executeMove(
|
|
|
469
462
|
} else if (showdown and side.active.volatiles.Charging) {
|
|
470
463
|
// Incorrect mslot with Pokémon Showdown choice semantics so we need to recover from index
|
|
471
464
|
assert(mslot == 1);
|
|
472
|
-
mslot = @intCast(
|
|
473
|
-
battle.last_moves.p1_index
|
|
474
|
-
else
|
|
475
|
-
battle.last_moves.p2_index);
|
|
465
|
+
mslot = @intCast(battle.lastMove(player).index);
|
|
476
466
|
}
|
|
477
467
|
|
|
478
468
|
var skip_can = false;
|
|
479
469
|
var skip_pp = false;
|
|
480
|
-
switch (try beforeMove(battle, player, skip, from, residual,
|
|
470
|
+
switch (try beforeMove(battle, player, skip, from, residual, options)) {
|
|
481
471
|
.done => return null,
|
|
482
472
|
.skip_can => skip_can = true,
|
|
483
473
|
.skip_pp => skip_pp = true,
|
|
@@ -485,11 +475,11 @@ fn executeMove(
|
|
|
485
475
|
.err => return @as(?Result, Result.Error),
|
|
486
476
|
}
|
|
487
477
|
|
|
488
|
-
if (!skip_can and !try canMove(battle, player, mslot, auto, skip_pp, from, residual,
|
|
478
|
+
if (!skip_can and !try canMove(battle, player, mslot, auto, skip_pp, from, residual, options)) {
|
|
489
479
|
return null;
|
|
490
480
|
}
|
|
491
481
|
|
|
492
|
-
return doMove(battle, player, mslot, rewrap, auto, residual,
|
|
482
|
+
return doMove(battle, player, mslot, rewrap, auto, residual, options);
|
|
493
483
|
}
|
|
494
484
|
|
|
495
485
|
const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
|
|
@@ -500,8 +490,9 @@ fn beforeMove(
|
|
|
500
490
|
skip: bool,
|
|
501
491
|
from: ?Move,
|
|
502
492
|
residual: *bool,
|
|
503
|
-
|
|
493
|
+
options: anytype,
|
|
504
494
|
) !BeforeMove {
|
|
495
|
+
var log = options.log;
|
|
505
496
|
var side = battle.side(player);
|
|
506
497
|
const foe = battle.foe(player);
|
|
507
498
|
var active = &side.active;
|
|
@@ -510,11 +501,21 @@ fn beforeMove(
|
|
|
510
501
|
var volatiles = &active.volatiles;
|
|
511
502
|
|
|
512
503
|
if (Status.is(stored.status, .SLP)) {
|
|
513
|
-
const
|
|
504
|
+
const before = stored.status;
|
|
514
505
|
// Even if the EXT bit is set this will still correctly modify the sleep duration
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
506
|
+
if (options.calc.modify(player, .sleep)) |extend| {
|
|
507
|
+
if (!extend) stored.status = 0;
|
|
508
|
+
} else {
|
|
509
|
+
stored.status -= 1;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const duration = Status.duration(stored.status);
|
|
513
|
+
if (!Status.is(stored.status, .EXT)) {
|
|
514
|
+
try options.chance.sleep(player, duration);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (duration == 0) {
|
|
518
|
+
try log.curestatus(ident, before, .Message);
|
|
518
519
|
stored.status = 0; // clears EXT if present
|
|
519
520
|
} else {
|
|
520
521
|
try log.cant(ident, .Sleep);
|
|
@@ -549,14 +550,20 @@ fn beforeMove(
|
|
|
549
550
|
}
|
|
550
551
|
|
|
551
552
|
if (volatiles.disabled_duration > 0) {
|
|
552
|
-
|
|
553
|
+
if (options.calc.modify(player, .disable)) |extend| {
|
|
554
|
+
if (!extend) volatiles.disabled_duration = 0;
|
|
555
|
+
} else {
|
|
556
|
+
volatiles.disabled_duration -= 1;
|
|
557
|
+
}
|
|
558
|
+
try options.chance.disable(player, volatiles.disabled_duration);
|
|
559
|
+
|
|
553
560
|
if (volatiles.disabled_duration == 0) {
|
|
554
561
|
volatiles.disabled_move = 0;
|
|
555
562
|
try log.end(ident, .Disable);
|
|
556
563
|
}
|
|
557
564
|
}
|
|
558
565
|
// Pokémon Showdown's disable condition has a single onBeforeMove handler
|
|
559
|
-
if (showdown and try disabled(side, ident,
|
|
566
|
+
if (showdown and try disabled(side, ident, options)) return .done;
|
|
560
567
|
|
|
561
568
|
// This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
|
|
562
569
|
// before the side had a selected a move - we simply need to assume this leads to a desync
|
|
@@ -568,14 +575,20 @@ fn beforeMove(
|
|
|
568
575
|
if (volatiles.Confusion) {
|
|
569
576
|
assert(volatiles.confusion > 0);
|
|
570
577
|
|
|
571
|
-
|
|
578
|
+
if (options.calc.modify(player, .confusion)) |extend| {
|
|
579
|
+
if (!extend) volatiles.confusion = 0;
|
|
580
|
+
} else {
|
|
581
|
+
volatiles.confusion -= 1;
|
|
582
|
+
}
|
|
583
|
+
try options.chance.confusion(player, volatiles.confusion);
|
|
584
|
+
|
|
572
585
|
if (volatiles.confusion == 0) {
|
|
573
586
|
volatiles.Confusion = false;
|
|
574
587
|
try log.end(ident, .Confusion);
|
|
575
588
|
} else {
|
|
576
589
|
try log.activate(ident, .Confusion);
|
|
577
590
|
|
|
578
|
-
if (Rolls.confused(battle)) {
|
|
591
|
+
if (try Rolls.confused(battle, player, options)) {
|
|
579
592
|
assert(!volatiles.MultiHit);
|
|
580
593
|
if (!volatiles.Rage) volatiles.state = 0;
|
|
581
594
|
volatiles.Bide = false;
|
|
@@ -585,6 +598,7 @@ fn beforeMove(
|
|
|
585
598
|
volatiles.Charging = false;
|
|
586
599
|
volatiles.Binding = false;
|
|
587
600
|
volatiles.Invulnerable = false;
|
|
601
|
+
options.chance.clearDurations(player, null);
|
|
588
602
|
{
|
|
589
603
|
// This feels (and is) disgusting but the cartridge literally just overwrites
|
|
590
604
|
// the opponent's defense with the user's defense and resets it after. As a
|
|
@@ -604,7 +618,7 @@ fn beforeMove(
|
|
|
604
618
|
|
|
605
619
|
const uncapped = battle.last_damage;
|
|
606
620
|
// Skipping adjustDamage / randomizeDamage / checkHit
|
|
607
|
-
_ = try applyDamage(battle, player, target, .Confusion,
|
|
621
|
+
_ = try applyDamage(battle, player, target, .Confusion, options);
|
|
608
622
|
// Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
|
|
609
623
|
if (showdown) battle.last_damage = uncapped;
|
|
610
624
|
|
|
@@ -613,14 +627,15 @@ fn beforeMove(
|
|
|
613
627
|
}
|
|
614
628
|
}
|
|
615
629
|
|
|
616
|
-
if (!showdown and try disabled(side, ident,
|
|
630
|
+
if (!showdown and try disabled(side, ident, options)) return .done;
|
|
617
631
|
|
|
618
|
-
if (Status.is(stored.status, .PAR) and Rolls.paralyzed(battle)) {
|
|
632
|
+
if (Status.is(stored.status, .PAR) and try Rolls.paralyzed(battle, player, options)) {
|
|
619
633
|
if (!volatiles.Rage) volatiles.state = 0;
|
|
620
634
|
volatiles.Bide = false;
|
|
621
635
|
volatiles.Thrashing = false;
|
|
622
636
|
volatiles.Charging = false;
|
|
623
637
|
volatiles.Binding = false;
|
|
638
|
+
options.chance.clearDurations(player, null);
|
|
624
639
|
// GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
|
|
625
640
|
try log.cant(ident, .Paralysis);
|
|
626
641
|
return .done;
|
|
@@ -640,7 +655,13 @@ fn beforeMove(
|
|
|
640
655
|
|
|
641
656
|
assert(volatiles.attacks > 0);
|
|
642
657
|
|
|
643
|
-
|
|
658
|
+
if (options.calc.modify(player, .attacking)) |extend| {
|
|
659
|
+
if (!extend) volatiles.attacks = 0;
|
|
660
|
+
} else {
|
|
661
|
+
volatiles.attacks -= 1;
|
|
662
|
+
}
|
|
663
|
+
try options.chance.attacking(player, volatiles.attacks);
|
|
664
|
+
|
|
644
665
|
if (volatiles.attacks != 0) {
|
|
645
666
|
try log.activate(ident, .Bide);
|
|
646
667
|
return .done;
|
|
@@ -658,15 +679,15 @@ fn beforeMove(
|
|
|
658
679
|
}
|
|
659
680
|
|
|
660
681
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
661
|
-
_ = try applyDamage(battle, player.foe(), player.foe(), .None,
|
|
662
|
-
if (foe.stored().hp > 0 and !sub) try buildRage(battle, player.foe(),
|
|
682
|
+
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
683
|
+
if (foe.stored().hp > 0 and !sub) try buildRage(battle, player.foe(), options);
|
|
663
684
|
|
|
664
685
|
// For reasons passing understanding, Pokémon Showdown still inflicts residual damage to
|
|
665
686
|
// Bide's user even if the above damage has caused the foe to faint. It's simpler to always
|
|
666
687
|
// run residual here regardless of whether the foe fainted and opt-out of the default flow
|
|
667
688
|
if (showdown) {
|
|
668
689
|
residual.* = false;
|
|
669
|
-
try handleResidual(battle, player,
|
|
690
|
+
try handleResidual(battle, player, options);
|
|
670
691
|
}
|
|
671
692
|
|
|
672
693
|
return .done;
|
|
@@ -675,8 +696,15 @@ fn beforeMove(
|
|
|
675
696
|
if (volatiles.Thrashing) {
|
|
676
697
|
assert(volatiles.attacks > 0);
|
|
677
698
|
assert(from != null);
|
|
678
|
-
|
|
679
|
-
if (
|
|
699
|
+
|
|
700
|
+
if (options.calc.modify(player, .attacking)) |extend| {
|
|
701
|
+
if (!extend) volatiles.attacks = 0;
|
|
702
|
+
} else {
|
|
703
|
+
volatiles.attacks -= 1;
|
|
704
|
+
}
|
|
705
|
+
try options.chance.attacking(player, volatiles.attacks);
|
|
706
|
+
|
|
707
|
+
if (!showdown and handleThrashing(battle, active, player, options)) {
|
|
680
708
|
try log.start(battle.active(player), .ConfusionSilent);
|
|
681
709
|
}
|
|
682
710
|
try log.move(ident, side.last_selected_move, battle.active(player.foe()), from);
|
|
@@ -684,7 +712,7 @@ fn beforeMove(
|
|
|
684
712
|
// This shouldn't actually set last_used_move, but Pokémon Showdown sets last
|
|
685
713
|
// used in useMove and doesn't have the notion of skipping canMove semantics
|
|
686
714
|
side.last_used_move = side.last_selected_move;
|
|
687
|
-
if (handleThrashing(battle, active)) {
|
|
715
|
+
if (handleThrashing(battle, active, player, options)) {
|
|
688
716
|
try log.start(battle.active(player), .ConfusionSilent);
|
|
689
717
|
}
|
|
690
718
|
}
|
|
@@ -693,7 +721,14 @@ fn beforeMove(
|
|
|
693
721
|
|
|
694
722
|
if (volatiles.Binding) {
|
|
695
723
|
assert(volatiles.attacks > 0);
|
|
696
|
-
|
|
724
|
+
|
|
725
|
+
if (options.calc.modify(player, .binding)) |extend| {
|
|
726
|
+
if (!extend) volatiles.attacks = 0;
|
|
727
|
+
} else {
|
|
728
|
+
volatiles.attacks -= 1;
|
|
729
|
+
}
|
|
730
|
+
try options.chance.binding(player, volatiles.attacks);
|
|
731
|
+
|
|
697
732
|
try log.move(
|
|
698
733
|
battle.active(player),
|
|
699
734
|
side.last_selected_move,
|
|
@@ -702,9 +737,9 @@ fn beforeMove(
|
|
|
702
737
|
);
|
|
703
738
|
if (showdown or battle.last_damage != 0) {
|
|
704
739
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
705
|
-
_ = try applyDamage(battle, player.foe(), player.foe(), .None,
|
|
740
|
+
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
706
741
|
if (battle.foe(player).stored().hp > 0 and !sub) {
|
|
707
|
-
try buildRage(battle, player.foe(),
|
|
742
|
+
try buildRage(battle, player.foe(), options);
|
|
708
743
|
}
|
|
709
744
|
}
|
|
710
745
|
return .done;
|
|
@@ -721,7 +756,7 @@ fn canMove(
|
|
|
721
756
|
skip_pp: bool,
|
|
722
757
|
from: ?Move,
|
|
723
758
|
residual: *bool,
|
|
724
|
-
|
|
759
|
+
options: anytype,
|
|
725
760
|
) !bool {
|
|
726
761
|
var side = battle.side(player);
|
|
727
762
|
const player_ident = battle.active(player);
|
|
@@ -736,9 +771,9 @@ fn canMove(
|
|
|
736
771
|
side.active.volatiles.Charging = false;
|
|
737
772
|
side.active.volatiles.Invulnerable = false;
|
|
738
773
|
} else if (move.effect == .Charge) {
|
|
739
|
-
try log.move(player_ident, side.last_selected_move, .{}, from);
|
|
774
|
+
try options.log.move(player_ident, side.last_selected_move, .{}, from);
|
|
740
775
|
setCounterable(battle, player, side, move);
|
|
741
|
-
try Effects.charge(battle, player,
|
|
776
|
+
try Effects.charge(battle, player, options);
|
|
742
777
|
return false;
|
|
743
778
|
}
|
|
744
779
|
|
|
@@ -753,19 +788,19 @@ fn canMove(
|
|
|
753
788
|
// relevant for control flow, but we don't have anything useful to put in the log. This is
|
|
754
789
|
// regrettable, but this information is more "nice to have" than required
|
|
755
790
|
const f = if (from != null and from.? == .None) null else from;
|
|
756
|
-
try log.move(player_ident, side.last_selected_move, battle.active(target), f);
|
|
791
|
+
try options.log.move(player_ident, side.last_selected_move, battle.active(target), f);
|
|
757
792
|
setCounterable(battle, player, side, move);
|
|
758
793
|
|
|
759
794
|
if (move.effect.onBegin()) {
|
|
760
|
-
try onBegin(battle, player, move, mslot, residual,
|
|
795
|
+
try onBegin(battle, player, move, mslot, residual, options);
|
|
761
796
|
return false;
|
|
762
797
|
}
|
|
763
798
|
|
|
764
799
|
if (move.effect == .Thrashing) {
|
|
765
|
-
Effects.thrashing(battle, player);
|
|
800
|
+
Effects.thrashing(battle, player, options);
|
|
766
801
|
} else if (!showdown and move.effect == .Binding) {
|
|
767
802
|
// Pokémon Showdown handles this after hit/miss checks and damage calculation
|
|
768
|
-
Effects.binding(battle, player, false);
|
|
803
|
+
try Effects.binding(battle, player, false, options);
|
|
769
804
|
}
|
|
770
805
|
|
|
771
806
|
return true;
|
|
@@ -779,11 +814,7 @@ fn setCounterable(battle: anytype, player: Player, side: *Side, move: Move.Data)
|
|
|
779
814
|
// as the desync results from actually having two separate battle states that subtly disagree
|
|
780
815
|
const counterable = side.last_selected_move != .Counter and move.bp > 0 and
|
|
781
816
|
(move.type == .Normal or move.type == .Fighting);
|
|
782
|
-
|
|
783
|
-
battle.last_moves.p1_counterable = @boolToInt(counterable);
|
|
784
|
-
} else {
|
|
785
|
-
battle.last_moves.p2_counterable = @boolToInt(counterable);
|
|
786
|
-
}
|
|
817
|
+
battle.lastMove(player).counterable = @intFromBool(counterable);
|
|
787
818
|
}
|
|
788
819
|
|
|
789
820
|
fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
|
|
@@ -796,11 +827,11 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
|
|
|
796
827
|
if (volatiles.Bide) return;
|
|
797
828
|
|
|
798
829
|
assert(active.move(mslot).pp > 0 or auto);
|
|
799
|
-
active.move(mslot).pp = @
|
|
830
|
+
active.move(mslot).pp = @as(u6, @intCast(active.move(mslot).pp)) -% 1;
|
|
800
831
|
if (volatiles.Transform) return;
|
|
801
832
|
|
|
802
833
|
assert(side.stored().move(mslot).pp > 0 or auto);
|
|
803
|
-
side.stored().move(mslot).pp = @
|
|
834
|
+
side.stored().move(mslot).pp = @as(u6, @intCast(side.stored().move(mslot).pp)) -% 1;
|
|
804
835
|
assert(active.move(mslot).pp == side.stored().move(mslot).pp);
|
|
805
836
|
}
|
|
806
837
|
|
|
@@ -808,12 +839,12 @@ fn incrementPP(side: *Side, mslot: u4) void {
|
|
|
808
839
|
var active = &side.active;
|
|
809
840
|
const volatiles = &active.volatiles;
|
|
810
841
|
|
|
811
|
-
active.move(mslot).pp = @
|
|
842
|
+
active.move(mslot).pp = @as(u6, @intCast(active.move(mslot).pp)) +% 1;
|
|
812
843
|
// GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
|
|
813
844
|
if (showdown and volatiles.Transform) return;
|
|
814
845
|
|
|
815
846
|
assert(mslot > 0 and mslot <= 4);
|
|
816
|
-
side.stored().moves[mslot - 1].pp = @
|
|
847
|
+
side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% 1;
|
|
817
848
|
}
|
|
818
849
|
|
|
819
850
|
// Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
|
|
@@ -824,8 +855,9 @@ fn doMove(
|
|
|
824
855
|
rewrap: bool,
|
|
825
856
|
auto: bool,
|
|
826
857
|
residual: *bool,
|
|
827
|
-
|
|
858
|
+
options: anytype,
|
|
828
859
|
) !?Result {
|
|
860
|
+
var log = options.log;
|
|
829
861
|
var side = battle.side(player);
|
|
830
862
|
const foe = battle.foe(player);
|
|
831
863
|
|
|
@@ -840,23 +872,27 @@ fn doMove(
|
|
|
840
872
|
var hits: u4 = 1;
|
|
841
873
|
var effectiveness = Effectiveness.neutral;
|
|
842
874
|
|
|
875
|
+
// Due to control flow shenanigans we need to clear last_damage for Pokémon Showdown
|
|
876
|
+
if (showdown and !counter) battle.last_damage = 0;
|
|
877
|
+
|
|
843
878
|
// The cartridge handles set damage moves in applyDamage but we short circuit to simplify things
|
|
844
879
|
if (move.effect == .SuperFang or move.effect == .SpecialDamage) {
|
|
845
|
-
return specialDamage(battle, player, move,
|
|
880
|
+
return specialDamage(battle, player, move, options);
|
|
846
881
|
}
|
|
847
882
|
|
|
848
883
|
// Pokémon Showdown runs invulnerability / immunity checks before checking accuracy - simply
|
|
849
884
|
// calling moveHit early covers most of that but we also need to check type immunity first
|
|
850
885
|
var miss = showdown and miss: {
|
|
851
886
|
immune = move.target != .Self and !status and !counter and
|
|
852
|
-
(@
|
|
853
|
-
@
|
|
887
|
+
(@intFromEnum(move.type.effectiveness(foe.active.types.type1)) == 0 or
|
|
888
|
+
@intFromEnum(move.type.effectiveness(foe.active.types.type2)) == 0);
|
|
854
889
|
if (immune and move.effect != .Binding) break :miss true;
|
|
855
890
|
if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) {
|
|
856
891
|
battle.last_damage = 0;
|
|
857
892
|
break :miss true;
|
|
858
893
|
}
|
|
859
|
-
break :miss move.target != .Self and
|
|
894
|
+
break :miss (move.target != .Self and
|
|
895
|
+
!moveHit(battle, player, move, &immune, &mist, options));
|
|
860
896
|
};
|
|
861
897
|
assert(!immune or miss or (showdown and move.effect == .Binding));
|
|
862
898
|
|
|
@@ -864,16 +900,16 @@ fn doMove(
|
|
|
864
900
|
const skip = status or immune;
|
|
865
901
|
if ((!showdown or (!skip or counter)) and !miss) blk: {
|
|
866
902
|
if (showdown and move.effect.isMulti()) {
|
|
867
|
-
Effects.multiHit(battle, player, move);
|
|
903
|
+
try Effects.multiHit(battle, player, move, options);
|
|
868
904
|
hits = side.active.volatiles.attacks;
|
|
869
905
|
late = false;
|
|
870
906
|
}
|
|
871
907
|
|
|
872
908
|
// Cartridge rolls for crit even for moves that can't crit (Counter/Metronome/status/OHKO)
|
|
873
909
|
const check = !showdown or (!counter and move.effect != .OHKO);
|
|
874
|
-
if (check) crit = checkCriticalHit(battle, player, move);
|
|
910
|
+
if (check) crit = try checkCriticalHit(battle, player, move, options);
|
|
875
911
|
|
|
876
|
-
if (counter) return counterDamage(battle, player, move,
|
|
912
|
+
if (counter) return counterDamage(battle, player, move, options);
|
|
877
913
|
|
|
878
914
|
battle.last_damage = 0;
|
|
879
915
|
|
|
@@ -895,17 +931,15 @@ fn doMove(
|
|
|
895
931
|
effectiveness = adjustDamage(battle, player);
|
|
896
932
|
immune = effectiveness == 0;
|
|
897
933
|
}
|
|
898
|
-
randomizeDamage(battle);
|
|
934
|
+
try randomizeDamage(battle, player, options);
|
|
899
935
|
}
|
|
900
936
|
}
|
|
901
937
|
|
|
902
|
-
|
|
903
|
-
if (showdown and skip) battle.last_damage = 0;
|
|
904
|
-
|
|
938
|
+
var zero = battle.last_damage == 0;
|
|
905
939
|
miss = if (showdown or skip)
|
|
906
940
|
miss
|
|
907
941
|
else
|
|
908
|
-
(!moveHit(battle, player, move, &immune, &mist) or
|
|
942
|
+
(!moveHit(battle, player, move, &immune, &mist, options) or zero);
|
|
909
943
|
|
|
910
944
|
assert(showdown or miss or battle.last_damage > 0 or skip);
|
|
911
945
|
assert((!showdown and miss) or !(ohko and immune));
|
|
@@ -913,11 +947,11 @@ fn doMove(
|
|
|
913
947
|
|
|
914
948
|
if (!showdown or !miss) {
|
|
915
949
|
if (move.effect == .MirrorMove) {
|
|
916
|
-
return mirrorMove(battle, player, mslot, rewrap, auto, residual,
|
|
950
|
+
return mirrorMove(battle, player, mslot, rewrap, auto, residual, options);
|
|
917
951
|
} else if (move.effect == .Metronome) {
|
|
918
|
-
return metronome(battle, player, mslot, rewrap, auto, residual,
|
|
952
|
+
return metronome(battle, player, mslot, rewrap, auto, residual, options);
|
|
919
953
|
} else if (move.effect.onEnd()) {
|
|
920
|
-
try onEnd(battle, player, move,
|
|
954
|
+
try onEnd(battle, player, move, options);
|
|
921
955
|
return null;
|
|
922
956
|
}
|
|
923
957
|
}
|
|
@@ -936,29 +970,34 @@ fn doMove(
|
|
|
936
970
|
if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
|
|
937
971
|
try log.fail(foe_ident, .None);
|
|
938
972
|
} else {
|
|
973
|
+
if (showdown or !zero) try options.chance.commit(player, .miss);
|
|
939
974
|
try log.lastmiss();
|
|
940
975
|
try log.miss(battle.active(player));
|
|
941
976
|
}
|
|
942
977
|
if (move.effect == .JumpKick) {
|
|
943
978
|
// Recoil is supposed to be damage/8 but damage will always be 0 here
|
|
944
|
-
assert(battle.last_damage == 0);
|
|
979
|
+
assert(showdown or battle.last_damage == 0);
|
|
945
980
|
battle.last_damage = 1;
|
|
946
|
-
_ = try applyDamage(battle, player, player.foe(), .None,
|
|
981
|
+
_ = try applyDamage(battle, player, player.foe(), .None, options);
|
|
947
982
|
if (showdown and side.stored().hp == 0) residual.* = false;
|
|
948
983
|
} else if (move.effect == .Explode) {
|
|
949
984
|
try Effects.explode(battle, player);
|
|
950
|
-
try buildRage(battle, player.foe(),
|
|
985
|
+
try buildRage(battle, player.foe(), options);
|
|
951
986
|
} else if (showdown and move.effect == .Disable) {
|
|
952
|
-
try buildRage(battle, player.foe(),
|
|
987
|
+
try buildRage(battle, player.foe(), options);
|
|
988
|
+
} else if (!showdown and immune and move.effect == .Binding) {
|
|
989
|
+
try options.chance.commit(player, .binding);
|
|
953
990
|
}
|
|
954
991
|
return null;
|
|
992
|
+
} else {
|
|
993
|
+
try options.chance.commit(player, .hit);
|
|
955
994
|
}
|
|
956
995
|
|
|
957
996
|
// On the cartridge MultiHit doesn't get set up until after damage has been applied for the
|
|
958
997
|
// first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
|
|
959
998
|
// it up above before damage calculation).
|
|
960
999
|
if (!showdown and move.effect.isMulti()) {
|
|
961
|
-
Effects.multiHit(battle, player, move);
|
|
1000
|
+
try Effects.multiHit(battle, player, move, options);
|
|
962
1001
|
hits = side.active.volatiles.attacks;
|
|
963
1002
|
}
|
|
964
1003
|
|
|
@@ -976,11 +1015,13 @@ fn doMove(
|
|
|
976
1015
|
try log.resisted(battle.active(player.foe()));
|
|
977
1016
|
}
|
|
978
1017
|
}
|
|
979
|
-
if (!skip) nullified = try applyDamage(battle, player.foe(), player.foe(), .None,
|
|
1018
|
+
if (!skip) nullified = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
980
1019
|
if (hit == 0 and ohko) try log.ohko();
|
|
981
1020
|
hit += 1;
|
|
982
1021
|
if (foe.stored().hp == 0) break;
|
|
983
|
-
if (!late and (!sub or move.effect == .Explode))
|
|
1022
|
+
if (!late and (!sub or move.effect == .Explode)) {
|
|
1023
|
+
try buildRage(battle, player.foe(), options);
|
|
1024
|
+
}
|
|
984
1025
|
// If the substitute breaks during a multi-hit attack, the attack ends
|
|
985
1026
|
if (nullified) break;
|
|
986
1027
|
}
|
|
@@ -990,13 +1031,13 @@ fn doMove(
|
|
|
990
1031
|
assert(nullified or foe.stored().hp == 0 or side.active.volatiles.attacks - hit == 0);
|
|
991
1032
|
side.active.volatiles.attacks = 0;
|
|
992
1033
|
if (showdown and move.effect == .Twineedle and !nullified and foe.stored().hp > 0) {
|
|
993
|
-
try Effects.poison(battle, player, Move.get(.PoisonSting),
|
|
1034
|
+
try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
994
1035
|
}
|
|
995
1036
|
try log.hitcount(battle.active(player.foe()), hit);
|
|
996
1037
|
} else if (showdown) {
|
|
997
1038
|
// This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
|
|
998
1039
|
if (move.effect == .Binding) {
|
|
999
|
-
Effects.binding(battle, player, rewrap);
|
|
1040
|
+
try Effects.binding(battle, player, rewrap, options);
|
|
1000
1041
|
if (immune) {
|
|
1001
1042
|
battle.last_damage = 0;
|
|
1002
1043
|
assert(foe.stored().hp > 0);
|
|
@@ -1005,7 +1046,7 @@ fn doMove(
|
|
|
1005
1046
|
try log.activate(battle.active(player.foe()), .Substitute);
|
|
1006
1047
|
} else {
|
|
1007
1048
|
try log.damage(battle.active(player.foe()), foe.stored(), .None);
|
|
1008
|
-
try buildRage(battle, player.foe(),
|
|
1049
|
+
try buildRage(battle, player.foe(), options);
|
|
1009
1050
|
}
|
|
1010
1051
|
return null;
|
|
1011
1052
|
}
|
|
@@ -1019,12 +1060,12 @@ fn doMove(
|
|
|
1019
1060
|
// On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
|
|
1020
1061
|
// but this is only done to setup the MultiHit looping in the first place. Moving the MultiHit
|
|
1021
1062
|
// setup before the loop means we can avoid having to waste time doing no-op handler searches.
|
|
1022
|
-
if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual,
|
|
1063
|
+
if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
|
|
1023
1064
|
|
|
1024
1065
|
if (foe.stored().hp == 0) return null;
|
|
1025
1066
|
|
|
1026
1067
|
// Pokémon Showdown builds Rage at the wrong time for non-MultiHit move
|
|
1027
|
-
if (late and !sub and move.effect != .Disable) try buildRage(battle, player.foe(),
|
|
1068
|
+
if (late and !sub and move.effect != .Disable) try buildRage(battle, player.foe(), options);
|
|
1028
1069
|
|
|
1029
1070
|
if (!move.effect.isSpecial()) {
|
|
1030
1071
|
// On the cartridge Rage is not considered to be "special" and thus gets executed for a
|
|
@@ -1035,29 +1076,29 @@ fn doMove(
|
|
|
1035
1076
|
// Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
|
|
1036
1077
|
// puts the |-status| message before |-hitcount| instead of after.
|
|
1037
1078
|
if (move.effect == .Twineedle) {
|
|
1038
|
-
if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting),
|
|
1079
|
+
if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
1039
1080
|
} else if (move.effect == .Disable) {
|
|
1040
|
-
const result = try Effects.disable(battle, player, move,
|
|
1041
|
-
if (showdown) try buildRage(battle, player.foe(),
|
|
1081
|
+
const result = try Effects.disable(battle, player, move, options);
|
|
1082
|
+
if (showdown) try buildRage(battle, player.foe(), options);
|
|
1042
1083
|
return result;
|
|
1043
1084
|
} else {
|
|
1044
|
-
try moveEffect(battle, player, move,
|
|
1085
|
+
try moveEffect(battle, player, move, options);
|
|
1045
1086
|
}
|
|
1046
1087
|
}
|
|
1047
1088
|
|
|
1048
1089
|
return null;
|
|
1049
1090
|
}
|
|
1050
1091
|
|
|
1051
|
-
fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
|
|
1092
|
+
fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !bool {
|
|
1052
1093
|
const side = battle.side(player);
|
|
1053
1094
|
|
|
1054
1095
|
// Base speed is used for the critical hit calculation, even when Transform-ed
|
|
1055
|
-
var
|
|
1056
|
-
// GLITCH: Focus Energy reduces critical hit
|
|
1057
|
-
|
|
1058
|
-
|
|
1096
|
+
var rate = @as(u16, Species.chance(side.stored().species));
|
|
1097
|
+
// GLITCH: Focus Energy reduces critical hit rate instead of increasing it
|
|
1098
|
+
rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
|
|
1099
|
+
rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
|
|
1059
1100
|
|
|
1060
|
-
return Rolls.criticalHit(battle,
|
|
1101
|
+
return Rolls.criticalHit(battle, player, @intCast(rate), options);
|
|
1061
1102
|
}
|
|
1062
1103
|
|
|
1063
1104
|
fn calcDamage(
|
|
@@ -1123,7 +1164,7 @@ fn calcDamage(
|
|
|
1123
1164
|
d = @min(997, d);
|
|
1124
1165
|
d += 2;
|
|
1125
1166
|
|
|
1126
|
-
battle.last_damage = @intCast(
|
|
1167
|
+
battle.last_damage = @intCast(d);
|
|
1127
1168
|
|
|
1128
1169
|
return true;
|
|
1129
1170
|
}
|
|
@@ -1137,9 +1178,9 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
|
1137
1178
|
var d = battle.last_damage;
|
|
1138
1179
|
if (side.active.types.includes(move.type)) d +%= d / 2;
|
|
1139
1180
|
|
|
1140
|
-
const neutral = @
|
|
1141
|
-
const eff1: u16 = @
|
|
1142
|
-
const eff2: u16 = @
|
|
1181
|
+
const neutral = @intFromEnum(Effectiveness.Neutral);
|
|
1182
|
+
const eff1: u16 = @intFromEnum(move.type.effectiveness(types.type1));
|
|
1183
|
+
const eff2: u16 = @intFromEnum(move.type.effectiveness(types.type2));
|
|
1143
1184
|
|
|
1144
1185
|
// Type effectiveness matchup precedence only matters with (NVE, SE)
|
|
1145
1186
|
if (!showdown and (eff1 + eff2) == Effectiveness.mismatch and
|
|
@@ -1159,17 +1200,20 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
|
1159
1200
|
return if (types.type1 == types.type2) eff1 * neutral else eff1 * eff2;
|
|
1160
1201
|
}
|
|
1161
1202
|
|
|
1162
|
-
fn randomizeDamage(battle: anytype) void {
|
|
1203
|
+
fn randomizeDamage(battle: anytype, player: Player, options: anytype) !void {
|
|
1163
1204
|
if (battle.last_damage <= 1) return;
|
|
1164
|
-
const random = Rolls.damage(battle);
|
|
1165
|
-
|
|
1205
|
+
const random = try Rolls.damage(battle, player, options);
|
|
1206
|
+
options.calc.base(player.foe(), battle.last_damage);
|
|
1207
|
+
battle.last_damage = @intCast(@as(u32, battle.last_damage) *% random / 255);
|
|
1208
|
+
options.calc.final(player.foe(), battle.last_damage);
|
|
1166
1209
|
}
|
|
1167
1210
|
|
|
1168
|
-
fn specialDamage(battle: anytype, player: Player, move: Move.Data,
|
|
1211
|
+
fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
|
|
1169
1212
|
const side = battle.side(player);
|
|
1170
1213
|
const foe = battle.foe(player);
|
|
1171
1214
|
|
|
1172
|
-
if (!try checkHit(battle, player, move,
|
|
1215
|
+
if (!try checkHit(battle, player, move, options)) return null;
|
|
1216
|
+
try options.chance.commit(player, .hit);
|
|
1173
1217
|
|
|
1174
1218
|
battle.last_damage = switch (side.last_selected_move) {
|
|
1175
1219
|
.SuperFang => @max(foe.stored().hp / 2, 1),
|
|
@@ -1178,10 +1222,10 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
|
|
|
1178
1222
|
.DragonRage => 40,
|
|
1179
1223
|
// GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
|
|
1180
1224
|
.Psywave => power: {
|
|
1181
|
-
const max = @intCast(
|
|
1225
|
+
const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
|
|
1182
1226
|
// GLITCH: Psywave infinite glitch loop
|
|
1183
1227
|
if (!showdown and max <= 1) return Result.Error;
|
|
1184
|
-
break :power Rolls.psywave(battle, max);
|
|
1228
|
+
break :power try Rolls.psywave(battle, player, max, options);
|
|
1185
1229
|
},
|
|
1186
1230
|
else => unreachable,
|
|
1187
1231
|
};
|
|
@@ -1189,17 +1233,17 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
|
|
|
1189
1233
|
if (battle.last_damage == 0) return if (showdown) null else Result.Error;
|
|
1190
1234
|
|
|
1191
1235
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1192
|
-
_ = try applyDamage(battle, player.foe(), player.foe(), .None,
|
|
1193
|
-
if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(),
|
|
1236
|
+
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
1237
|
+
if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), options);
|
|
1194
1238
|
|
|
1195
1239
|
return null;
|
|
1196
1240
|
}
|
|
1197
1241
|
|
|
1198
|
-
fn counterDamage(battle: anytype, player: Player, move: Move.Data,
|
|
1242
|
+
fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
|
|
1199
1243
|
const foe = battle.foe(player);
|
|
1200
1244
|
|
|
1201
1245
|
if (battle.last_damage == 0) {
|
|
1202
|
-
try log.fail(battle.active(player), .None);
|
|
1246
|
+
try options.log.fail(battle.active(player), .None);
|
|
1203
1247
|
return null;
|
|
1204
1248
|
}
|
|
1205
1249
|
|
|
@@ -1210,36 +1254,33 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
|
|
|
1210
1254
|
else
|
|
1211
1255
|
foe.last_selected_move);
|
|
1212
1256
|
|
|
1213
|
-
const used = (
|
|
1214
|
-
battle.last_moves.p1_counterable
|
|
1215
|
-
else
|
|
1216
|
-
battle.last_moves.p2_counterable) != 0;
|
|
1217
|
-
|
|
1257
|
+
const used = battle.lastMove(player.foe()).counterable != 0;
|
|
1218
1258
|
const selected = foe_last_selected_move.bp > 0 and
|
|
1219
1259
|
foe.last_selected_move != .Counter and
|
|
1220
1260
|
(foe_last_selected_move.type == .Normal or
|
|
1221
1261
|
foe_last_selected_move.type == .Fighting);
|
|
1222
1262
|
|
|
1223
1263
|
if (!used and !selected) {
|
|
1224
|
-
try log.fail(battle.active(player), .None);
|
|
1264
|
+
try options.log.fail(battle.active(player), .None);
|
|
1225
1265
|
return null;
|
|
1226
1266
|
}
|
|
1227
1267
|
|
|
1228
1268
|
if (!used or !selected) {
|
|
1229
1269
|
// GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
|
|
1230
1270
|
if (!showdown) return Result.Error;
|
|
1231
|
-
try log.fail(battle.active(player), .None);
|
|
1271
|
+
try options.log.fail(battle.active(player), .None);
|
|
1232
1272
|
return null;
|
|
1233
1273
|
}
|
|
1234
1274
|
|
|
1235
1275
|
battle.last_damage *|= 2;
|
|
1236
1276
|
|
|
1237
1277
|
// Pokémon Showdown calls checkHit before Counter
|
|
1238
|
-
if (!showdown and !try checkHit(battle, player, move,
|
|
1278
|
+
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
1279
|
+
try options.chance.commit(player, .hit);
|
|
1239
1280
|
|
|
1240
1281
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1241
|
-
_ = try applyDamage(battle, player.foe(), player.foe(), .None,
|
|
1242
|
-
if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(),
|
|
1282
|
+
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
1283
|
+
if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), options);
|
|
1243
1284
|
return null;
|
|
1244
1285
|
}
|
|
1245
1286
|
|
|
@@ -1248,7 +1289,7 @@ fn applyDamage(
|
|
|
1248
1289
|
target_player: Player,
|
|
1249
1290
|
sub_player: Player,
|
|
1250
1291
|
reason: Damage,
|
|
1251
|
-
|
|
1292
|
+
options: anytype,
|
|
1252
1293
|
) !bool {
|
|
1253
1294
|
assert(showdown or battle.last_damage != 0);
|
|
1254
1295
|
|
|
@@ -1263,19 +1304,20 @@ fn applyDamage(
|
|
|
1263
1304
|
subbed.active.volatiles.substitute = 0;
|
|
1264
1305
|
subbed.active.volatiles.Substitute = false;
|
|
1265
1306
|
// battle.last_damage is not updated with the amount of HP the Substitute had
|
|
1266
|
-
try log.end(battle.active(sub_player), .Substitute);
|
|
1307
|
+
try options.log.end(battle.active(sub_player), .Substitute);
|
|
1308
|
+
options.calc.capped(target_player);
|
|
1267
1309
|
return true;
|
|
1268
1310
|
} else {
|
|
1269
1311
|
// Safe to truncate since less than subbed.volatiles.substitute which is a u8
|
|
1270
|
-
subbed.active.volatiles.substitute -= @intCast(
|
|
1271
|
-
try log.activate(battle.active(sub_player), .Substitute);
|
|
1312
|
+
subbed.active.volatiles.substitute -= @intCast(battle.last_damage);
|
|
1313
|
+
try options.log.activate(battle.active(sub_player), .Substitute);
|
|
1272
1314
|
return false;
|
|
1273
1315
|
}
|
|
1274
1316
|
}
|
|
1275
1317
|
|
|
1276
1318
|
if (battle.last_damage > target.stored().hp) battle.last_damage = target.stored().hp;
|
|
1277
1319
|
target.stored().hp -= battle.last_damage;
|
|
1278
|
-
try log.damage(battle.active(target_player), target.stored(), reason);
|
|
1320
|
+
try options.log.damage(battle.active(target_player), target.stored(), reason);
|
|
1279
1321
|
return false;
|
|
1280
1322
|
}
|
|
1281
1323
|
|
|
@@ -1286,22 +1328,24 @@ fn mirrorMove(
|
|
|
1286
1328
|
rewrap: bool,
|
|
1287
1329
|
auto: bool,
|
|
1288
1330
|
residual: *bool,
|
|
1289
|
-
|
|
1290
|
-
) !?Result {
|
|
1331
|
+
options: anytype,
|
|
1332
|
+
) anyerror!?Result {
|
|
1291
1333
|
var side = battle.side(player);
|
|
1292
1334
|
const foe = battle.foe(player);
|
|
1293
1335
|
|
|
1294
1336
|
side.last_selected_move = foe.last_used_move;
|
|
1295
1337
|
|
|
1296
1338
|
if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
|
|
1297
|
-
try log.fail(battle.active(player), .None);
|
|
1339
|
+
try options.log.fail(battle.active(player), .None);
|
|
1298
1340
|
return null;
|
|
1299
1341
|
} else if (!showdown or foe.last_used_move != .Struggle) {
|
|
1300
1342
|
incrementPP(side, mslot);
|
|
1301
1343
|
}
|
|
1302
1344
|
|
|
1303
|
-
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual,
|
|
1304
|
-
|
|
1345
|
+
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
|
|
1346
|
+
return null;
|
|
1347
|
+
}
|
|
1348
|
+
return doMove(battle, player, mslot, rewrap, auto, residual, options);
|
|
1305
1349
|
}
|
|
1306
1350
|
|
|
1307
1351
|
fn metronome(
|
|
@@ -1311,35 +1355,48 @@ fn metronome(
|
|
|
1311
1355
|
rewrap: bool,
|
|
1312
1356
|
auto: bool,
|
|
1313
1357
|
residual: *bool,
|
|
1314
|
-
|
|
1315
|
-
) !?Result {
|
|
1358
|
+
options: anytype,
|
|
1359
|
+
) anyerror!?Result {
|
|
1316
1360
|
var side = battle.side(player);
|
|
1317
1361
|
|
|
1318
|
-
side.last_selected_move = Rolls.metronome(battle);
|
|
1362
|
+
side.last_selected_move = try Rolls.metronome(battle, player, options);
|
|
1319
1363
|
incrementPP(side, mslot);
|
|
1320
1364
|
|
|
1321
|
-
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual,
|
|
1322
|
-
|
|
1365
|
+
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
|
|
1366
|
+
return null;
|
|
1367
|
+
}
|
|
1368
|
+
return doMove(battle, player, mslot, rewrap, auto, residual, options);
|
|
1323
1369
|
}
|
|
1324
1370
|
|
|
1325
|
-
fn checkHit(battle: anytype, player: Player, move: Move.Data,
|
|
1371
|
+
fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !bool {
|
|
1326
1372
|
var immune = false;
|
|
1327
1373
|
var mist = false;
|
|
1328
|
-
|
|
1374
|
+
|
|
1375
|
+
if (moveHit(battle, player, move, &immune, &mist, options)) return true;
|
|
1376
|
+
|
|
1329
1377
|
assert(!immune);
|
|
1330
1378
|
if (mist) {
|
|
1331
1379
|
assert(!showdown);
|
|
1332
1380
|
const foe_ident = battle.active(player.foe());
|
|
1333
|
-
try log.activate(foe_ident, .Mist);
|
|
1334
|
-
try log.fail(foe_ident, .None);
|
|
1381
|
+
try options.log.activate(foe_ident, .Mist);
|
|
1382
|
+
try options.log.fail(foe_ident, .None);
|
|
1335
1383
|
} else {
|
|
1336
|
-
try
|
|
1337
|
-
try log.
|
|
1384
|
+
try options.chance.commit(player, .miss);
|
|
1385
|
+
try options.log.lastmiss();
|
|
1386
|
+
try options.log.miss(battle.active(player));
|
|
1338
1387
|
}
|
|
1388
|
+
|
|
1339
1389
|
return false;
|
|
1340
1390
|
}
|
|
1341
1391
|
|
|
1342
|
-
fn moveHit(
|
|
1392
|
+
fn moveHit(
|
|
1393
|
+
battle: anytype,
|
|
1394
|
+
player: Player,
|
|
1395
|
+
move: Move.Data,
|
|
1396
|
+
immune: *bool,
|
|
1397
|
+
mist: *bool,
|
|
1398
|
+
options: anytype,
|
|
1399
|
+
) bool {
|
|
1343
1400
|
var side = battle.side(player);
|
|
1344
1401
|
const foe = battle.foe(player);
|
|
1345
1402
|
|
|
@@ -1378,10 +1435,10 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
|
|
|
1378
1435
|
const overwritten = overwrite and state > 0;
|
|
1379
1436
|
assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
|
|
1380
1437
|
|
|
1381
|
-
var accuracy = if (overwritten) state else @as(u16,
|
|
1382
|
-
var boost = BOOSTS[@
|
|
1438
|
+
var accuracy = if (overwritten) state else @as(u16, move.accuracy);
|
|
1439
|
+
var boost = BOOSTS[@as(u4, @intCast(@as(i8, side.active.boosts.accuracy) + 6))];
|
|
1383
1440
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1384
|
-
boost = BOOSTS[@
|
|
1441
|
+
boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
|
|
1385
1442
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1386
1443
|
accuracy = @min(255, @max(1, accuracy));
|
|
1387
1444
|
|
|
@@ -1390,7 +1447,7 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
|
|
|
1390
1447
|
if (overwrite) side.active.volatiles.state = accuracy;
|
|
1391
1448
|
|
|
1392
1449
|
// GLITCH: max accuracy is 255 so 1/256 chance of miss
|
|
1393
|
-
break :miss !Rolls.hit(battle, accuracy);
|
|
1450
|
+
break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
|
|
1394
1451
|
};
|
|
1395
1452
|
|
|
1396
1453
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1400,7 +1457,7 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
|
|
|
1400
1457
|
}
|
|
1401
1458
|
|
|
1402
1459
|
if (!miss) return true;
|
|
1403
|
-
battle.last_damage = 0;
|
|
1460
|
+
if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
|
|
1404
1461
|
side.active.volatiles.Binding = false;
|
|
1405
1462
|
return false;
|
|
1406
1463
|
}
|
|
@@ -1408,8 +1465,8 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
|
|
|
1408
1465
|
fn checkFaint(
|
|
1409
1466
|
battle: anytype,
|
|
1410
1467
|
player: Player,
|
|
1411
|
-
|
|
1412
|
-
) @TypeOf(log).Error!?Result {
|
|
1468
|
+
options: anytype,
|
|
1469
|
+
) @TypeOf(options.log).Error!?Result {
|
|
1413
1470
|
const side = battle.side(player);
|
|
1414
1471
|
if (side.stored().hp > 0) return null;
|
|
1415
1472
|
|
|
@@ -1421,20 +1478,20 @@ fn checkFaint(
|
|
|
1421
1478
|
const tie = player_out and foe_out;
|
|
1422
1479
|
const more = tie or player_out or foe_out;
|
|
1423
1480
|
|
|
1424
|
-
if (try faint(battle, player,
|
|
1425
|
-
if (foe_fainted) if (try faint(battle, player.foe(),
|
|
1481
|
+
if (try faint(battle, player, !(more or foe_fainted), options)) |r| return r;
|
|
1482
|
+
if (foe_fainted) if (try faint(battle, player.foe(), !more, options)) |r| return r;
|
|
1426
1483
|
|
|
1427
1484
|
assert(!side.active.volatiles.MultiHit);
|
|
1428
1485
|
assert(!foe.active.volatiles.MultiHit);
|
|
1429
1486
|
|
|
1430
1487
|
if (tie) {
|
|
1431
|
-
try log.tie();
|
|
1488
|
+
try options.log.tie();
|
|
1432
1489
|
return Result.Tie;
|
|
1433
1490
|
} else if (player_out) {
|
|
1434
|
-
try log.win(player.foe());
|
|
1491
|
+
try options.log.win(player.foe());
|
|
1435
1492
|
return if (player == .P1) Result.Lose else Result.Win;
|
|
1436
1493
|
} else if (foe_out) {
|
|
1437
|
-
try log.win(player);
|
|
1494
|
+
try options.log.win(player);
|
|
1438
1495
|
return if (player == .P1) Result.Win else Result.Lose;
|
|
1439
1496
|
}
|
|
1440
1497
|
|
|
@@ -1443,7 +1500,7 @@ fn checkFaint(
|
|
|
1443
1500
|
return Result{ .p1 = foe_choice, .p2 = .Switch };
|
|
1444
1501
|
}
|
|
1445
1502
|
|
|
1446
|
-
fn faint(battle: anytype, player: Player,
|
|
1503
|
+
fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
|
|
1447
1504
|
var side = battle.side(player);
|
|
1448
1505
|
var foe = battle.foe(player);
|
|
1449
1506
|
assert(side.stored().hp == 0);
|
|
@@ -1476,32 +1533,31 @@ fn faint(battle: anytype, player: Player, log: anytype, done: bool) !?Result {
|
|
|
1476
1533
|
side.stored().stats.spe;
|
|
1477
1534
|
}
|
|
1478
1535
|
|
|
1479
|
-
try log.faint(battle.active(player), done);
|
|
1536
|
+
try options.log.faint(battle.active(player), done);
|
|
1537
|
+
options.calc.capped(player);
|
|
1480
1538
|
return null;
|
|
1481
1539
|
}
|
|
1482
1540
|
|
|
1483
|
-
fn handleResidual(battle: anytype, player: Player,
|
|
1541
|
+
fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
|
|
1484
1542
|
var side = battle.side(player);
|
|
1485
1543
|
var stored = side.stored();
|
|
1486
1544
|
const ident = battle.active(player);
|
|
1487
1545
|
var volatiles = &side.active.volatiles;
|
|
1488
1546
|
|
|
1489
1547
|
const brn = Status.is(stored.status, .BRN);
|
|
1490
|
-
if (brn or Status.is(stored.status, .PSN))
|
|
1548
|
+
if (brn or Status.is(stored.status, .PSN)) {
|
|
1491
1549
|
var damage = @max(stored.stats.hp / 16, 1);
|
|
1492
|
-
|
|
1493
1550
|
if (volatiles.Toxic) {
|
|
1494
1551
|
volatiles.toxic += 1;
|
|
1495
1552
|
damage *= volatiles.toxic;
|
|
1496
1553
|
}
|
|
1554
|
+
damage = @min(damage, stored.hp);
|
|
1497
1555
|
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
// Pokémon Showdown uses damageOf here but its not relevant in Generation I
|
|
1504
|
-
try log.damage(ident, stored, if (brn) Damage.Burn else Damage.Poison);
|
|
1556
|
+
if (!showdown or damage > 0) {
|
|
1557
|
+
stored.hp -= damage;
|
|
1558
|
+
// Pokémon Showdown uses damageOf here but its not relevant in Generation I
|
|
1559
|
+
try options.log.damage(ident, stored, if (brn) Damage.Burn else Damage.Poison);
|
|
1560
|
+
}
|
|
1505
1561
|
}
|
|
1506
1562
|
|
|
1507
1563
|
if (volatiles.LeechSeed) {
|
|
@@ -1526,42 +1582,42 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
|
|
|
1526
1582
|
stored.hp -= amount;
|
|
1527
1583
|
|
|
1528
1584
|
// As above, Pokémon Showdown uses damageOf but its not relevant
|
|
1529
|
-
if (amount > 0) try log.damage(ident, stored, .LeechSeed);
|
|
1585
|
+
if (amount > 0) try options.log.damage(ident, stored, .LeechSeed);
|
|
1530
1586
|
|
|
1531
1587
|
const before = foe_stored.hp;
|
|
1532
1588
|
// Uncapped damage is added back to the foe
|
|
1533
1589
|
foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
|
|
1534
1590
|
// Pokémon Showdown uses the less specific heal here instead of drain... because reasons?
|
|
1535
|
-
if (foe_stored.hp > before) try log.heal(foe_ident, foe_stored, .Silent);
|
|
1591
|
+
if (foe_stored.hp > before) try options.log.heal(foe_ident, foe_stored, .Silent);
|
|
1536
1592
|
}
|
|
1537
1593
|
}
|
|
1538
1594
|
|
|
1539
|
-
fn endTurn(battle: anytype,
|
|
1595
|
+
fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result {
|
|
1540
1596
|
assert(!battle.side(.P1).active.volatiles.MultiHit);
|
|
1541
1597
|
assert(!battle.side(.P2).active.volatiles.MultiHit);
|
|
1542
1598
|
|
|
1543
1599
|
battle.turn += 1;
|
|
1544
1600
|
|
|
1545
|
-
if (showdown and options.ebc and checkEBC(battle)) {
|
|
1546
|
-
try log.tie();
|
|
1601
|
+
if (showdown and pkmn.options.ebc and checkEBC(battle)) {
|
|
1602
|
+
try options.log.tie();
|
|
1547
1603
|
return Result.Tie;
|
|
1548
1604
|
}
|
|
1549
1605
|
|
|
1550
1606
|
if (showdown and battle.turn >= 1000) {
|
|
1551
|
-
try log.tie();
|
|
1607
|
+
try options.log.tie();
|
|
1552
1608
|
return Result.Tie;
|
|
1553
1609
|
} else if (battle.turn >= 65535) {
|
|
1554
1610
|
return Result.Error;
|
|
1555
1611
|
}
|
|
1556
1612
|
|
|
1557
|
-
try log.turn(battle.turn);
|
|
1613
|
+
try options.log.turn(battle.turn);
|
|
1558
1614
|
|
|
1559
1615
|
return Result.Default;
|
|
1560
1616
|
}
|
|
1561
1617
|
|
|
1562
1618
|
fn checkEBC(battle: anytype) bool {
|
|
1563
1619
|
for (battle.sides, 0..) |side, i| {
|
|
1564
|
-
const foe = battle.sides[~@
|
|
1620
|
+
const foe = battle.sides[~@as(u1, @intCast(i))];
|
|
1565
1621
|
|
|
1566
1622
|
var foe_all_ghosts = true;
|
|
1567
1623
|
var foe_all_transform = true;
|
|
@@ -1613,7 +1669,7 @@ fn checkEBC(battle: anytype) bool {
|
|
|
1613
1669
|
return false;
|
|
1614
1670
|
}
|
|
1615
1671
|
|
|
1616
|
-
fn disabled(side: *Side, ident: ID,
|
|
1672
|
+
fn disabled(side: *Side, ident: ID, options: anytype) !bool {
|
|
1617
1673
|
if (side.active.volatiles.disabled_move != 0) {
|
|
1618
1674
|
// A Pokémon that transforms after being disabled may end up with less move slots
|
|
1619
1675
|
const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
|
|
@@ -1624,28 +1680,28 @@ fn disabled(side: *Side, ident: ID, log: anytype) !bool {
|
|
|
1624
1680
|
side.last_selected_move;
|
|
1625
1681
|
if (m != .None and m == last) {
|
|
1626
1682
|
side.active.volatiles.Charging = false;
|
|
1627
|
-
try log.disabled(ident, last);
|
|
1683
|
+
try options.log.disabled(ident, last);
|
|
1628
1684
|
return true;
|
|
1629
1685
|
}
|
|
1630
1686
|
}
|
|
1631
1687
|
return false;
|
|
1632
1688
|
}
|
|
1633
1689
|
|
|
1634
|
-
inline fn buildRage(battle: anytype, who: Player,
|
|
1690
|
+
inline fn buildRage(battle: anytype, who: Player, options: anytype) !void {
|
|
1635
1691
|
const side = battle.side(who);
|
|
1636
1692
|
if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
|
|
1637
|
-
try Effects.boost(battle, who, Move.get(.Rage),
|
|
1693
|
+
try Effects.boost(battle, who, Move.get(.Rage), options);
|
|
1638
1694
|
}
|
|
1639
1695
|
}
|
|
1640
1696
|
|
|
1641
|
-
fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
|
|
1697
|
+
fn handleThrashing(battle: anytype, active: *ActivePokemon, player: Player, options: anytype) bool {
|
|
1642
1698
|
var volatiles = &active.volatiles;
|
|
1643
1699
|
assert(volatiles.Thrashing);
|
|
1644
1700
|
if (volatiles.attacks > 0) return false;
|
|
1645
1701
|
|
|
1646
1702
|
volatiles.Thrashing = false;
|
|
1647
1703
|
volatiles.Confusion = true;
|
|
1648
|
-
volatiles.confusion = Rolls.confusionDuration(battle);
|
|
1704
|
+
volatiles.confusion = Rolls.confusionDuration(battle, player, true, options);
|
|
1649
1705
|
return true;
|
|
1650
1706
|
}
|
|
1651
1707
|
|
|
@@ -1655,42 +1711,42 @@ inline fn onBegin(
|
|
|
1655
1711
|
move: Move.Data,
|
|
1656
1712
|
mslot: u8,
|
|
1657
1713
|
residual: *bool,
|
|
1658
|
-
|
|
1714
|
+
options: anytype,
|
|
1659
1715
|
) !void {
|
|
1660
1716
|
assert(move.effect.onBegin());
|
|
1661
1717
|
return switch (move.effect) {
|
|
1662
|
-
.Confusion => Effects.confusion(battle, player, move,
|
|
1663
|
-
.Conversion => Effects.conversion(battle, player,
|
|
1664
|
-
.FocusEnergy => Effects.focusEnergy(battle, player,
|
|
1665
|
-
.Haze => Effects.haze(battle, player,
|
|
1666
|
-
.Heal => Effects.heal(battle, player,
|
|
1667
|
-
.LeechSeed => Effects.leechSeed(battle, player, move,
|
|
1668
|
-
.LightScreen => Effects.lightScreen(battle, player,
|
|
1669
|
-
.Mimic => Effects.mimic(battle, player, move, mslot,
|
|
1670
|
-
.Mist => Effects.mist(battle, player,
|
|
1671
|
-
.Paralyze => Effects.paralyze(battle, player, move,
|
|
1672
|
-
.Poison => Effects.poison(battle, player, move,
|
|
1673
|
-
.Reflect => Effects.reflect(battle, player,
|
|
1674
|
-
.Splash => Effects.splash(battle, player,
|
|
1675
|
-
.Substitute => Effects.substitute(battle, player, residual,
|
|
1676
|
-
.SwitchAndTeleport => Effects.switchAndTeleport(battle, player, move,
|
|
1677
|
-
.Transform => Effects.transform(battle, player,
|
|
1718
|
+
.Confusion => Effects.confusion(battle, player, move, options),
|
|
1719
|
+
.Conversion => Effects.conversion(battle, player, options),
|
|
1720
|
+
.FocusEnergy => Effects.focusEnergy(battle, player, options),
|
|
1721
|
+
.Haze => Effects.haze(battle, player, options),
|
|
1722
|
+
.Heal => Effects.heal(battle, player, options),
|
|
1723
|
+
.LeechSeed => Effects.leechSeed(battle, player, move, options),
|
|
1724
|
+
.LightScreen => Effects.lightScreen(battle, player, options),
|
|
1725
|
+
.Mimic => Effects.mimic(battle, player, move, mslot, options),
|
|
1726
|
+
.Mist => Effects.mist(battle, player, options),
|
|
1727
|
+
.Paralyze => Effects.paralyze(battle, player, move, options),
|
|
1728
|
+
.Poison => Effects.poison(battle, player, move, options),
|
|
1729
|
+
.Reflect => Effects.reflect(battle, player, options),
|
|
1730
|
+
.Splash => Effects.splash(battle, player, options),
|
|
1731
|
+
.Substitute => Effects.substitute(battle, player, residual, options),
|
|
1732
|
+
.SwitchAndTeleport => Effects.switchAndTeleport(battle, player, move, options),
|
|
1733
|
+
.Transform => Effects.transform(battle, player, options),
|
|
1678
1734
|
else => unreachable,
|
|
1679
1735
|
};
|
|
1680
1736
|
}
|
|
1681
1737
|
|
|
1682
|
-
inline fn onEnd(battle: anytype, player: Player, move: Move.Data,
|
|
1738
|
+
inline fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1683
1739
|
assert(move.effect.onEnd());
|
|
1684
1740
|
return switch (move.effect) {
|
|
1685
|
-
.Bide => Effects.bide(battle, player,
|
|
1741
|
+
.Bide => Effects.bide(battle, player, options),
|
|
1686
1742
|
// zig fmt: off
|
|
1687
1743
|
.AttackUp1, .AttackUp2, .DefenseUp1, .DefenseUp2,
|
|
1688
1744
|
.EvasionUp1, .SpecialUp1, .SpecialUp2, .SpeedUp2 =>
|
|
1689
|
-
Effects.boost(battle, player, move,
|
|
1745
|
+
Effects.boost(battle, player, move, options),
|
|
1690
1746
|
.AccuracyDown1, .AttackDown1, .DefenseDown1, .DefenseDown2, .SpeedDown1 =>
|
|
1691
|
-
Effects.unboost(battle, player, move,
|
|
1747
|
+
Effects.unboost(battle, player, move, options),
|
|
1692
1748
|
// zig fmt: on
|
|
1693
|
-
.Sleep => Effects.sleep(battle, player, move,
|
|
1749
|
+
.Sleep => Effects.sleep(battle, player, move, options),
|
|
1694
1750
|
else => unreachable,
|
|
1695
1751
|
};
|
|
1696
1752
|
}
|
|
@@ -1700,51 +1756,51 @@ inline fn alwaysHappens(
|
|
|
1700
1756
|
player: Player,
|
|
1701
1757
|
move: Move.Data,
|
|
1702
1758
|
residual: *bool,
|
|
1703
|
-
|
|
1759
|
+
options: anytype,
|
|
1704
1760
|
) !void {
|
|
1705
1761
|
assert(move.effect.alwaysHappens());
|
|
1706
1762
|
return switch (move.effect) {
|
|
1707
|
-
.DrainHP, .DreamEater => Effects.drainHP(battle, player,
|
|
1763
|
+
.DrainHP, .DreamEater => Effects.drainHP(battle, player, options),
|
|
1708
1764
|
.Explode => Effects.explode(battle, player),
|
|
1709
|
-
.PayDay => Effects.payDay(battle, player,
|
|
1765
|
+
.PayDay => Effects.payDay(battle, player, options),
|
|
1710
1766
|
.Rage => Effects.rage(battle, player),
|
|
1711
|
-
.Recoil => Effects.recoil(battle, player, residual,
|
|
1767
|
+
.Recoil => Effects.recoil(battle, player, residual, options),
|
|
1712
1768
|
.JumpKick, .Binding => {},
|
|
1713
1769
|
else => unreachable,
|
|
1714
1770
|
};
|
|
1715
1771
|
}
|
|
1716
1772
|
|
|
1717
|
-
inline fn moveEffect(battle: anytype, player: Player, move: Move.Data,
|
|
1773
|
+
inline fn moveEffect(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1718
1774
|
return switch (move.effect) {
|
|
1719
|
-
.BurnChance1, .BurnChance2 => Effects.burnChance(battle, player, move,
|
|
1720
|
-
.ConfusionChance => Effects.confusion(battle, player, move,
|
|
1721
|
-
.FlinchChance1, .FlinchChance2 => Effects.flinchChance(battle, player, move),
|
|
1722
|
-
.FreezeChance => Effects.freezeChance(battle, player, move,
|
|
1723
|
-
.HyperBeam => Effects.hyperBeam(battle, player,
|
|
1775
|
+
.BurnChance1, .BurnChance2 => Effects.burnChance(battle, player, move, options),
|
|
1776
|
+
.ConfusionChance => Effects.confusion(battle, player, move, options),
|
|
1777
|
+
.FlinchChance1, .FlinchChance2 => Effects.flinchChance(battle, player, move, options),
|
|
1778
|
+
.FreezeChance => Effects.freezeChance(battle, player, move, options),
|
|
1779
|
+
.HyperBeam => Effects.hyperBeam(battle, player, options),
|
|
1724
1780
|
.MultiHit, .DoubleHit, .Twineedle => unreachable,
|
|
1725
|
-
.ParalyzeChance1, .ParalyzeChance2 => Effects.paralyzeChance(battle, player, move,
|
|
1726
|
-
.PoisonChance1, .PoisonChance2 => Effects.poison(battle, player, move,
|
|
1781
|
+
.ParalyzeChance1, .ParalyzeChance2 => Effects.paralyzeChance(battle, player, move, options),
|
|
1782
|
+
.PoisonChance1, .PoisonChance2 => Effects.poison(battle, player, move, options),
|
|
1727
1783
|
// zig fmt: off
|
|
1728
1784
|
.AttackDownChance, .DefenseDownChance, .SpecialDownChance, .SpeedDownChance =>
|
|
1729
|
-
Effects.unboost(battle, player, move,
|
|
1785
|
+
Effects.unboost(battle, player, move, options),
|
|
1730
1786
|
// zig fmt: on
|
|
1731
1787
|
else => {},
|
|
1732
1788
|
};
|
|
1733
1789
|
}
|
|
1734
1790
|
|
|
1735
1791
|
pub const Effects = struct {
|
|
1736
|
-
fn bide(battle: anytype, player: Player,
|
|
1792
|
+
fn bide(battle: anytype, player: Player, options: anytype) !void {
|
|
1737
1793
|
var side = battle.side(player);
|
|
1738
1794
|
|
|
1739
1795
|
side.active.volatiles.Bide = true;
|
|
1740
1796
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1741
1797
|
side.active.volatiles.state = 0;
|
|
1742
|
-
side.active.volatiles.attacks = Rolls.
|
|
1798
|
+
side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
1743
1799
|
|
|
1744
|
-
try log.start(battle.active(player), .Bide);
|
|
1800
|
+
try options.log.start(battle.active(player), .Bide);
|
|
1745
1801
|
}
|
|
1746
1802
|
|
|
1747
|
-
fn burnChance(battle: anytype, player: Player, move: Move.Data,
|
|
1803
|
+
fn burnChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1748
1804
|
var foe = battle.foe(player);
|
|
1749
1805
|
var foe_stored = foe.stored();
|
|
1750
1806
|
|
|
@@ -1755,83 +1811,97 @@ pub const Effects = struct {
|
|
|
1755
1811
|
// GLITCH: Freeze top move selection desync can occur if thawed Pokémon is slower
|
|
1756
1812
|
if (Status.is(foe_stored.status, .FRZ)) {
|
|
1757
1813
|
assert(move.type == .Fire);
|
|
1758
|
-
try log.curestatus(
|
|
1814
|
+
try options.log.curestatus(
|
|
1815
|
+
battle.active(player.foe()),
|
|
1816
|
+
foe_stored.status,
|
|
1817
|
+
.Message,
|
|
1818
|
+
);
|
|
1759
1819
|
foe_stored.status = 0;
|
|
1760
1820
|
}
|
|
1761
1821
|
return;
|
|
1762
1822
|
}
|
|
1763
1823
|
|
|
1764
1824
|
if (foe.active.types.includes(move.type)) return;
|
|
1765
|
-
if (!Rolls.secondaryChance(battle, move.effect == .BurnChance1))
|
|
1825
|
+
if (!try Rolls.secondaryChance(battle, player, move.effect == .BurnChance1, options)) {
|
|
1826
|
+
return;
|
|
1827
|
+
}
|
|
1766
1828
|
|
|
1767
1829
|
foe_stored.status = Status.init(.BRN);
|
|
1768
1830
|
foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
|
|
1769
1831
|
|
|
1770
|
-
try log.status(battle.active(player.foe()), foe_stored.status, .None);
|
|
1832
|
+
try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
|
|
1771
1833
|
}
|
|
1772
1834
|
|
|
1773
|
-
fn charge(battle: anytype, player: Player,
|
|
1835
|
+
fn charge(battle: anytype, player: Player, options: anytype) !void {
|
|
1774
1836
|
var side = battle.side(player);
|
|
1775
1837
|
var volatiles = &side.active.volatiles;
|
|
1776
1838
|
|
|
1777
1839
|
volatiles.Charging = true;
|
|
1778
1840
|
const move = side.last_selected_move;
|
|
1779
1841
|
if (move == .Fly or move == .Dig) volatiles.Invulnerable = true;
|
|
1780
|
-
try log.laststill();
|
|
1781
|
-
try log.prepare(battle.active(player), move);
|
|
1842
|
+
try options.log.laststill();
|
|
1843
|
+
try options.log.prepare(battle.active(player), move);
|
|
1782
1844
|
}
|
|
1783
1845
|
|
|
1784
|
-
fn confusion(battle: anytype, player: Player, move: Move.Data,
|
|
1846
|
+
fn confusion(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1785
1847
|
var foe = battle.foe(player);
|
|
1786
1848
|
const sub = foe.active.volatiles.Substitute;
|
|
1787
1849
|
|
|
1788
1850
|
if (move.effect == .ConfusionChance) {
|
|
1789
|
-
if (!Rolls.confusionChance(battle)) return;
|
|
1851
|
+
if (!try Rolls.confusionChance(battle, player, options)) return;
|
|
1790
1852
|
} else {
|
|
1791
1853
|
if (showdown) {
|
|
1792
|
-
if (!try checkHit(battle, player, move,
|
|
1854
|
+
if (!try checkHit(battle, player, move, options)) {
|
|
1793
1855
|
return;
|
|
1794
1856
|
} else if (sub) {
|
|
1795
|
-
return log.fail(battle.active(player.foe()), .None);
|
|
1857
|
+
return options.log.fail(battle.active(player.foe()), .None);
|
|
1796
1858
|
}
|
|
1797
1859
|
} else {
|
|
1798
1860
|
if (sub) {
|
|
1799
|
-
return log.fail(battle.active(player.foe()), .None);
|
|
1800
|
-
} else if (!try checkHit(battle, player, move,
|
|
1861
|
+
return options.log.fail(battle.active(player.foe()), .None);
|
|
1862
|
+
} else if (!try checkHit(battle, player, move, options)) {
|
|
1801
1863
|
return;
|
|
1802
1864
|
}
|
|
1803
1865
|
}
|
|
1866
|
+
try options.chance.commit(player, .hit);
|
|
1804
1867
|
}
|
|
1805
1868
|
|
|
1806
1869
|
if (foe.active.volatiles.Confusion) return;
|
|
1807
1870
|
foe.active.volatiles.Confusion = true;
|
|
1808
|
-
foe.active.volatiles.confusion = Rolls.confusionDuration(battle);
|
|
1871
|
+
foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
|
|
1809
1872
|
|
|
1810
|
-
try log.start(battle.active(player.foe()), .Confusion);
|
|
1873
|
+
try options.log.start(battle.active(player.foe()), .Confusion);
|
|
1811
1874
|
}
|
|
1812
1875
|
|
|
1813
|
-
fn conversion(battle: anytype, player: Player,
|
|
1876
|
+
fn conversion(battle: anytype, player: Player, options: anytype) !void {
|
|
1814
1877
|
const foe = battle.foe(player);
|
|
1815
1878
|
|
|
1816
1879
|
if (foe.active.volatiles.Invulnerable) {
|
|
1817
|
-
try log.lastmiss();
|
|
1818
|
-
return log.miss(battle.active(player));
|
|
1880
|
+
try options.log.lastmiss();
|
|
1881
|
+
return options.log.miss(battle.active(player));
|
|
1819
1882
|
}
|
|
1820
1883
|
|
|
1821
1884
|
battle.side(player).active.types = foe.active.types;
|
|
1822
|
-
return log.typechange(
|
|
1885
|
+
return options.log.typechange(
|
|
1886
|
+
battle.active(player),
|
|
1887
|
+
foe.active.types,
|
|
1888
|
+
battle.active(player.foe()),
|
|
1889
|
+
);
|
|
1823
1890
|
}
|
|
1824
1891
|
|
|
1825
|
-
fn disable(battle: anytype, player: Player, move: Move.Data,
|
|
1892
|
+
fn disable(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
|
|
1826
1893
|
var foe = battle.foe(player);
|
|
1827
1894
|
var volatiles = &foe.active.volatiles;
|
|
1828
1895
|
const foe_ident = battle.active(player.foe());
|
|
1829
1896
|
|
|
1830
1897
|
// Pokémon Showdown handles hit/miss earlier in doMove
|
|
1831
|
-
if (!showdown
|
|
1898
|
+
if (!showdown) {
|
|
1899
|
+
if (!try checkHit(battle, player, move, options)) return null;
|
|
1900
|
+
try options.chance.commit(player, .hit);
|
|
1901
|
+
}
|
|
1832
1902
|
|
|
1833
1903
|
if (volatiles.disabled_move != 0) {
|
|
1834
|
-
try log.fail(foe_ident, .None);
|
|
1904
|
+
try options.log.fail(foe_ident, .None);
|
|
1835
1905
|
return null;
|
|
1836
1906
|
}
|
|
1837
1907
|
|
|
@@ -1847,7 +1917,7 @@ pub const Effects = struct {
|
|
|
1847
1917
|
// Technically this is still considered simply a "miss" on the cartridge,
|
|
1848
1918
|
// but diverging from Pokémon Showdown here would mostly just be pedantic
|
|
1849
1919
|
if (n == 0) {
|
|
1850
|
-
try log.fail(foe_ident, .None);
|
|
1920
|
+
try options.log.fail(foe_ident, .None);
|
|
1851
1921
|
return null;
|
|
1852
1922
|
} else if (err) {
|
|
1853
1923
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
@@ -1855,14 +1925,16 @@ pub const Effects = struct {
|
|
|
1855
1925
|
return Result.Error;
|
|
1856
1926
|
}
|
|
1857
1927
|
|
|
1858
|
-
volatiles.disabled_move =
|
|
1859
|
-
|
|
1928
|
+
volatiles.disabled_move =
|
|
1929
|
+
@intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
|
|
1930
|
+
volatiles.disabled_duration = Rolls.disableDuration(battle, player, options);
|
|
1860
1931
|
|
|
1861
|
-
|
|
1932
|
+
const id = foe.active.move(volatiles.disabled_move).id;
|
|
1933
|
+
try options.log.startEffect(foe_ident, .Disable, id);
|
|
1862
1934
|
return null;
|
|
1863
1935
|
}
|
|
1864
1936
|
|
|
1865
|
-
fn drainHP(battle: anytype, player: Player,
|
|
1937
|
+
fn drainHP(battle: anytype, player: Player, options: anytype) !void {
|
|
1866
1938
|
var side = battle.side(player);
|
|
1867
1939
|
var stored = side.stored();
|
|
1868
1940
|
|
|
@@ -1877,7 +1949,7 @@ pub const Effects = struct {
|
|
|
1877
1949
|
if (stored.hp == stored.stats.hp) return;
|
|
1878
1950
|
stored.hp = @min(stored.stats.hp, stored.hp + drain);
|
|
1879
1951
|
|
|
1880
|
-
try log.drain(battle.active(player), stored, battle.active(player.foe()));
|
|
1952
|
+
try options.log.drain(battle.active(player), stored, battle.active(player.foe()));
|
|
1881
1953
|
}
|
|
1882
1954
|
|
|
1883
1955
|
fn explode(battle: anytype, player: Player) !void {
|
|
@@ -1891,26 +1963,28 @@ pub const Effects = struct {
|
|
|
1891
1963
|
side.active.volatiles.LeechSeed = false;
|
|
1892
1964
|
}
|
|
1893
1965
|
|
|
1894
|
-
fn flinchChance(battle: anytype, player: Player, move: Move.Data) void {
|
|
1966
|
+
fn flinchChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1895
1967
|
var volatiles = &battle.foe(player).active.volatiles;
|
|
1896
1968
|
|
|
1897
1969
|
if (volatiles.Substitute) return;
|
|
1898
|
-
if (!Rolls.secondaryChance(battle, move.effect == .FlinchChance1))
|
|
1970
|
+
if (!try Rolls.secondaryChance(battle, player, move.effect == .FlinchChance1, options)) {
|
|
1971
|
+
return;
|
|
1972
|
+
}
|
|
1899
1973
|
|
|
1900
1974
|
volatiles.Flinch = true;
|
|
1901
1975
|
volatiles.Recharging = false;
|
|
1902
1976
|
}
|
|
1903
1977
|
|
|
1904
|
-
fn focusEnergy(battle: anytype, player: Player,
|
|
1978
|
+
fn focusEnergy(battle: anytype, player: Player, options: anytype) !void {
|
|
1905
1979
|
var side = battle.side(player);
|
|
1906
1980
|
|
|
1907
1981
|
if (side.active.volatiles.FocusEnergy) return;
|
|
1908
1982
|
side.active.volatiles.FocusEnergy = true;
|
|
1909
1983
|
|
|
1910
|
-
try log.start(battle.active(player), .FocusEnergy);
|
|
1984
|
+
try options.log.start(battle.active(player), .FocusEnergy);
|
|
1911
1985
|
}
|
|
1912
1986
|
|
|
1913
|
-
fn freezeChance(battle: anytype, player: Player, move: Move.Data,
|
|
1987
|
+
fn freezeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1914
1988
|
var foe = battle.foe(player);
|
|
1915
1989
|
var foe_stored = foe.stored();
|
|
1916
1990
|
const foe_ident = battle.active(player.foe());
|
|
@@ -1922,17 +1996,18 @@ pub const Effects = struct {
|
|
|
1922
1996
|
}
|
|
1923
1997
|
|
|
1924
1998
|
if (foe.active.types.includes(move.type)) return;
|
|
1925
|
-
if (!Rolls.secondaryChance(battle, true)) return;
|
|
1999
|
+
if (!try Rolls.secondaryChance(battle, player, true, options)) return;
|
|
1926
2000
|
// Freeze Clause Mod
|
|
1927
2001
|
if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
1928
2002
|
|
|
1929
2003
|
foe_stored.status = Status.init(.FRZ);
|
|
1930
2004
|
// GLITCH: Hyper Beam recharging status is not cleared
|
|
1931
2005
|
|
|
1932
|
-
try log.status(foe_ident, foe_stored.status, .None);
|
|
2006
|
+
try options.log.status(foe_ident, foe_stored.status, .None);
|
|
1933
2007
|
}
|
|
1934
2008
|
|
|
1935
|
-
fn haze(battle: anytype, player: Player,
|
|
2009
|
+
fn haze(battle: anytype, player: Player, options: anytype) !void {
|
|
2010
|
+
var log = options.log;
|
|
1936
2011
|
var side = battle.side(player);
|
|
1937
2012
|
var foe = battle.foe(player);
|
|
1938
2013
|
|
|
@@ -1954,7 +2029,7 @@ pub const Effects = struct {
|
|
|
1954
2029
|
// Pokémon Showdown clears P1 then P2 instead of status -> side -> foe
|
|
1955
2030
|
if (showdown) {
|
|
1956
2031
|
for (&battle.sides, 0..) |*s, i| {
|
|
1957
|
-
const p = @
|
|
2032
|
+
const p: Player = @enumFromInt(i);
|
|
1958
2033
|
// Pokémon Showdown incorrectly does not prevent sleep/freeze from moving
|
|
1959
2034
|
if (p != player and Status.any(s.stored().status)) {
|
|
1960
2035
|
try log.curestatus(foe_ident, foe_stored.status, .Silent);
|
|
@@ -1963,7 +2038,7 @@ pub const Effects = struct {
|
|
|
1963
2038
|
s.stored().status = Status.init(.PSN);
|
|
1964
2039
|
try log.status(battle.active(p), s.stored().status, .None);
|
|
1965
2040
|
}
|
|
1966
|
-
try clearVolatiles(battle, p,
|
|
2041
|
+
try clearVolatiles(battle, p, p != player, options);
|
|
1967
2042
|
}
|
|
1968
2043
|
} else {
|
|
1969
2044
|
if (Status.any(foe_stored.status)) {
|
|
@@ -1973,71 +2048,76 @@ pub const Effects = struct {
|
|
|
1973
2048
|
try log.curestatus(foe_ident, foe_stored.status, .Silent);
|
|
1974
2049
|
foe_stored.status = 0;
|
|
1975
2050
|
}
|
|
1976
|
-
try clearVolatiles(battle, player,
|
|
1977
|
-
try clearVolatiles(battle, player.foe(),
|
|
2051
|
+
try clearVolatiles(battle, player, false, options);
|
|
2052
|
+
try clearVolatiles(battle, player.foe(), true, options);
|
|
1978
2053
|
}
|
|
1979
2054
|
}
|
|
1980
2055
|
|
|
1981
|
-
fn heal(battle: anytype, player: Player,
|
|
2056
|
+
fn heal(battle: anytype, player: Player, options: anytype) !void {
|
|
1982
2057
|
var side = battle.side(player);
|
|
1983
2058
|
var stored = side.stored();
|
|
1984
2059
|
const ident = battle.active(player);
|
|
1985
2060
|
|
|
1986
2061
|
// GLITCH: HP recovery move failure glitches
|
|
1987
2062
|
const delta = stored.stats.hp - stored.hp;
|
|
1988
|
-
if (delta == 0 or delta & 255 == 255
|
|
2063
|
+
if (delta == 0 or (delta & 255 == 255 and stored.hp % 256 != 0)) {
|
|
2064
|
+
return try options.log.fail(ident, .None);
|
|
2065
|
+
}
|
|
1989
2066
|
|
|
1990
2067
|
const rest = side.last_selected_move == .Rest;
|
|
1991
2068
|
if (rest) {
|
|
1992
2069
|
// Adding the sleep status runs the sleep condition handler to roll duration
|
|
1993
2070
|
if (showdown) battle.rng.advance(1);
|
|
1994
2071
|
stored.status = Status.slf(2);
|
|
1995
|
-
try log.statusFrom(ident, stored.status, Move.Rest);
|
|
2072
|
+
try options.log.statusFrom(ident, stored.status, Move.Rest);
|
|
1996
2073
|
stored.hp = stored.stats.hp;
|
|
1997
2074
|
} else {
|
|
1998
2075
|
stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
|
|
1999
2076
|
}
|
|
2000
|
-
try log.heal(ident, stored, if (rest) Heal.Silent else Heal.None);
|
|
2077
|
+
try options.log.heal(ident, stored, if (rest) Heal.Silent else Heal.None);
|
|
2001
2078
|
}
|
|
2002
2079
|
|
|
2003
|
-
fn hyperBeam(battle: anytype, player: Player,
|
|
2080
|
+
fn hyperBeam(battle: anytype, player: Player, options: anytype) !void {
|
|
2004
2081
|
battle.side(player).active.volatiles.Recharging = true;
|
|
2005
|
-
try log.mustrecharge(battle.active(player));
|
|
2082
|
+
try options.log.mustrecharge(battle.active(player));
|
|
2006
2083
|
}
|
|
2007
2084
|
|
|
2008
|
-
fn leechSeed(battle: anytype, player: Player, move: Move.Data,
|
|
2085
|
+
fn leechSeed(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
2009
2086
|
var foe = battle.foe(player);
|
|
2010
2087
|
|
|
2011
2088
|
if (showdown) {
|
|
2012
2089
|
// Invulnerability trumps type immunity on Pokémon Showdown
|
|
2013
2090
|
if (!foe.active.volatiles.Invulnerable and foe.active.types.includes(.Grass)) {
|
|
2014
|
-
return log.immune(battle.active(player.foe()), .None);
|
|
2091
|
+
return options.log.immune(battle.active(player.foe()), .None);
|
|
2015
2092
|
}
|
|
2016
|
-
if (!try checkHit(battle, player, move,
|
|
2093
|
+
if (!try checkHit(battle, player, move, options)) return;
|
|
2017
2094
|
if (foe.active.volatiles.LeechSeed) return;
|
|
2018
2095
|
} else {
|
|
2019
|
-
if (!try checkHit(battle, player, move,
|
|
2096
|
+
if (!try checkHit(battle, player, move, options)) return;
|
|
2020
2097
|
if (foe.active.types.includes(.Grass) or foe.active.volatiles.LeechSeed) {
|
|
2021
|
-
try log.lastmiss();
|
|
2022
|
-
return log.miss(battle.active(player));
|
|
2098
|
+
try options.log.lastmiss();
|
|
2099
|
+
return options.log.miss(battle.active(player));
|
|
2023
2100
|
}
|
|
2024
2101
|
}
|
|
2025
2102
|
|
|
2103
|
+
try options.chance.commit(player, .hit);
|
|
2026
2104
|
foe.active.volatiles.LeechSeed = true;
|
|
2027
2105
|
|
|
2028
|
-
try log.start(battle.active(player.foe()), .LeechSeed);
|
|
2106
|
+
try options.log.start(battle.active(player.foe()), .LeechSeed);
|
|
2029
2107
|
}
|
|
2030
2108
|
|
|
2031
|
-
fn lightScreen(battle: anytype, player: Player,
|
|
2109
|
+
fn lightScreen(battle: anytype, player: Player, options: anytype) !void {
|
|
2032
2110
|
var side = battle.side(player);
|
|
2033
2111
|
|
|
2034
|
-
if (side.active.volatiles.LightScreen)
|
|
2112
|
+
if (side.active.volatiles.LightScreen) {
|
|
2113
|
+
return options.log.fail(battle.active(player), .None);
|
|
2114
|
+
}
|
|
2035
2115
|
side.active.volatiles.LightScreen = true;
|
|
2036
2116
|
|
|
2037
|
-
try log.start(battle.active(player), .LightScreen);
|
|
2117
|
+
try options.log.start(battle.active(player), .LightScreen);
|
|
2038
2118
|
}
|
|
2039
2119
|
|
|
2040
|
-
fn mimic(battle: anytype, player: Player, move: Move.Data, mslot: u8,
|
|
2120
|
+
fn mimic(battle: anytype, player: Player, move: Move.Data, mslot: u8, options: anytype) !void {
|
|
2041
2121
|
var side = battle.side(player);
|
|
2042
2122
|
var foe = battle.foe(player);
|
|
2043
2123
|
|
|
@@ -2053,7 +2133,7 @@ pub const Effects = struct {
|
|
|
2053
2133
|
const has_mimic = has_mimic: {
|
|
2054
2134
|
for (side.active.moves, 0..) |m, i| {
|
|
2055
2135
|
if (m.id == .Mimic) {
|
|
2056
|
-
oslot = @intCast(
|
|
2136
|
+
oslot = @intCast(i + 1);
|
|
2057
2137
|
break :has_mimic true;
|
|
2058
2138
|
}
|
|
2059
2139
|
}
|
|
@@ -2061,38 +2141,43 @@ pub const Effects = struct {
|
|
|
2061
2141
|
};
|
|
2062
2142
|
if (!has_mimic) {
|
|
2063
2143
|
// Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
|
|
2064
|
-
if (!try checkHit(battle, player, move,
|
|
2065
|
-
|
|
2144
|
+
if (!try checkHit(battle, player, move, options)) return;
|
|
2145
|
+
try options.chance.commit(player, .hit);
|
|
2146
|
+
return try options.log.fail(battle.active(player.foe()), .None);
|
|
2066
2147
|
}
|
|
2067
2148
|
}
|
|
2068
|
-
if (!try checkHit(battle, player, move,
|
|
2149
|
+
if (!try checkHit(battle, player, move, options)) return;
|
|
2150
|
+
try options.chance.commit(player, .hit);
|
|
2069
2151
|
|
|
2070
|
-
const rslot = Rolls.moveSlot(battle, &foe.active.moves, 0);
|
|
2152
|
+
const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
|
|
2071
2153
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
2072
2154
|
|
|
2073
|
-
try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
|
|
2155
|
+
try options.log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
|
|
2074
2156
|
}
|
|
2075
2157
|
|
|
2076
|
-
fn mist(battle: anytype, player: Player,
|
|
2158
|
+
fn mist(battle: anytype, player: Player, options: anytype) !void {
|
|
2077
2159
|
var side = battle.side(player);
|
|
2078
2160
|
|
|
2079
2161
|
if (side.active.volatiles.Mist) return;
|
|
2080
2162
|
side.active.volatiles.Mist = true;
|
|
2081
2163
|
|
|
2082
|
-
try log.start(battle.active(player), .Mist);
|
|
2164
|
+
try options.log.start(battle.active(player), .Mist);
|
|
2083
2165
|
}
|
|
2084
2166
|
|
|
2085
|
-
fn multiHit(battle: anytype, player: Player, move: Move.Data) void {
|
|
2167
|
+
fn multiHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
2086
2168
|
var side = battle.side(player);
|
|
2087
2169
|
|
|
2088
2170
|
assert(!side.active.volatiles.MultiHit);
|
|
2089
2171
|
side.active.volatiles.MultiHit = true;
|
|
2090
2172
|
|
|
2091
|
-
side.active.volatiles.attacks =
|
|
2092
|
-
|
|
2173
|
+
side.active.volatiles.attacks = if (move.effect == .MultiHit)
|
|
2174
|
+
try Rolls.distribution(battle, .MultiHit, player, options)
|
|
2175
|
+
else
|
|
2176
|
+
2;
|
|
2093
2177
|
}
|
|
2094
2178
|
|
|
2095
|
-
fn paralyze(battle: anytype, player: Player, move: Move.Data,
|
|
2179
|
+
fn paralyze(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
2180
|
+
var log = options.log;
|
|
2096
2181
|
var foe = battle.foe(player);
|
|
2097
2182
|
var foe_stored = foe.stored();
|
|
2098
2183
|
const foe_ident = battle.active(player.foe());
|
|
@@ -2103,7 +2188,7 @@ pub const Effects = struct {
|
|
|
2103
2188
|
if (showdown) {
|
|
2104
2189
|
// Invulnerability trumps type immunity on Pokémon Showdown
|
|
2105
2190
|
if (immune and !foe.active.volatiles.Invulnerable) return log.immune(foe_ident, .None);
|
|
2106
|
-
if (!try checkHit(battle, player, move,
|
|
2191
|
+
if (!try checkHit(battle, player, move, options)) return;
|
|
2107
2192
|
}
|
|
2108
2193
|
if (Status.any(foe_stored.status)) {
|
|
2109
2194
|
return log.fail(
|
|
@@ -2113,8 +2198,9 @@ pub const Effects = struct {
|
|
|
2113
2198
|
}
|
|
2114
2199
|
if (!showdown) {
|
|
2115
2200
|
if (immune) return log.immune(foe_ident, .None);
|
|
2116
|
-
if (!try checkHit(battle, player, move,
|
|
2201
|
+
if (!try checkHit(battle, player, move, options)) return;
|
|
2117
2202
|
}
|
|
2203
|
+
try options.chance.commit(player, .hit);
|
|
2118
2204
|
|
|
2119
2205
|
foe_stored.status = Status.init(.PAR);
|
|
2120
2206
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
@@ -2122,7 +2208,7 @@ pub const Effects = struct {
|
|
|
2122
2208
|
try log.status(foe_ident, foe_stored.status, .None);
|
|
2123
2209
|
}
|
|
2124
2210
|
|
|
2125
|
-
fn paralyzeChance(battle: anytype, player: Player, move: Move.Data,
|
|
2211
|
+
fn paralyzeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
2126
2212
|
var foe = battle.foe(player);
|
|
2127
2213
|
var foe_stored = foe.stored();
|
|
2128
2214
|
|
|
@@ -2134,25 +2220,30 @@ pub const Effects = struct {
|
|
|
2134
2220
|
|
|
2135
2221
|
// Body Slam can't paralyze a Normal type Pokémon
|
|
2136
2222
|
if (foe.active.types.includes(move.type)) return;
|
|
2137
|
-
if (!Rolls.secondaryChance(battle, move.effect == .ParalyzeChance1))
|
|
2223
|
+
if (!try Rolls.secondaryChance(battle, player, move.effect == .ParalyzeChance1, options)) {
|
|
2224
|
+
return;
|
|
2225
|
+
}
|
|
2138
2226
|
|
|
2139
2227
|
foe_stored.status = Status.init(.PAR);
|
|
2140
2228
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
2141
2229
|
|
|
2142
|
-
try log.status(battle.active(player.foe()), foe_stored.status, .None);
|
|
2230
|
+
try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
|
|
2143
2231
|
}
|
|
2144
2232
|
|
|
2145
|
-
fn payDay(battle: anytype, player: Player,
|
|
2146
|
-
if (!showdown or !battle.foe(player).active.volatiles.Substitute)
|
|
2233
|
+
fn payDay(battle: anytype, player: Player, options: anytype) !void {
|
|
2234
|
+
if (!showdown or !battle.foe(player).active.volatiles.Substitute) {
|
|
2235
|
+
try options.log.fieldactivate();
|
|
2236
|
+
}
|
|
2147
2237
|
}
|
|
2148
2238
|
|
|
2149
|
-
fn poison(battle: anytype, player: Player, move: Move.Data,
|
|
2239
|
+
fn poison(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
2240
|
+
var log = options.log;
|
|
2150
2241
|
var foe = battle.foe(player);
|
|
2151
2242
|
var foe_stored = foe.stored();
|
|
2152
2243
|
const foe_ident = battle.active(player.foe());
|
|
2153
2244
|
const toxic = battle.side(player).last_selected_move == .Toxic;
|
|
2154
2245
|
|
|
2155
|
-
if (showdown and move.effect == .Poison and !try checkHit(battle, player, move,
|
|
2246
|
+
if (showdown and move.effect == .Poison and !try checkHit(battle, player, move, options)) {
|
|
2156
2247
|
return;
|
|
2157
2248
|
} else if (foe.active.volatiles.Substitute) {
|
|
2158
2249
|
if (move.effect != .Poison) return;
|
|
@@ -2176,8 +2267,13 @@ pub const Effects = struct {
|
|
|
2176
2267
|
}
|
|
2177
2268
|
|
|
2178
2269
|
if (move.effect == .Poison) {
|
|
2179
|
-
if (!showdown and !try checkHit(battle, player, move,
|
|
2180
|
-
|
|
2270
|
+
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2271
|
+
try options.chance.commit(player, .hit);
|
|
2272
|
+
} else {
|
|
2273
|
+
if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
|
|
2274
|
+
return;
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2181
2277
|
|
|
2182
2278
|
foe_stored.status = Status.init(.PSN);
|
|
2183
2279
|
if (toxic) {
|
|
@@ -2195,28 +2291,36 @@ pub const Effects = struct {
|
|
|
2195
2291
|
volatiles.Rage = true;
|
|
2196
2292
|
}
|
|
2197
2293
|
|
|
2198
|
-
fn recoil(battle: anytype, player: Player, residual: *bool,
|
|
2294
|
+
fn recoil(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
|
|
2199
2295
|
var side = battle.side(player);
|
|
2200
2296
|
var stored = side.stored();
|
|
2201
2297
|
|
|
2202
|
-
|
|
2298
|
+
assert(showdown or battle.last_damage > 0);
|
|
2299
|
+
if (showdown and battle.last_damage == 0) return;
|
|
2300
|
+
|
|
2301
|
+
const damage: i16 = @intCast(@max(battle.last_damage /
|
|
2203
2302
|
@as(u8, if (side.last_selected_move == .Struggle) 2 else 4), 1));
|
|
2204
|
-
stored.hp = @intCast(
|
|
2303
|
+
stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
|
|
2205
2304
|
|
|
2206
|
-
try log.damageOf(
|
|
2305
|
+
try options.log.damageOf(
|
|
2306
|
+
battle.active(player),
|
|
2307
|
+
stored,
|
|
2308
|
+
.RecoilOf,
|
|
2309
|
+
battle.active(player.foe()),
|
|
2310
|
+
);
|
|
2207
2311
|
if (showdown and stored.hp == 0) residual.* = false;
|
|
2208
2312
|
}
|
|
2209
2313
|
|
|
2210
|
-
fn reflect(battle: anytype, player: Player,
|
|
2314
|
+
fn reflect(battle: anytype, player: Player, options: anytype) !void {
|
|
2211
2315
|
var side = battle.side(player);
|
|
2212
2316
|
|
|
2213
|
-
if (side.active.volatiles.Reflect) return log.fail(battle.active(player), .None);
|
|
2317
|
+
if (side.active.volatiles.Reflect) return options.log.fail(battle.active(player), .None);
|
|
2214
2318
|
side.active.volatiles.Reflect = true;
|
|
2215
2319
|
|
|
2216
|
-
try log.start(battle.active(player), .Reflect);
|
|
2320
|
+
try options.log.start(battle.active(player), .Reflect);
|
|
2217
2321
|
}
|
|
2218
2322
|
|
|
2219
|
-
fn sleep(battle: anytype, player: Player, move: Move.Data,
|
|
2323
|
+
fn sleep(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
2220
2324
|
var foe = battle.foe(player);
|
|
2221
2325
|
var foe_stored = foe.stored();
|
|
2222
2326
|
const foe_ident = battle.active(player.foe());
|
|
@@ -2227,13 +2331,14 @@ pub const Effects = struct {
|
|
|
2227
2331
|
// the Sleep Clause Mod might activate, causing us to not actually bypass
|
|
2228
2332
|
} else {
|
|
2229
2333
|
if (Status.any(foe_stored.status)) {
|
|
2230
|
-
return log.fail(
|
|
2334
|
+
return options.log.fail(
|
|
2231
2335
|
foe_ident,
|
|
2232
2336
|
if (Status.is(foe_stored.status, .SLP)) .Sleep else .None,
|
|
2233
2337
|
);
|
|
2234
2338
|
}
|
|
2235
2339
|
// If checkHit in doMove didn't return true Pokémon Showdown wouldn't be in here
|
|
2236
|
-
if (!showdown and !try checkHit(battle, player, move,
|
|
2340
|
+
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2341
|
+
try options.chance.commit(player, .hit);
|
|
2237
2342
|
}
|
|
2238
2343
|
|
|
2239
2344
|
// Sleep Clause Mod
|
|
@@ -2244,31 +2349,32 @@ pub const Effects = struct {
|
|
|
2244
2349
|
}
|
|
2245
2350
|
foe.active.volatiles.Recharging = false;
|
|
2246
2351
|
|
|
2247
|
-
foe_stored.status = Status.slp(Rolls.sleepDuration(battle));
|
|
2248
|
-
|
|
2352
|
+
foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
|
|
2353
|
+
const last = battle.side(player).last_selected_move;
|
|
2354
|
+
try options.log.statusFrom(foe_ident, foe_stored.status, last);
|
|
2249
2355
|
}
|
|
2250
2356
|
|
|
2251
|
-
fn splash(battle: anytype, player: Player,
|
|
2252
|
-
try log.activate(battle.active(player), .Splash);
|
|
2357
|
+
fn splash(battle: anytype, player: Player, options: anytype) !void {
|
|
2358
|
+
try options.log.activate(battle.active(player), .Splash);
|
|
2253
2359
|
}
|
|
2254
2360
|
|
|
2255
|
-
fn substitute(battle: anytype, player: Player, residual: *bool,
|
|
2361
|
+
fn substitute(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
|
|
2256
2362
|
var side = battle.side(player);
|
|
2257
2363
|
if (side.active.volatiles.Substitute) {
|
|
2258
|
-
try log.fail(battle.active(player), .Substitute);
|
|
2364
|
+
try options.log.fail(battle.active(player), .Substitute);
|
|
2259
2365
|
return;
|
|
2260
2366
|
}
|
|
2261
2367
|
|
|
2262
2368
|
assert(side.stored().stats.hp <= 1023);
|
|
2263
2369
|
// Will be 0 if HP is <= 3 meaning that the user gets a 1 HP Substitute for "free"
|
|
2264
|
-
const hp = @intCast(
|
|
2370
|
+
const hp: u8 = @intCast(side.stored().stats.hp / 4);
|
|
2265
2371
|
// Pokénon Showdown incorrectly checks for 1/4 HP based on `target.maxhp / 4` which returns
|
|
2266
2372
|
// a floating point value and thus only correctly implements the Substitute 1/4 glitch when
|
|
2267
2373
|
// the target's HP is exactly divisible by 4 (here we're using an inlined divCeil routine to
|
|
2268
2374
|
// avoid having to convert to floating point)
|
|
2269
2375
|
const required_hp = if (showdown) @divFloor(side.stored().stats.hp - 1, 4) + 1 else hp;
|
|
2270
2376
|
if (side.stored().hp < required_hp) {
|
|
2271
|
-
try log.fail(battle.active(player), .Weak);
|
|
2377
|
+
try options.log.fail(battle.active(player), .Weak);
|
|
2272
2378
|
return;
|
|
2273
2379
|
}
|
|
2274
2380
|
|
|
@@ -2276,31 +2382,37 @@ pub const Effects = struct {
|
|
|
2276
2382
|
side.stored().hp -= hp;
|
|
2277
2383
|
side.active.volatiles.substitute = hp + 1;
|
|
2278
2384
|
side.active.volatiles.Substitute = true;
|
|
2279
|
-
try log.start(battle.active(player), .Substitute);
|
|
2385
|
+
try options.log.start(battle.active(player), .Substitute);
|
|
2280
2386
|
if (hp > 0) {
|
|
2281
|
-
try log.damage(battle.active(player), side.stored(), .None);
|
|
2387
|
+
try options.log.damage(battle.active(player), side.stored(), .None);
|
|
2282
2388
|
if (showdown and side.stored().hp == 0) residual.* = false;
|
|
2283
2389
|
}
|
|
2284
2390
|
}
|
|
2285
2391
|
|
|
2286
|
-
fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data,
|
|
2287
|
-
if (!showdown or battle.side(player).last_selected_move == .Teleport) return;
|
|
2288
|
-
|
|
2392
|
+
fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
2289
2393
|
// Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
|
|
2290
|
-
|
|
2291
|
-
|
|
2394
|
+
if (showdown) {
|
|
2395
|
+
if (battle.side(player).last_selected_move == .Teleport) return;
|
|
2396
|
+
if (try checkHit(battle, player, move, options)) {
|
|
2397
|
+
try options.chance.commit(player, .hit);
|
|
2398
|
+
}
|
|
2399
|
+
battle.last_damage = 0;
|
|
2400
|
+
} else {
|
|
2401
|
+
try options.log.fail(battle.active(player), .None);
|
|
2402
|
+
try options.log.laststill();
|
|
2403
|
+
}
|
|
2292
2404
|
}
|
|
2293
2405
|
|
|
2294
|
-
fn thrashing(battle: anytype, player: Player) void {
|
|
2406
|
+
fn thrashing(battle: anytype, player: Player, options: anytype) void {
|
|
2295
2407
|
var volatiles = &battle.side(player).active.volatiles;
|
|
2296
2408
|
assert(!volatiles.Thrashing);
|
|
2297
2409
|
assert(!volatiles.Bide);
|
|
2298
2410
|
|
|
2299
2411
|
volatiles.Thrashing = true;
|
|
2300
|
-
volatiles.attacks = Rolls.
|
|
2412
|
+
volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
2301
2413
|
}
|
|
2302
2414
|
|
|
2303
|
-
fn transform(battle: anytype, player: Player,
|
|
2415
|
+
fn transform(battle: anytype, player: Player, options: anytype) !void {
|
|
2304
2416
|
var side = battle.side(player);
|
|
2305
2417
|
const foe = battle.foe(player);
|
|
2306
2418
|
const foe_ident = battle.active(player.foe());
|
|
@@ -2326,10 +2438,10 @@ pub const Effects = struct {
|
|
|
2326
2438
|
side.active.moves[i].pp = if (m.id != .None) 5 else 0;
|
|
2327
2439
|
}
|
|
2328
2440
|
|
|
2329
|
-
try log.transform(battle.active(player), foe_ident);
|
|
2441
|
+
try options.log.transform(battle.active(player), foe_ident);
|
|
2330
2442
|
}
|
|
2331
2443
|
|
|
2332
|
-
fn binding(battle: anytype, player: Player, rewrap: bool) void {
|
|
2444
|
+
fn binding(battle: anytype, player: Player, rewrap: bool, options: anytype) !void {
|
|
2333
2445
|
var side = battle.side(player);
|
|
2334
2446
|
var foe = battle.foe(player);
|
|
2335
2447
|
|
|
@@ -2341,10 +2453,12 @@ pub const Effects = struct {
|
|
|
2341
2453
|
foe.active.volatiles.Recharging = false;
|
|
2342
2454
|
} else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
|
|
2343
2455
|
|
|
2344
|
-
side.active.volatiles.attacks =
|
|
2456
|
+
side.active.volatiles.attacks =
|
|
2457
|
+
try Rolls.distribution(battle, .Binding, player, options) - 1;
|
|
2345
2458
|
}
|
|
2346
2459
|
|
|
2347
|
-
fn boost(battle: anytype, player: Player, move: Move.Data,
|
|
2460
|
+
fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
2461
|
+
var log = options.log;
|
|
2348
2462
|
var side = battle.side(player);
|
|
2349
2463
|
const ident = battle.active(player);
|
|
2350
2464
|
|
|
@@ -2356,7 +2470,7 @@ pub const Effects = struct {
|
|
|
2356
2470
|
assert(boosts.atk >= -6 and boosts.atk <= 6);
|
|
2357
2471
|
if (boosts.atk == 6) return try log.fail(ident, .None);
|
|
2358
2472
|
const n: u2 = if (move.effect == .AttackUp2) 2 else 1;
|
|
2359
|
-
boosts.atk = @
|
|
2473
|
+
boosts.atk = @as(i4, @intCast(@min(6, @as(i8, boosts.atk) + n)));
|
|
2360
2474
|
const reason = if (move.effect == .Rage) Boost.Rage else Boost.Attack;
|
|
2361
2475
|
if (stats.atk == MAX_STAT_VALUE) {
|
|
2362
2476
|
boosts.atk -= 1;
|
|
@@ -2367,7 +2481,7 @@ pub const Effects = struct {
|
|
|
2367
2481
|
}
|
|
2368
2482
|
return try log.fail(ident, .None);
|
|
2369
2483
|
}
|
|
2370
|
-
var mod = BOOSTS[@
|
|
2484
|
+
var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
|
|
2371
2485
|
const stat = unmodifiedStats(battle, side).atk;
|
|
2372
2486
|
stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2373
2487
|
try log.boost(ident, reason, n);
|
|
@@ -2378,7 +2492,7 @@ pub const Effects = struct {
|
|
|
2378
2492
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
2379
2493
|
if (boosts.def == 6) return try log.fail(ident, .None);
|
|
2380
2494
|
const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
|
|
2381
|
-
boosts.def = @intCast(
|
|
2495
|
+
boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
|
|
2382
2496
|
if (stats.def == MAX_STAT_VALUE) {
|
|
2383
2497
|
boosts.def -= 1;
|
|
2384
2498
|
if (showdown) {
|
|
@@ -2387,7 +2501,7 @@ pub const Effects = struct {
|
|
|
2387
2501
|
}
|
|
2388
2502
|
return try log.fail(ident, .None);
|
|
2389
2503
|
}
|
|
2390
|
-
var mod = BOOSTS[@
|
|
2504
|
+
var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
|
|
2391
2505
|
const stat = unmodifiedStats(battle, side).def;
|
|
2392
2506
|
stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2393
2507
|
try log.boost(ident, .Defense, n);
|
|
@@ -2395,7 +2509,7 @@ pub const Effects = struct {
|
|
|
2395
2509
|
.SpeedUp2 => {
|
|
2396
2510
|
assert(boosts.spe >= -6 and boosts.spe <= 6);
|
|
2397
2511
|
if (boosts.spe == 6) return try log.fail(ident, .None);
|
|
2398
|
-
boosts.spe = @intCast(
|
|
2512
|
+
boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
|
|
2399
2513
|
if (stats.spe == MAX_STAT_VALUE) {
|
|
2400
2514
|
boosts.spe -= 1;
|
|
2401
2515
|
if (showdown) {
|
|
@@ -2404,7 +2518,7 @@ pub const Effects = struct {
|
|
|
2404
2518
|
}
|
|
2405
2519
|
return try log.fail(ident, .None);
|
|
2406
2520
|
}
|
|
2407
|
-
var mod = BOOSTS[@
|
|
2521
|
+
var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
|
|
2408
2522
|
const stat = unmodifiedStats(battle, side).spe;
|
|
2409
2523
|
stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2410
2524
|
try log.boost(ident, .Speed, 2);
|
|
@@ -2413,7 +2527,7 @@ pub const Effects = struct {
|
|
|
2413
2527
|
assert(boosts.spc >= -6 and boosts.spc <= 6);
|
|
2414
2528
|
if (boosts.spc == 6) return try log.fail(ident, .None);
|
|
2415
2529
|
const n: u2 = if (move.effect == .SpecialUp2) 2 else 1;
|
|
2416
|
-
boosts.spc = @intCast(
|
|
2530
|
+
boosts.spc = @intCast(@min(6, @as(i8, boosts.spc) + n));
|
|
2417
2531
|
if (stats.spc == MAX_STAT_VALUE) {
|
|
2418
2532
|
boosts.spc -= 1;
|
|
2419
2533
|
if (showdown) {
|
|
@@ -2424,7 +2538,7 @@ pub const Effects = struct {
|
|
|
2424
2538
|
}
|
|
2425
2539
|
return try log.fail(ident, .None);
|
|
2426
2540
|
}
|
|
2427
|
-
var mod = BOOSTS[@
|
|
2541
|
+
var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
|
|
2428
2542
|
const stat = unmodifiedStats(battle, side).spc;
|
|
2429
2543
|
stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2430
2544
|
try log.boost(ident, .SpecialAttack, n);
|
|
@@ -2433,7 +2547,7 @@ pub const Effects = struct {
|
|
|
2433
2547
|
.EvasionUp1 => {
|
|
2434
2548
|
assert(boosts.evasion >= -6 and boosts.evasion <= 6);
|
|
2435
2549
|
if (boosts.evasion == 6) return try log.fail(ident, .None);
|
|
2436
|
-
boosts.evasion = @intCast(
|
|
2550
|
+
boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
|
|
2437
2551
|
try log.boost(ident, .Evasion, 1);
|
|
2438
2552
|
},
|
|
2439
2553
|
else => unreachable,
|
|
@@ -2443,28 +2557,32 @@ pub const Effects = struct {
|
|
|
2443
2557
|
statusModify(battle.foe(player).stored().status, &battle.foe(player).active.stats);
|
|
2444
2558
|
}
|
|
2445
2559
|
|
|
2446
|
-
fn unboost(battle: anytype, player: Player, move: Move.Data,
|
|
2560
|
+
fn unboost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
2561
|
+
var log = options.log;
|
|
2447
2562
|
var foe = battle.foe(player);
|
|
2448
2563
|
const foe_ident = battle.active(player.foe());
|
|
2564
|
+
const secondary = move.effect.isStatDownChance();
|
|
2449
2565
|
|
|
2450
|
-
if (foe.active.volatiles.Substitute)
|
|
2451
|
-
return if (!move.effect.isStatDownChance()) log.fail(foe_ident, .None);
|
|
2452
|
-
}
|
|
2566
|
+
if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(foe_ident, .None);
|
|
2453
2567
|
|
|
2454
|
-
if (
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2568
|
+
if (secondary) {
|
|
2569
|
+
const proc = try Rolls.unboost(battle, player, options);
|
|
2570
|
+
if (!proc or foe.active.volatiles.Invulnerable) return;
|
|
2571
|
+
} else {
|
|
2572
|
+
// checkHit already checks for Invulnerable
|
|
2573
|
+
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2574
|
+
try options.chance.commit(player, .hit);
|
|
2458
2575
|
}
|
|
2459
2576
|
|
|
2460
2577
|
var stats = &foe.active.stats;
|
|
2461
2578
|
var boosts = &foe.active.boosts;
|
|
2579
|
+
const fail = showdown or !secondary;
|
|
2462
2580
|
|
|
2463
2581
|
switch (move.effect) {
|
|
2464
2582
|
.AttackDown1, .AttackDownChance => {
|
|
2465
2583
|
assert(boosts.atk >= -6 and boosts.atk <= 6);
|
|
2466
|
-
if (boosts.atk == -6) return try log.fail(foe_ident, .None);
|
|
2467
|
-
boosts.atk = @intCast(
|
|
2584
|
+
if (boosts.atk == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2585
|
+
boosts.atk = @intCast(@max(-6, @as(i8, boosts.atk) - 1));
|
|
2468
2586
|
if (stats.atk == 1) {
|
|
2469
2587
|
boosts.atk += 1;
|
|
2470
2588
|
if (showdown) {
|
|
@@ -2473,16 +2591,16 @@ pub const Effects = struct {
|
|
|
2473
2591
|
}
|
|
2474
2592
|
return try log.fail(foe_ident, .None);
|
|
2475
2593
|
}
|
|
2476
|
-
var mod = BOOSTS[@
|
|
2594
|
+
var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
|
|
2477
2595
|
const stat = unmodifiedStats(battle, foe).atk;
|
|
2478
2596
|
stats.atk = @max(1, stat * mod[0] / mod[1]);
|
|
2479
2597
|
try log.boost(foe_ident, .Attack, -1);
|
|
2480
2598
|
},
|
|
2481
2599
|
.DefenseDown1, .DefenseDown2, .DefenseDownChance => {
|
|
2482
2600
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
2483
|
-
if (boosts.def == -6) return try log.fail(foe_ident, .None);
|
|
2601
|
+
if (boosts.def == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2484
2602
|
const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
|
|
2485
|
-
boosts.def = @intCast(
|
|
2603
|
+
boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
|
|
2486
2604
|
if (stats.def == 1) {
|
|
2487
2605
|
boosts.def += 1;
|
|
2488
2606
|
if (showdown) {
|
|
@@ -2491,15 +2609,15 @@ pub const Effects = struct {
|
|
|
2491
2609
|
}
|
|
2492
2610
|
return try log.fail(foe_ident, .None);
|
|
2493
2611
|
}
|
|
2494
|
-
var mod = BOOSTS[@
|
|
2612
|
+
var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
|
|
2495
2613
|
const stat = unmodifiedStats(battle, foe).def;
|
|
2496
2614
|
stats.def = @max(1, stat * mod[0] / mod[1]);
|
|
2497
2615
|
try log.boost(foe_ident, .Defense, -@as(i8, n));
|
|
2498
2616
|
},
|
|
2499
2617
|
.SpeedDown1, .SpeedDownChance => {
|
|
2500
2618
|
assert(boosts.spe >= -6 and boosts.spe <= 6);
|
|
2501
|
-
if (boosts.spe == -6) return try log.fail(foe_ident, .None);
|
|
2502
|
-
boosts.spe = @intCast(
|
|
2619
|
+
if (boosts.spe == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2620
|
+
boosts.spe = @intCast(@max(-6, @as(i8, boosts.spe) - 1));
|
|
2503
2621
|
if (stats.spe == 1) {
|
|
2504
2622
|
boosts.spe += 1;
|
|
2505
2623
|
if (showdown) {
|
|
@@ -2508,7 +2626,7 @@ pub const Effects = struct {
|
|
|
2508
2626
|
}
|
|
2509
2627
|
return try log.fail(foe_ident, .None);
|
|
2510
2628
|
}
|
|
2511
|
-
var mod = BOOSTS[@
|
|
2629
|
+
var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
|
|
2512
2630
|
const stat = unmodifiedStats(battle, foe).spe;
|
|
2513
2631
|
stats.spe = @max(1, stat * mod[0] / mod[1]);
|
|
2514
2632
|
try log.boost(foe_ident, .Speed, -1);
|
|
@@ -2516,8 +2634,8 @@ pub const Effects = struct {
|
|
|
2516
2634
|
},
|
|
2517
2635
|
.SpecialDownChance => {
|
|
2518
2636
|
assert(boosts.spc >= -6 and boosts.spc <= 6);
|
|
2519
|
-
if (boosts.spc == -6) return try log.fail(foe_ident, .None);
|
|
2520
|
-
boosts.spc = @intCast(
|
|
2637
|
+
if (boosts.spc == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2638
|
+
boosts.spc = @intCast(@max(-6, @as(i8, boosts.spc) - 1));
|
|
2521
2639
|
if (stats.spc == 1) {
|
|
2522
2640
|
boosts.spc += 1;
|
|
2523
2641
|
if (showdown) {
|
|
@@ -2528,7 +2646,7 @@ pub const Effects = struct {
|
|
|
2528
2646
|
}
|
|
2529
2647
|
return try log.fail(foe_ident, .None);
|
|
2530
2648
|
}
|
|
2531
|
-
var mod = BOOSTS[@
|
|
2649
|
+
var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
|
|
2532
2650
|
const stat = unmodifiedStats(battle, foe).spc;
|
|
2533
2651
|
stats.spc = @max(1, stat * mod[0] / mod[1]);
|
|
2534
2652
|
try log.boost(foe_ident, .SpecialAttack, -1);
|
|
@@ -2536,8 +2654,8 @@ pub const Effects = struct {
|
|
|
2536
2654
|
},
|
|
2537
2655
|
.AccuracyDown1 => {
|
|
2538
2656
|
assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
|
|
2539
|
-
if (boosts.accuracy == -6) return try log.fail(foe_ident, .None);
|
|
2540
|
-
boosts.accuracy = @intCast(
|
|
2657
|
+
if (boosts.accuracy == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2658
|
+
boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
|
|
2541
2659
|
try log.boost(foe_ident, .Accuracy, -1);
|
|
2542
2660
|
},
|
|
2543
2661
|
else => unreachable,
|
|
@@ -2562,16 +2680,19 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
|
|
|
2562
2680
|
}
|
|
2563
2681
|
}
|
|
2564
2682
|
|
|
2565
|
-
inline fn isForced(active:
|
|
2683
|
+
inline fn isForced(active: anytype) bool {
|
|
2566
2684
|
return active.volatiles.Recharging or active.volatiles.Rage or
|
|
2567
2685
|
active.volatiles.Thrashing or active.volatiles.Charging;
|
|
2568
2686
|
}
|
|
2569
2687
|
|
|
2570
|
-
fn clearVolatiles(battle: anytype, who: Player,
|
|
2688
|
+
fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !void {
|
|
2689
|
+
var log = options.log;
|
|
2571
2690
|
var side = battle.side(who);
|
|
2572
2691
|
var volatiles = &side.active.volatiles;
|
|
2573
2692
|
const ident = battle.active(who);
|
|
2574
2693
|
|
|
2694
|
+
options.chance.clearDurations(who, clear);
|
|
2695
|
+
|
|
2575
2696
|
if (volatiles.disabled_move != 0) {
|
|
2576
2697
|
volatiles.disabled_move = 0;
|
|
2577
2698
|
volatiles.disabled_duration = 0;
|
|
@@ -2615,137 +2736,299 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
|
|
|
2615
2736
|
}
|
|
2616
2737
|
|
|
2617
2738
|
pub const Rolls = struct {
|
|
2618
|
-
inline fn speedTie(battle: anytype) bool {
|
|
2619
|
-
if (
|
|
2620
|
-
|
|
2621
|
-
|
|
2739
|
+
inline fn speedTie(battle: anytype, options: anytype) !bool {
|
|
2740
|
+
const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
|
|
2741
|
+
player == .P1
|
|
2742
|
+
else if (showdown)
|
|
2743
|
+
battle.rng.range(u8, 0, 2) == 0
|
|
2744
|
+
else
|
|
2745
|
+
battle.rng.next() < Gen12.percent(50) + 1;
|
|
2622
2746
|
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
return std.math.rotl(u8, battle.rng.next(), 3) < chance;
|
|
2747
|
+
try options.chance.speedTie(p1);
|
|
2748
|
+
return p1;
|
|
2626
2749
|
}
|
|
2627
2750
|
|
|
2628
|
-
inline fn
|
|
2629
|
-
if (
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2751
|
+
inline fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
|
|
2752
|
+
const crit = if (options.calc.overridden(player, .critical_hit)) |val|
|
|
2753
|
+
val == .true
|
|
2754
|
+
else if (showdown)
|
|
2755
|
+
battle.rng.chance(u8, rate, 256)
|
|
2756
|
+
else
|
|
2757
|
+
std.math.rotl(u8, battle.rng.next(), 3) < rate;
|
|
2635
2758
|
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
return battle.rng.next() < accuracy;
|
|
2759
|
+
try options.chance.criticalHit(player, crit, rate);
|
|
2760
|
+
return crit;
|
|
2639
2761
|
}
|
|
2640
2762
|
|
|
2641
|
-
inline fn
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2763
|
+
inline fn damage(battle: anytype, player: anytype, options: anytype) !u8 {
|
|
2764
|
+
const roll = if (options.calc.overridden(player, .damage)) |val|
|
|
2765
|
+
val
|
|
2766
|
+
else roll: {
|
|
2767
|
+
if (showdown) break :roll battle.rng.range(u8, 217, 256);
|
|
2768
|
+
while (true) {
|
|
2769
|
+
const r = std.math.rotr(u8, battle.rng.next(), 1);
|
|
2770
|
+
if (r >= 217) break :roll r;
|
|
2771
|
+
}
|
|
2772
|
+
};
|
|
2645
2773
|
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
return
|
|
2774
|
+
assert(roll >= 217 and roll <= 255);
|
|
2775
|
+
try options.chance.damage(player, roll);
|
|
2776
|
+
return roll;
|
|
2649
2777
|
}
|
|
2650
2778
|
|
|
2651
|
-
inline fn
|
|
2652
|
-
if (
|
|
2653
|
-
|
|
2779
|
+
inline fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) bool {
|
|
2780
|
+
const ok = if (options.calc.overridden(player, .hit)) |val|
|
|
2781
|
+
val == .true
|
|
2782
|
+
else if (showdown)
|
|
2783
|
+
battle.rng.chance(u8, accuracy, 256)
|
|
2784
|
+
else
|
|
2785
|
+
battle.rng.next() < accuracy;
|
|
2786
|
+
|
|
2787
|
+
options.chance.hit(ok, accuracy);
|
|
2788
|
+
return ok;
|
|
2654
2789
|
}
|
|
2655
2790
|
|
|
2656
|
-
inline fn
|
|
2657
|
-
if (
|
|
2658
|
-
|
|
2791
|
+
inline fn confused(battle: anytype, player: Player, options: anytype) !bool {
|
|
2792
|
+
const cfz = if (options.calc.overridden(player, .confused)) |val|
|
|
2793
|
+
val == .true
|
|
2794
|
+
else if (showdown)
|
|
2795
|
+
!battle.rng.chance(u8, 128, 256)
|
|
2796
|
+
else
|
|
2797
|
+
battle.rng.next() >= Gen12.percent(50) + 1;
|
|
2798
|
+
|
|
2799
|
+
try options.chance.confused(player, cfz);
|
|
2800
|
+
return cfz;
|
|
2659
2801
|
}
|
|
2660
2802
|
|
|
2661
|
-
inline fn
|
|
2662
|
-
if (
|
|
2663
|
-
|
|
2803
|
+
inline fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
|
|
2804
|
+
const par = if (options.calc.overridden(player, .paralyzed)) |val|
|
|
2805
|
+
val == .true
|
|
2806
|
+
else if (showdown)
|
|
2807
|
+
battle.rng.chance(u8, 63, 256)
|
|
2808
|
+
else
|
|
2809
|
+
battle.rng.next() < Gen12.percent(25);
|
|
2810
|
+
|
|
2811
|
+
try options.chance.paralyzed(player, par);
|
|
2812
|
+
return par;
|
|
2664
2813
|
}
|
|
2665
2814
|
|
|
2666
|
-
inline fn
|
|
2667
|
-
if (
|
|
2668
|
-
|
|
2815
|
+
inline fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
|
|
2816
|
+
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2817
|
+
val == .true
|
|
2818
|
+
else if (showdown)
|
|
2819
|
+
battle.rng.chance(u8, 25, 256)
|
|
2820
|
+
else
|
|
2821
|
+
battle.rng.next() < Gen12.percent(10);
|
|
2822
|
+
|
|
2823
|
+
try options.chance.secondaryChance(player, proc, 25);
|
|
2824
|
+
return proc;
|
|
2669
2825
|
}
|
|
2670
2826
|
|
|
2671
|
-
inline fn
|
|
2672
|
-
if (
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2827
|
+
inline fn secondaryChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
|
|
2828
|
+
const rate: u8 = if (low) 26 else 77;
|
|
2829
|
+
|
|
2830
|
+
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2831
|
+
val == .true
|
|
2832
|
+
else if (showdown)
|
|
2833
|
+
battle.rng.chance(u8, rate, 256)
|
|
2834
|
+
else
|
|
2835
|
+
battle.rng.next() < 1 + (if (low) Gen12.percent(10) else Gen12.percent(30));
|
|
2836
|
+
|
|
2837
|
+
try options.chance.secondaryChance(player, proc, rate);
|
|
2838
|
+
return proc;
|
|
2677
2839
|
}
|
|
2678
2840
|
|
|
2679
|
-
inline fn
|
|
2680
|
-
if (
|
|
2681
|
-
|
|
2841
|
+
inline fn poisonChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
|
|
2842
|
+
const rate: u8 = if (low) 52 else 103;
|
|
2843
|
+
|
|
2844
|
+
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2845
|
+
val == .true
|
|
2846
|
+
else if (showdown)
|
|
2847
|
+
battle.rng.chance(u8, rate, 256)
|
|
2848
|
+
else
|
|
2849
|
+
battle.rng.next() < 1 + (if (low) Gen12.percent(20) else Gen12.percent(40));
|
|
2850
|
+
|
|
2851
|
+
try options.chance.secondaryChance(player, proc, rate);
|
|
2852
|
+
return proc;
|
|
2682
2853
|
}
|
|
2683
2854
|
|
|
2684
|
-
fn
|
|
2685
|
-
if (
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2855
|
+
inline fn unboost(battle: anytype, player: Player, options: anytype) !bool {
|
|
2856
|
+
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2857
|
+
val == .true
|
|
2858
|
+
else if (showdown)
|
|
2859
|
+
battle.rng.chance(u8, 85, 256)
|
|
2860
|
+
else
|
|
2861
|
+
battle.rng.next() < Gen12.percent(33) + 1;
|
|
2862
|
+
|
|
2863
|
+
try options.chance.secondaryChance(player, proc, 85);
|
|
2864
|
+
return proc;
|
|
2865
|
+
}
|
|
2866
|
+
|
|
2867
|
+
inline fn metronome(battle: anytype, player: Player, options: anytype) !Move {
|
|
2868
|
+
const move: Move = if (options.calc.overridden(player, .metronome)) |val|
|
|
2869
|
+
val
|
|
2870
|
+
else if (showdown) move: {
|
|
2871
|
+
const r = battle.rng.range(u8, 0, @intFromEnum(Move.Struggle) - 2);
|
|
2872
|
+
const mod = @as(u2, (if (r < @intFromEnum(Move.Metronome) - 1) 1 else 2));
|
|
2873
|
+
break :move @enumFromInt(r + mod);
|
|
2874
|
+
} else move: {
|
|
2875
|
+
while (true) {
|
|
2876
|
+
const r = battle.rng.next();
|
|
2877
|
+
if (r == 0 or r == @intFromEnum(Move.Metronome)) continue;
|
|
2878
|
+
if (r >= @intFromEnum(Move.Struggle)) continue;
|
|
2879
|
+
break :move @enumFromInt(r);
|
|
2704
2880
|
}
|
|
2705
|
-
}
|
|
2881
|
+
};
|
|
2706
2882
|
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
|
|
2710
|
-
}
|
|
2883
|
+
try options.chance.metronome(player, move);
|
|
2884
|
+
return move;
|
|
2711
2885
|
}
|
|
2712
2886
|
|
|
2713
|
-
inline fn
|
|
2714
|
-
|
|
2715
|
-
|
|
2887
|
+
inline fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
|
|
2888
|
+
const power = if (options.calc.overridden(player, .psywave)) |val|
|
|
2889
|
+
val - 1
|
|
2890
|
+
else if (showdown)
|
|
2891
|
+
battle.rng.range(u8, 0, max)
|
|
2892
|
+
else power: {
|
|
2893
|
+
while (true) {
|
|
2894
|
+
const r = battle.rng.next();
|
|
2895
|
+
if (r < max) break :power r;
|
|
2896
|
+
}
|
|
2897
|
+
};
|
|
2898
|
+
|
|
2899
|
+
assert(power < max);
|
|
2900
|
+
try options.chance.psywave(player, power, max);
|
|
2901
|
+
return power;
|
|
2716
2902
|
}
|
|
2717
2903
|
|
|
2718
|
-
inline fn
|
|
2719
|
-
if (
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2904
|
+
inline fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2905
|
+
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2906
|
+
@intCast(val)
|
|
2907
|
+
else if (showdown)
|
|
2908
|
+
@intCast(battle.rng.range(u8, 1, 8))
|
|
2909
|
+
else duration: {
|
|
2910
|
+
while (true) {
|
|
2911
|
+
const r = battle.rng.next() & 7;
|
|
2912
|
+
if (r != 0) break :duration @intCast(r);
|
|
2913
|
+
}
|
|
2914
|
+
};
|
|
2915
|
+
|
|
2916
|
+
assert(duration >= 1 and duration <= 7);
|
|
2917
|
+
options.chance.duration(.sleep, player, player.foe(), duration);
|
|
2918
|
+
return duration;
|
|
2730
2919
|
}
|
|
2731
2920
|
|
|
2732
|
-
inline fn
|
|
2733
|
-
if (
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2921
|
+
inline fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
|
|
2922
|
+
const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
|
|
2923
|
+
val
|
|
2924
|
+
else if (showdown)
|
|
2925
|
+
@intCast(battle.rng.range(u8, 1, 9))
|
|
2926
|
+
else
|
|
2927
|
+
@intCast((battle.rng.next() & 7) + 1);
|
|
2928
|
+
|
|
2929
|
+
assert(duration >= 1 and duration <= 8);
|
|
2930
|
+
options.chance.duration(.disable, player, player.foe(), duration);
|
|
2931
|
+
return duration;
|
|
2738
2932
|
}
|
|
2739
2933
|
|
|
2740
|
-
inline fn
|
|
2741
|
-
if (
|
|
2742
|
-
|
|
2743
|
-
|
|
2934
|
+
inline fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
|
|
2935
|
+
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2936
|
+
@intCast(val)
|
|
2937
|
+
else if (showdown)
|
|
2938
|
+
@intCast(battle.rng.range(u8, 2, 6))
|
|
2939
|
+
else
|
|
2940
|
+
@intCast((battle.rng.next() & 3) + 2);
|
|
2941
|
+
|
|
2942
|
+
assert(duration >= 2 and duration <= 5);
|
|
2943
|
+
options.chance.duration(.confusion, player, if (self) player else player.foe(), duration);
|
|
2944
|
+
return duration;
|
|
2744
2945
|
}
|
|
2745
2946
|
|
|
2746
|
-
inline fn
|
|
2747
|
-
if (
|
|
2748
|
-
|
|
2947
|
+
inline fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2948
|
+
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2949
|
+
@intCast(val)
|
|
2950
|
+
else if (showdown)
|
|
2951
|
+
@intCast(battle.rng.range(u4, 2, 4))
|
|
2952
|
+
else
|
|
2953
|
+
@intCast((battle.rng.next() & 1) + 2);
|
|
2954
|
+
|
|
2955
|
+
assert(duration >= 2 and duration <= 3);
|
|
2956
|
+
options.chance.duration(.attacking, player, player, duration);
|
|
2957
|
+
return duration;
|
|
2958
|
+
}
|
|
2959
|
+
|
|
2960
|
+
inline fn distribution(
|
|
2961
|
+
battle: anytype,
|
|
2962
|
+
comptime effect: Move.Effect,
|
|
2963
|
+
player: Player,
|
|
2964
|
+
options: anytype,
|
|
2965
|
+
) !u3 {
|
|
2966
|
+
const roll = if (effect == .Binding) .duration else .multi_hit;
|
|
2967
|
+
const n: u3 = if (options.calc.overridden(player, roll)) |val|
|
|
2968
|
+
@intCast(val)
|
|
2969
|
+
else if (showdown)
|
|
2970
|
+
DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)]
|
|
2971
|
+
else n: {
|
|
2972
|
+
const r = (battle.rng.next() & 3);
|
|
2973
|
+
break :n @intCast((if (r < 2) r else battle.rng.next() & 3) + 2);
|
|
2974
|
+
};
|
|
2975
|
+
|
|
2976
|
+
assert(n >= 2 and n <= 5);
|
|
2977
|
+
if (effect == .Binding) {
|
|
2978
|
+
options.chance.duration(.binding, player, player, n);
|
|
2979
|
+
} else {
|
|
2980
|
+
try options.chance.multiHit(player, n);
|
|
2981
|
+
}
|
|
2982
|
+
return n;
|
|
2983
|
+
}
|
|
2984
|
+
|
|
2985
|
+
fn moveSlot(
|
|
2986
|
+
battle: anytype,
|
|
2987
|
+
player: Player,
|
|
2988
|
+
moves: []MoveSlot,
|
|
2989
|
+
check_pp: u4,
|
|
2990
|
+
options: anytype,
|
|
2991
|
+
) !u4 {
|
|
2992
|
+
// TODO: consider throwing error instead of rerolling?
|
|
2993
|
+
const overridden = if (options.calc.overridden(player, .move_slot)) |val|
|
|
2994
|
+
if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
|
|
2995
|
+
val
|
|
2996
|
+
else
|
|
2997
|
+
null
|
|
2998
|
+
else
|
|
2999
|
+
null;
|
|
3000
|
+
|
|
3001
|
+
const slot: u4 = overridden orelse slot: {
|
|
3002
|
+
if (showdown) {
|
|
3003
|
+
if (check_pp == 0) {
|
|
3004
|
+
var i: usize = moves.len;
|
|
3005
|
+
while (i > 0) {
|
|
3006
|
+
i -= 1;
|
|
3007
|
+
if (moves[i].id != .None) {
|
|
3008
|
+
break :slot battle.rng.range(u4, 0, @as(u4, @intCast(i + 1))) + 1;
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
} else {
|
|
3012
|
+
var r = battle.rng.range(u4, 0, check_pp) + 1;
|
|
3013
|
+
var i: usize = 0;
|
|
3014
|
+
while (i < moves.len and r > 0) : (i += 1) {
|
|
3015
|
+
if (moves[i].pp > 0) {
|
|
3016
|
+
r -= 1;
|
|
3017
|
+
if (r == 0) break;
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
break :slot @intCast(i + 1);
|
|
3021
|
+
}
|
|
3022
|
+
}
|
|
3023
|
+
|
|
3024
|
+
while (true) {
|
|
3025
|
+
const r: u4 = @intCast(battle.rng.next() & 3);
|
|
3026
|
+
if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) break :slot r + 1;
|
|
3027
|
+
}
|
|
3028
|
+
};
|
|
3029
|
+
|
|
3030
|
+
try options.chance.moveSlot(player, slot, moves, check_pp);
|
|
3031
|
+
return slot;
|
|
2749
3032
|
}
|
|
2750
3033
|
};
|
|
2751
3034
|
|
|
@@ -2753,7 +3036,7 @@ test "RNG agreement" {
|
|
|
2753
3036
|
if (!showdown) return;
|
|
2754
3037
|
var expected: [256]u32 = undefined;
|
|
2755
3038
|
for (0..expected.len) |i| {
|
|
2756
|
-
expected[i] = @intCast(
|
|
3039
|
+
expected[i] = @intCast(i * 0x1000000);
|
|
2757
3040
|
}
|
|
2758
3041
|
|
|
2759
3042
|
var spe = rng.FixedRNG(1, expected.len){ .rolls = expected };
|
|
@@ -2796,7 +3079,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
2796
3079
|
const side = battle.side(player);
|
|
2797
3080
|
const foe = battle.foe(player);
|
|
2798
3081
|
|
|
2799
|
-
var active = side.active;
|
|
3082
|
+
var active = &side.active;
|
|
2800
3083
|
const stored = side.stored();
|
|
2801
3084
|
|
|
2802
3085
|
// While players are not given any input options on the cartridge in these cases,
|
|
@@ -2810,7 +3093,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
2810
3093
|
// strictly correct as the player would not have been presented the option to move or
|
|
2811
3094
|
// switch at all.
|
|
2812
3095
|
if (isForced(active)) {
|
|
2813
|
-
out[n] = .{ .type = .Move, .data = @
|
|
3096
|
+
out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
|
|
2814
3097
|
n += 1;
|
|
2815
3098
|
return n;
|
|
2816
3099
|
}
|