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