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