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