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