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