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