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