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