@pkmn/engine 0.1.0-dev.c5fe6d07 → 0.1.0-dev.ce4c0b2a
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 +46 -19
- 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 +230 -99
- package/package.json +63 -30
- package/src/bin/install-pkmn-engine +29 -19
- 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 +11 -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 +807 -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 +660 -137
- package/src/lib/gen1/chance.zig +409 -284
- 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 +48 -59
- package/src/lib/gen1/helpers.zig +94 -552
- package/src/lib/gen1/mechanics.zig +580 -518
- 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 -892
- package/src/lib/gen2/README.md +0 -308
- 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,15 +159,13 @@ 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);
|
|
@@ -210,16 +195,13 @@ fn selectMove(
|
|
|
210
195
|
if (choice.data == 0) {
|
|
211
196
|
const struggle = ok: {
|
|
212
197
|
for (side.active.moves, 0..) |move, i| {
|
|
213
|
-
if (move.pp > 0 and volatiles.
|
|
198
|
+
if (move.pp > 0 and volatiles.disable_move != i + 1) break :ok false;
|
|
214
199
|
}
|
|
215
200
|
break :ok true;
|
|
216
201
|
};
|
|
217
|
-
|
|
218
202
|
assert(struggle);
|
|
219
|
-
saveMove(battle, player, choice);
|
|
220
|
-
} else {
|
|
221
|
-
saveMove(battle, player, choice);
|
|
222
203
|
}
|
|
204
|
+
saveMove(battle, player, choice);
|
|
223
205
|
|
|
224
206
|
return null;
|
|
225
207
|
}
|
|
@@ -232,7 +214,7 @@ fn saveMove(battle: anytype, player: Player, choice: ?Choice) void {
|
|
|
232
214
|
if (c.data == 0) {
|
|
233
215
|
side.last_selected_move = .Struggle;
|
|
234
216
|
} else {
|
|
235
|
-
assert(showdown or side.active.volatiles.
|
|
217
|
+
assert(showdown or side.active.volatiles.disable_move != c.data);
|
|
236
218
|
const move = side.active.move(c.data);
|
|
237
219
|
// You cannot *select* a move with 0 PP (except on Pokémon Showdown where that is
|
|
238
220
|
// sometimes required...), but a 0 PP move can be used automatically
|
|
@@ -270,17 +252,17 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: a
|
|
|
270
252
|
active.moves = incoming.moves;
|
|
271
253
|
|
|
272
254
|
statusModify(incoming.status, &active.stats);
|
|
255
|
+
options.chance.switched(player, slot);
|
|
273
256
|
|
|
274
257
|
foe.active.volatiles.Binding = false;
|
|
275
258
|
|
|
276
|
-
try options.log.switched(battle.active(player), incoming);
|
|
277
|
-
options.chance.switched(player, side.order[0], out);
|
|
259
|
+
try options.log.switched(.{ battle.active(player), incoming });
|
|
278
260
|
|
|
279
261
|
if (showdown and incoming.status == Status.TOX) {
|
|
280
262
|
incoming.status = Status.init(.PSN);
|
|
281
263
|
// Technically, Pokémon Showdown adds these after *both* Pokémon have switched, but we'd
|
|
282
264
|
// rather not clutter up turnOrder just for this (incorrect) log message
|
|
283
|
-
try options.log.status(battle.active(player), incoming.status, .Silent);
|
|
265
|
+
try options.log.status(.{ battle.active(player), incoming.status, .Silent });
|
|
284
266
|
}
|
|
285
267
|
}
|
|
286
268
|
|
|
@@ -343,12 +325,10 @@ fn doTurn(
|
|
|
343
325
|
player_choice: Choice,
|
|
344
326
|
player_rewrap: bool,
|
|
345
327
|
player_skip: bool,
|
|
346
|
-
player_from: ?Move,
|
|
347
328
|
foe_player: Player,
|
|
348
329
|
foe_choice: Choice,
|
|
349
330
|
foe_rewrap: bool,
|
|
350
331
|
foe_skip: bool,
|
|
351
|
-
foe_from: ?Move,
|
|
352
332
|
options: anytype,
|
|
353
333
|
) !?Result {
|
|
354
334
|
assert(player_choice.type != .Pass);
|
|
@@ -361,19 +341,19 @@ fn doTurn(
|
|
|
361
341
|
player_choice,
|
|
362
342
|
player_rewrap,
|
|
363
343
|
player_skip,
|
|
364
|
-
|
|
344
|
+
true,
|
|
365
345
|
&residual,
|
|
366
346
|
options,
|
|
367
347
|
)) |r| return r;
|
|
368
348
|
if (!replace) {
|
|
369
349
|
if (player_choice.type != .Switch) {
|
|
370
|
-
if (try checkFaint(battle, foe_player, options)) |r| return r;
|
|
350
|
+
if (try checkFaint(battle, foe_player, true, options)) |r| return r;
|
|
371
351
|
}
|
|
372
352
|
if (residual) try handleResidual(battle, player, options);
|
|
373
|
-
if (try checkFaint(battle, player, options)) |r| return r;
|
|
353
|
+
if (try checkFaint(battle, player, false, options)) |r| return r;
|
|
374
354
|
} else if (foe_choice.type == .Pass) return null;
|
|
375
355
|
|
|
376
|
-
options.chance.clearPending();
|
|
356
|
+
if (!showdown) options.chance.clearPending();
|
|
377
357
|
|
|
378
358
|
residual = true;
|
|
379
359
|
replace = battle.side(foe_player).stored().hp == 0;
|
|
@@ -384,16 +364,16 @@ fn doTurn(
|
|
|
384
364
|
foe_choice,
|
|
385
365
|
foe_rewrap,
|
|
386
366
|
foe_skip,
|
|
387
|
-
|
|
367
|
+
false,
|
|
388
368
|
&residual,
|
|
389
369
|
options,
|
|
390
370
|
)) |r| return r;
|
|
391
371
|
if (!replace) {
|
|
392
372
|
if (!calc and foe_choice.type != .Switch) {
|
|
393
|
-
if (try checkFaint(battle, player, options)) |r| return r;
|
|
373
|
+
if (try checkFaint(battle, player, true, options)) |r| return r;
|
|
394
374
|
}
|
|
395
375
|
if (residual) try handleResidual(battle, foe_player, options);
|
|
396
|
-
if (try checkFaint(battle, foe_player, options)) |r| return r;
|
|
376
|
+
if (try checkFaint(battle, foe_player, false, options)) |r| return r;
|
|
397
377
|
}
|
|
398
378
|
|
|
399
379
|
// Flinch is bugged on Pokémon Showdown because it gets implemented with a duration which causes
|
|
@@ -412,7 +392,7 @@ fn executeMove(
|
|
|
412
392
|
choice: Choice,
|
|
413
393
|
rewrap: bool,
|
|
414
394
|
skip: bool,
|
|
415
|
-
|
|
395
|
+
first: bool,
|
|
416
396
|
residual: *bool,
|
|
417
397
|
options: anytype,
|
|
418
398
|
) !?Result {
|
|
@@ -429,7 +409,7 @@ fn executeMove(
|
|
|
429
409
|
if (side.last_selected_move == .SKIP_TURN) {
|
|
430
410
|
assert(!showdown);
|
|
431
411
|
if (battle.foe(player).active.volatiles.Binding) {
|
|
432
|
-
try options.log.cant(battle.active(player), .Bound);
|
|
412
|
+
try options.log.cant(.{ battle.active(player), .Bound });
|
|
433
413
|
}
|
|
434
414
|
return null;
|
|
435
415
|
}
|
|
@@ -447,11 +427,13 @@ fn executeMove(
|
|
|
447
427
|
mslot = @intCast(battle.lastMove(player).index);
|
|
448
428
|
const stored = side.stored();
|
|
449
429
|
// GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
|
|
450
|
-
auto = side.
|
|
451
|
-
|
|
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);
|
|
452
434
|
// If it wasn't Hyper Beam or the continuation of a move effect then we must have just
|
|
453
435
|
// thawed, in which case we will desync unless the last_selected_move happened to be at
|
|
454
|
-
// index 1 and the current Pokémon has the same move in its first slot
|
|
436
|
+
// index 1 and the current Pokémon has the same move in its first slot
|
|
455
437
|
if (!auto) {
|
|
456
438
|
// side.active.moves(slot) is safe to check even though the slot in question might not
|
|
457
439
|
// technically be from this Pokémon because it must be exactly 1 to not desync and
|
|
@@ -470,7 +452,7 @@ fn executeMove(
|
|
|
470
452
|
|
|
471
453
|
var skip_can = false;
|
|
472
454
|
var skip_pp = false;
|
|
473
|
-
switch (try beforeMove(battle, player, skip,
|
|
455
|
+
switch (try beforeMove(battle, player, skip, first, residual, options)) {
|
|
474
456
|
.done => return null,
|
|
475
457
|
.skip_can => skip_can = true,
|
|
476
458
|
.skip_pp => skip_pp = true,
|
|
@@ -478,9 +460,9 @@ fn executeMove(
|
|
|
478
460
|
.err => return @as(?Result, Result.Error),
|
|
479
461
|
}
|
|
480
462
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
463
|
+
const can = skip_can or
|
|
464
|
+
try canMove(battle, player, mslot, auto, skip_pp, .None, residual, options);
|
|
465
|
+
if (!can) return null;
|
|
484
466
|
|
|
485
467
|
return doMove(battle, player, mslot, rewrap, auto, residual, options);
|
|
486
468
|
}
|
|
@@ -491,7 +473,7 @@ fn beforeMove(
|
|
|
491
473
|
battle: anytype,
|
|
492
474
|
player: Player,
|
|
493
475
|
skip: bool,
|
|
494
|
-
|
|
476
|
+
first: bool,
|
|
495
477
|
residual: *bool,
|
|
496
478
|
options: anytype,
|
|
497
479
|
) !BeforeMove {
|
|
@@ -505,36 +487,41 @@ fn beforeMove(
|
|
|
505
487
|
|
|
506
488
|
if (Status.is(stored.status, .SLP)) {
|
|
507
489
|
const before = stored.status;
|
|
490
|
+
const slf = Status.is(stored.status, .EXT);
|
|
508
491
|
// Even if the EXT bit is set this will still correctly modify the sleep duration
|
|
509
|
-
if (options.calc.
|
|
510
|
-
|
|
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,
|
|
511
498
|
} else {
|
|
512
499
|
stored.status -= 1;
|
|
513
500
|
}
|
|
514
501
|
|
|
515
502
|
const duration = Status.duration(stored.status);
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
503
|
+
try options.chance.sleep(player, if (slf)
|
|
504
|
+
.None
|
|
505
|
+
else if (duration == 0) .ended else .continuing);
|
|
519
506
|
|
|
520
507
|
if (duration == 0) {
|
|
521
|
-
try log.curestatus(ident, before, .Message);
|
|
508
|
+
try log.curestatus(.{ ident, before, .Message });
|
|
522
509
|
stored.status = 0; // clears EXT if present
|
|
523
510
|
} else {
|
|
524
|
-
try log.cant(ident, .Sleep);
|
|
511
|
+
try log.cant(.{ ident, .Sleep });
|
|
525
512
|
}
|
|
526
513
|
side.last_used_move = .None;
|
|
527
514
|
return .done;
|
|
528
515
|
}
|
|
529
516
|
|
|
530
517
|
if (Status.is(stored.status, .FRZ)) {
|
|
531
|
-
try log.cant(ident, .Freeze);
|
|
518
|
+
try log.cant(.{ ident, .Freeze });
|
|
532
519
|
side.last_used_move = .None;
|
|
533
520
|
return .done;
|
|
534
521
|
}
|
|
535
522
|
|
|
536
523
|
if (skip or foe.active.volatiles.Binding) {
|
|
537
|
-
try log.cant(ident, .Bound);
|
|
524
|
+
try log.cant(.{ ident, .Bound });
|
|
538
525
|
return .done;
|
|
539
526
|
}
|
|
540
527
|
|
|
@@ -542,27 +529,27 @@ fn beforeMove(
|
|
|
542
529
|
// Pokémon Showdown doesn't clear Flinch until its imaginary "residual" phase, meaning
|
|
543
530
|
// Pokémon can sometimes flinch multiple times from the same original hit
|
|
544
531
|
if (!showdown) volatiles.Flinch = false;
|
|
545
|
-
try log.cant(ident, .Flinch);
|
|
532
|
+
try log.cant(.{ ident, .Flinch });
|
|
546
533
|
return .done;
|
|
547
534
|
}
|
|
548
535
|
|
|
549
536
|
if (volatiles.Recharging) {
|
|
550
537
|
volatiles.Recharging = false;
|
|
551
|
-
try log.cant(ident, .Recharge);
|
|
538
|
+
try log.cant(.{ ident, .Recharge });
|
|
552
539
|
return .done;
|
|
553
540
|
}
|
|
554
541
|
|
|
555
|
-
if (volatiles.
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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
|
+
);
|
|
562
549
|
|
|
563
|
-
if (volatiles.
|
|
564
|
-
volatiles.
|
|
565
|
-
try log.end(ident, .Disable);
|
|
550
|
+
if (volatiles.disable_duration == 0) {
|
|
551
|
+
volatiles.disable_move = 0;
|
|
552
|
+
try log.end(.{ ident, .Disable });
|
|
566
553
|
}
|
|
567
554
|
}
|
|
568
555
|
// Pokémon Showdown's disable condition has a single onBeforeMove handler
|
|
@@ -577,19 +564,31 @@ fn beforeMove(
|
|
|
577
564
|
|
|
578
565
|
if (volatiles.Confusion) {
|
|
579
566
|
assert(volatiles.confusion > 0);
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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,
|
|
583
581
|
} else {
|
|
584
582
|
volatiles.confusion -= 1;
|
|
585
583
|
}
|
|
586
|
-
|
|
584
|
+
|
|
585
|
+
try options.chance.confusion(player, if (volatiles.confusion == 0) .ended else .continuing);
|
|
587
586
|
|
|
588
587
|
if (volatiles.confusion == 0) {
|
|
589
588
|
volatiles.Confusion = false;
|
|
590
|
-
try log.end(ident, .Confusion);
|
|
589
|
+
try log.end(.{ ident, .Confusion });
|
|
591
590
|
} else {
|
|
592
|
-
try log.activate(ident, .Confusion);
|
|
591
|
+
try log.activate(.{ ident, .Confusion });
|
|
593
592
|
|
|
594
593
|
if (try Rolls.confused(battle, player, options)) {
|
|
595
594
|
assert(!volatiles.MultiHit);
|
|
@@ -601,7 +600,9 @@ fn beforeMove(
|
|
|
601
600
|
volatiles.Charging = false;
|
|
602
601
|
volatiles.Binding = false;
|
|
603
602
|
volatiles.Invulnerable = false;
|
|
604
|
-
|
|
603
|
+
|
|
604
|
+
options.chance.observe(.attacking, player, .None);
|
|
605
|
+
options.chance.observe(.binding, player, .None);
|
|
605
606
|
{
|
|
606
607
|
// This feels (and is) disgusting but the cartridge literally just overwrites
|
|
607
608
|
// the opponent's defense with the user's defense and resets it after. As a
|
|
@@ -609,7 +610,9 @@ fn beforeMove(
|
|
|
609
610
|
const def = foe.active.stats.def;
|
|
610
611
|
foe.active.stats.def = active.stats.def;
|
|
611
612
|
defer foe.active.stats.def = def;
|
|
612
|
-
if (!calcDamage(battle, player, player.foe(), null, false))
|
|
613
|
+
if (!calcDamage(battle, player, player.foe(), null, false, options)) {
|
|
614
|
+
return .err;
|
|
615
|
+
}
|
|
613
616
|
}
|
|
614
617
|
// Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
|
|
615
618
|
// on the targeting behavior of the confused Pokémon's selected move which results
|
|
@@ -638,9 +641,12 @@ fn beforeMove(
|
|
|
638
641
|
volatiles.Thrashing = false;
|
|
639
642
|
volatiles.Charging = false;
|
|
640
643
|
volatiles.Binding = false;
|
|
641
|
-
|
|
644
|
+
|
|
645
|
+
options.chance.observe(.attacking, player, .None);
|
|
646
|
+
options.chance.observe(.binding, player, .None);
|
|
647
|
+
|
|
642
648
|
// GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
|
|
643
|
-
try log.cant(ident, .Paralysis);
|
|
649
|
+
try log.cant(.{ ident, .Paralysis });
|
|
644
650
|
return .done;
|
|
645
651
|
}
|
|
646
652
|
|
|
@@ -656,28 +662,22 @@ fn beforeMove(
|
|
|
656
662
|
volatiles.state +%= battle.last_damage;
|
|
657
663
|
}
|
|
658
664
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
if (options.calc.modify(player, .attacking)) |extend| {
|
|
662
|
-
if (!extend) volatiles.attacks = 0;
|
|
663
|
-
} else {
|
|
664
|
-
volatiles.attacks -= 1;
|
|
665
|
-
}
|
|
666
|
-
try options.chance.attacking(player, volatiles.attacks);
|
|
665
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
666
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
667
667
|
|
|
668
668
|
if (volatiles.attacks != 0) {
|
|
669
|
-
try log.activate(ident, .Bide);
|
|
669
|
+
try log.activate(.{ ident, .Bide });
|
|
670
670
|
return .done;
|
|
671
671
|
}
|
|
672
672
|
|
|
673
673
|
volatiles.Bide = false;
|
|
674
|
-
try log.end(ident, .Bide);
|
|
674
|
+
try log.end(.{ ident, .Bide });
|
|
675
675
|
|
|
676
676
|
battle.last_damage = volatiles.state *% 2;
|
|
677
677
|
volatiles.state = 0;
|
|
678
678
|
|
|
679
679
|
if (battle.last_damage == 0) {
|
|
680
|
-
try log.fail(ident, .None);
|
|
680
|
+
try log.fail(.{ ident, .None });
|
|
681
681
|
return .done;
|
|
682
682
|
}
|
|
683
683
|
|
|
@@ -697,47 +697,38 @@ fn beforeMove(
|
|
|
697
697
|
}
|
|
698
698
|
|
|
699
699
|
if (volatiles.Thrashing) {
|
|
700
|
-
|
|
701
|
-
assert(from != null);
|
|
700
|
+
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
702
701
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
} else {
|
|
706
|
-
volatiles.attacks -= 1;
|
|
707
|
-
}
|
|
708
|
-
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);
|
|
709
704
|
|
|
710
|
-
if (
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
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);
|
|
721
718
|
}
|
|
719
|
+
|
|
720
|
+
// This shouldn't actually set last_used_move, but Pokémon Showdown sets last
|
|
721
|
+
// used in useMove and doesn't have the notion of skipping canMove semantics
|
|
722
|
+
if (showdown) side.last_used_move = side.last_selected_move;
|
|
723
|
+
|
|
722
724
|
return .skip_can;
|
|
723
725
|
}
|
|
724
726
|
|
|
725
727
|
if (volatiles.Binding) {
|
|
726
|
-
|
|
728
|
+
volatiles.attacks = decrement(.binding, player, options, volatiles.attacks);
|
|
729
|
+
try options.chance.binding(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
727
730
|
|
|
728
|
-
|
|
729
|
-
if (!extend) volatiles.attacks = 0;
|
|
730
|
-
} else {
|
|
731
|
-
volatiles.attacks -= 1;
|
|
732
|
-
}
|
|
733
|
-
try options.chance.binding(player, volatiles.attacks);
|
|
734
|
-
|
|
735
|
-
try log.move(
|
|
736
|
-
battle.active(player),
|
|
737
|
-
side.last_selected_move,
|
|
738
|
-
battle.active(player.foe()),
|
|
739
|
-
side.last_selected_move,
|
|
740
|
-
);
|
|
731
|
+
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
741
732
|
if (showdown or battle.last_damage != 0) {
|
|
742
733
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
743
734
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
@@ -757,7 +748,7 @@ fn canMove(
|
|
|
757
748
|
mslot: u4,
|
|
758
749
|
auto: bool,
|
|
759
750
|
skip_pp: bool,
|
|
760
|
-
from:
|
|
751
|
+
from: Move,
|
|
761
752
|
residual: *bool,
|
|
762
753
|
options: anytype,
|
|
763
754
|
) !bool {
|
|
@@ -765,33 +756,21 @@ fn canMove(
|
|
|
765
756
|
const player_ident = battle.active(player);
|
|
766
757
|
const move = Move.get(side.last_selected_move);
|
|
767
758
|
|
|
768
|
-
const foe_last = battle.foe(player).last_used_move;
|
|
769
|
-
const special = from != null and (from.? == .Metronome and
|
|
770
|
-
(from.? == .MirrorMove and !(foe_last == .None or foe_last == .MirrorMove)));
|
|
771
|
-
|
|
772
|
-
var skip = skip_pp;
|
|
773
759
|
if (side.active.volatiles.Charging) {
|
|
774
760
|
side.active.volatiles.Charging = false;
|
|
775
761
|
side.active.volatiles.Invulnerable = false;
|
|
776
762
|
} else if (move.effect == .Charge) {
|
|
777
|
-
try options.log.move(player_ident, side.last_selected_move,
|
|
763
|
+
try options.log.move(.{ player_ident, side.last_selected_move, ID{}, from });
|
|
778
764
|
setCounterable(battle, player, side, move);
|
|
779
765
|
try Effects.charge(battle, player, options);
|
|
780
766
|
return false;
|
|
781
767
|
}
|
|
782
768
|
|
|
783
|
-
|
|
784
|
-
if (!
|
|
769
|
+
side.last_used_move = side.last_selected_move;
|
|
770
|
+
if (!skip_pp) decrementPP(side, mslot, auto);
|
|
785
771
|
|
|
786
772
|
const target = if (move.target == .Self) player else player.foe();
|
|
787
|
-
|
|
788
|
-
// don't have space to be able to track it in all circumstances and thus usually try to infer it
|
|
789
|
-
// from side.last_used_move. However, this gets reset by the opponent switching or fainting,
|
|
790
|
-
// meaning sometimes we lose this information. from is still set because it not being null is
|
|
791
|
-
// relevant for control flow, but we don't have anything useful to put in the log. This is
|
|
792
|
-
// regrettable, but this information is more "nice to have" than required
|
|
793
|
-
const f = if (from != null and from.? == .None) null else from;
|
|
794
|
-
try options.log.move(player_ident, side.last_selected_move, battle.active(target), f);
|
|
773
|
+
try options.log.move(.{ player_ident, side.last_selected_move, battle.active(target), from });
|
|
795
774
|
setCounterable(battle, player, side, move);
|
|
796
775
|
|
|
797
776
|
if (move.effect.onBegin()) {
|
|
@@ -829,16 +808,18 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
|
|
|
829
808
|
assert(!volatiles.Rage and !volatiles.Thrashing and !volatiles.MultiHit);
|
|
830
809
|
if (volatiles.Bide) return;
|
|
831
810
|
|
|
832
|
-
|
|
833
|
-
|
|
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;
|
|
834
814
|
if (volatiles.Transform) return;
|
|
835
815
|
|
|
836
|
-
|
|
837
|
-
|
|
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;
|
|
838
819
|
assert(active.move(mslot).pp == side.stored().move(mslot).pp);
|
|
839
820
|
}
|
|
840
821
|
|
|
841
|
-
fn incrementPP(side: *Side, mslot: u4) void {
|
|
822
|
+
fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
|
|
842
823
|
var active = &side.active;
|
|
843
824
|
const volatiles = &active.volatiles;
|
|
844
825
|
|
|
@@ -846,8 +827,11 @@ fn incrementPP(side: *Side, mslot: u4) void {
|
|
|
846
827
|
// GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
|
|
847
828
|
if (showdown and volatiles.Transform) return;
|
|
848
829
|
|
|
830
|
+
const n = options.calc.overridden(player, .pp) orelse 1;
|
|
831
|
+
if (volatiles.Transform and !options.chance.pp(player, n)) return;
|
|
832
|
+
|
|
849
833
|
assert(mslot > 0 and mslot <= 4);
|
|
850
|
-
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;
|
|
851
835
|
}
|
|
852
836
|
|
|
853
837
|
// Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
|
|
@@ -895,13 +879,13 @@ fn doMove(
|
|
|
895
879
|
break :miss true;
|
|
896
880
|
}
|
|
897
881
|
break :miss (move.target != .Self and
|
|
898
|
-
!moveHit(battle, player, move, &immune, &mist, options));
|
|
882
|
+
!try moveHit(battle, player, move, &immune, &mist, options));
|
|
899
883
|
};
|
|
900
884
|
assert(!immune or miss or (showdown and move.effect == .Binding));
|
|
901
885
|
|
|
902
886
|
var late = showdown and move.effect != .Explode;
|
|
903
887
|
const skip = status or immune;
|
|
904
|
-
if ((!showdown or (!skip or counter)) and !miss)
|
|
888
|
+
if ((!showdown or (!skip or counter)) and !miss) damage: {
|
|
905
889
|
if (showdown and move.effect.isMulti()) {
|
|
906
890
|
try Effects.multiHit(battle, player, move, options);
|
|
907
891
|
hits = side.active.volatiles.attacks;
|
|
@@ -923,8 +907,9 @@ fn doMove(
|
|
|
923
907
|
ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
|
|
924
908
|
// This can overflow after adjustDamage, but will still be sufficient to OHKO
|
|
925
909
|
battle.last_damage = if (ohko) 65535 else 0;
|
|
926
|
-
if (showdown) break :
|
|
927
|
-
} 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);
|
|
928
913
|
return @as(?Result, Result.Error);
|
|
929
914
|
}
|
|
930
915
|
if (battle.last_damage == 0) {
|
|
@@ -938,11 +923,12 @@ fn doMove(
|
|
|
938
923
|
}
|
|
939
924
|
}
|
|
940
925
|
|
|
941
|
-
var
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
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
|
+
}
|
|
946
932
|
|
|
947
933
|
assert(showdown or miss or battle.last_damage > 0 or skip);
|
|
948
934
|
assert((!showdown and miss) or !(ohko and immune));
|
|
@@ -966,16 +952,37 @@ fn doMove(
|
|
|
966
952
|
ohko = (!showdown or (!immune and !invulnerable)) and
|
|
967
953
|
move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
|
|
968
954
|
if (ohko) {
|
|
969
|
-
try log.immune(foe_ident, .OHKO);
|
|
955
|
+
try log.immune(.{ foe_ident, .OHKO });
|
|
970
956
|
} else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
|
|
971
|
-
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
|
+
}
|
|
972
979
|
} else if (mist) {
|
|
973
|
-
if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
|
|
974
|
-
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 });
|
|
975
982
|
} else {
|
|
976
|
-
if (showdown
|
|
977
|
-
try log.lastmiss();
|
|
978
|
-
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)});
|
|
979
986
|
}
|
|
980
987
|
if (move.effect == .JumpKick) {
|
|
981
988
|
// Recoil is supposed to be damage/8 but damage will always be 0 here
|
|
@@ -988,17 +995,15 @@ fn doMove(
|
|
|
988
995
|
try buildRage(battle, player.foe(), options);
|
|
989
996
|
} else if (showdown and move.effect == .Disable) {
|
|
990
997
|
try buildRage(battle, player.foe(), options);
|
|
991
|
-
} else if (!showdown and immune and move.effect == .Binding) {
|
|
992
|
-
try options.chance.commit(player, .binding);
|
|
993
998
|
}
|
|
994
999
|
return null;
|
|
995
|
-
} else {
|
|
1000
|
+
} else if (!showdown) {
|
|
996
1001
|
try options.chance.commit(player, .hit);
|
|
997
1002
|
}
|
|
998
1003
|
|
|
999
1004
|
// On the cartridge MultiHit doesn't get set up until after damage has been applied for the
|
|
1000
1005
|
// first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
|
|
1001
|
-
// it up above before damage calculation)
|
|
1006
|
+
// it up above before damage calculation)
|
|
1002
1007
|
if (!showdown and move.effect.isMulti()) {
|
|
1003
1008
|
try Effects.multiHit(battle, player, move, options);
|
|
1004
1009
|
hits = side.active.volatiles.attacks;
|
|
@@ -1011,15 +1016,15 @@ fn doMove(
|
|
|
1011
1016
|
var hit: u4 = 0;
|
|
1012
1017
|
while (hit < hits) {
|
|
1013
1018
|
if (hit == 0) {
|
|
1014
|
-
if (crit) try log.crit(battle.active(player.foe()));
|
|
1019
|
+
if (crit) try log.crit(.{battle.active(player.foe())});
|
|
1015
1020
|
if (effectiveness > Effectiveness.neutral) {
|
|
1016
|
-
try log.supereffective(battle.active(player.foe()));
|
|
1021
|
+
try log.supereffective(.{battle.active(player.foe())});
|
|
1017
1022
|
} else if (effectiveness < Effectiveness.neutral) {
|
|
1018
|
-
try log.resisted(battle.active(player.foe()));
|
|
1023
|
+
try log.resisted(.{battle.active(player.foe())});
|
|
1019
1024
|
}
|
|
1020
1025
|
}
|
|
1021
1026
|
if (!skip) nullified = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
1022
|
-
if (hit == 0 and ohko) try log.ohko();
|
|
1027
|
+
if (hit == 0 and ohko) try log.ohko(.{});
|
|
1023
1028
|
hit += 1;
|
|
1024
1029
|
if (foe.stored().hp == 0) break;
|
|
1025
1030
|
if (!late and (!sub or move.effect == .Explode)) {
|
|
@@ -1036,7 +1041,7 @@ fn doMove(
|
|
|
1036
1041
|
if (showdown and move.effect == .Twineedle and !nullified and foe.stored().hp > 0) {
|
|
1037
1042
|
try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
1038
1043
|
}
|
|
1039
|
-
try log.hitcount(battle.active(player.foe()), hit);
|
|
1044
|
+
try log.hitcount(.{ battle.active(player.foe()), hit });
|
|
1040
1045
|
} else if (showdown) {
|
|
1041
1046
|
// This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
|
|
1042
1047
|
if (move.effect == .Binding) {
|
|
@@ -1046,9 +1051,9 @@ fn doMove(
|
|
|
1046
1051
|
assert(foe.stored().hp > 0);
|
|
1047
1052
|
// Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
|
|
1048
1053
|
if (sub) {
|
|
1049
|
-
try log.activate(battle.active(player.foe()), .Substitute);
|
|
1054
|
+
try log.activate(.{ battle.active(player.foe()), .Substitute });
|
|
1050
1055
|
} else {
|
|
1051
|
-
try log.damage(battle.active(player.foe()), foe.stored(), .None);
|
|
1056
|
+
try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
|
|
1052
1057
|
try buildRage(battle, player.foe(), options);
|
|
1053
1058
|
}
|
|
1054
1059
|
return null;
|
|
@@ -1057,12 +1062,12 @@ fn doMove(
|
|
|
1057
1062
|
}
|
|
1058
1063
|
|
|
1059
1064
|
// Substitute being broken nullifies the move's effect completely so even
|
|
1060
|
-
// if an effect was intended to "always happen" it will still get skipped
|
|
1065
|
+
// if an effect was intended to "always happen" it will still get skipped
|
|
1061
1066
|
if (nullified) return null;
|
|
1062
1067
|
|
|
1063
1068
|
// On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
|
|
1064
1069
|
// but this is only done to setup the MultiHit looping in the first place. Moving the MultiHit
|
|
1065
|
-
// setup before the loop means we can avoid having to waste time doing no-op handler searches
|
|
1070
|
+
// setup before the loop means we can avoid having to waste time doing no-op handler searches
|
|
1066
1071
|
if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
|
|
1067
1072
|
|
|
1068
1073
|
if (foe.stored().hp == 0) return null;
|
|
@@ -1077,7 +1082,7 @@ fn doMove(
|
|
|
1077
1082
|
// below). For Twineedle we change the data to that of one with PoisonChance1 given its
|
|
1078
1083
|
// MultiHit behavior is complete after the loop above, though Pokémon Showdown handles the
|
|
1079
1084
|
// Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
|
|
1080
|
-
// puts the |-status| message before |-hitcount| instead of after
|
|
1085
|
+
// puts the |-status| message before |-hitcount| instead of after
|
|
1081
1086
|
if (move.effect == .Twineedle) {
|
|
1082
1087
|
if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
1083
1088
|
} else if (move.effect == .Disable) {
|
|
@@ -1096,12 +1101,14 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
|
|
|
1096
1101
|
const side = battle.side(player);
|
|
1097
1102
|
|
|
1098
1103
|
// Base speed is used for the critical hit calculation, even when Transform-ed
|
|
1099
|
-
var rate =
|
|
1104
|
+
var rate: u16 = Species.chance(side.stored().species);
|
|
1100
1105
|
// GLITCH: Focus Energy reduces critical hit rate instead of increasing it
|
|
1101
1106
|
rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
|
|
1102
1107
|
rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
|
|
1103
1108
|
|
|
1104
|
-
|
|
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);
|
|
1105
1112
|
}
|
|
1106
1113
|
|
|
1107
1114
|
fn calcDamage(
|
|
@@ -1110,6 +1117,7 @@ fn calcDamage(
|
|
|
1110
1117
|
target_player: Player,
|
|
1111
1118
|
m: ?Move.Data,
|
|
1112
1119
|
crit: bool,
|
|
1120
|
+
options: anytype,
|
|
1113
1121
|
) bool {
|
|
1114
1122
|
// Confusion (indicated when m == null) just needs a 40 BP physical move
|
|
1115
1123
|
const cfz = m == null;
|
|
@@ -1121,28 +1129,8 @@ fn calcDamage(
|
|
|
1121
1129
|
|
|
1122
1130
|
const special = move.type.special();
|
|
1123
1131
|
|
|
1124
|
-
|
|
1125
|
-
var
|
|
1126
|
-
if (crit)
|
|
1127
|
-
if (special) side.stored().stats.spc
|
|
1128
|
-
else side.stored().stats.atk
|
|
1129
|
-
else
|
|
1130
|
-
if (special) side.active.stats.spc
|
|
1131
|
-
else side.active.stats.atk;
|
|
1132
|
-
var def: u32 =
|
|
1133
|
-
if (crit)
|
|
1134
|
-
if (special) target.stored().stats.spc
|
|
1135
|
-
else target.stored().stats.def
|
|
1136
|
-
else
|
|
1137
|
-
// GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
|
|
1138
|
-
if (special)
|
|
1139
|
-
target.active.stats.spc *
|
|
1140
|
-
@as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
|
|
1141
|
-
// Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
|
|
1142
|
-
else
|
|
1143
|
-
target.active.stats.def *
|
|
1144
|
-
@as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
|
|
1145
|
-
// zig fmt: on
|
|
1132
|
+
var atk = attack(side, crit, special);
|
|
1133
|
+
var def = defense(target, crit, special, cfz);
|
|
1146
1134
|
|
|
1147
1135
|
// Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
|
|
1148
1136
|
// will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
|
|
@@ -1153,9 +1141,9 @@ fn calcDamage(
|
|
|
1153
1141
|
def = @max((def / 4) & 255, if (showdown) 1 else 0);
|
|
1154
1142
|
}
|
|
1155
1143
|
|
|
1156
|
-
const lvl =
|
|
1144
|
+
const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
|
|
1157
1145
|
|
|
1158
|
-
def =
|
|
1146
|
+
def = if (move.effect == .Explode) @max(def / 2, 1) else def;
|
|
1159
1147
|
|
|
1160
1148
|
if (def == 0) return false;
|
|
1161
1149
|
|
|
@@ -1167,11 +1155,41 @@ fn calcDamage(
|
|
|
1167
1155
|
d = @min(997, d);
|
|
1168
1156
|
d += 2;
|
|
1169
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
|
+
|
|
1170
1168
|
battle.last_damage = @intCast(d);
|
|
1171
1169
|
|
|
1172
1170
|
return true;
|
|
1173
1171
|
}
|
|
1174
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
|
+
|
|
1175
1193
|
fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
1176
1194
|
const side = battle.side(player);
|
|
1177
1195
|
const foe = battle.foe(player);
|
|
@@ -1216,7 +1234,6 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1216
1234
|
const foe = battle.foe(player);
|
|
1217
1235
|
|
|
1218
1236
|
if (!try checkHit(battle, player, move, options)) return null;
|
|
1219
|
-
try options.chance.commit(player, .hit);
|
|
1220
1237
|
|
|
1221
1238
|
battle.last_damage = switch (side.last_selected_move) {
|
|
1222
1239
|
.SuperFang => @max(foe.stored().hp / 2, 1),
|
|
@@ -1246,7 +1263,7 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1246
1263
|
const foe = battle.foe(player);
|
|
1247
1264
|
|
|
1248
1265
|
if (battle.last_damage == 0) {
|
|
1249
|
-
try options.log.fail(battle.active(player), .None);
|
|
1266
|
+
try options.log.fail(.{ battle.active(player), .None });
|
|
1250
1267
|
return null;
|
|
1251
1268
|
}
|
|
1252
1269
|
|
|
@@ -1264,22 +1281,21 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1264
1281
|
foe_last_selected_move.type == .Fighting);
|
|
1265
1282
|
|
|
1266
1283
|
if (!used and !selected) {
|
|
1267
|
-
try options.log.fail(battle.active(player), .None);
|
|
1284
|
+
try options.log.fail(.{ battle.active(player), .None });
|
|
1268
1285
|
return null;
|
|
1269
1286
|
}
|
|
1270
1287
|
|
|
1271
1288
|
if (!used or !selected) {
|
|
1272
1289
|
// GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
|
|
1273
1290
|
if (!showdown) return Result.Error;
|
|
1274
|
-
try options.log.fail(battle.active(player), .None);
|
|
1291
|
+
try options.log.fail(.{ battle.active(player), .None });
|
|
1275
1292
|
return null;
|
|
1276
1293
|
}
|
|
1277
1294
|
|
|
1278
1295
|
battle.last_damage *|= 2;
|
|
1279
1296
|
|
|
1280
|
-
// Pokémon Showdown calls
|
|
1297
|
+
// Pokémon Showdown calls moveHit before Counter
|
|
1281
1298
|
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
1282
|
-
try options.chance.commit(player, .hit);
|
|
1283
1299
|
|
|
1284
1300
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1285
1301
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
@@ -1307,20 +1323,20 @@ fn applyDamage(
|
|
|
1307
1323
|
subbed.active.volatiles.substitute = 0;
|
|
1308
1324
|
subbed.active.volatiles.Substitute = false;
|
|
1309
1325
|
// battle.last_damage is not updated with the amount of HP the Substitute had
|
|
1310
|
-
try options.log.end(battle.active(sub_player), .Substitute);
|
|
1311
|
-
options.calc.capped(
|
|
1326
|
+
try options.log.end(.{ battle.active(sub_player), .Substitute });
|
|
1327
|
+
options.calc.capped(sub_player);
|
|
1312
1328
|
return true;
|
|
1313
1329
|
} else {
|
|
1314
1330
|
// Safe to truncate since less than subbed.volatiles.substitute which is a u8
|
|
1315
1331
|
subbed.active.volatiles.substitute -= @intCast(battle.last_damage);
|
|
1316
|
-
try options.log.activate(battle.active(sub_player), .Substitute);
|
|
1332
|
+
try options.log.activate(.{ battle.active(sub_player), .Substitute });
|
|
1317
1333
|
return false;
|
|
1318
1334
|
}
|
|
1319
1335
|
}
|
|
1320
1336
|
|
|
1321
1337
|
if (battle.last_damage > target.stored().hp) battle.last_damage = target.stored().hp;
|
|
1322
1338
|
target.stored().hp -= battle.last_damage;
|
|
1323
|
-
try options.log.damage(battle.active(target_player), target.stored(), reason);
|
|
1339
|
+
try options.log.damage(.{ battle.active(target_player), target.stored(), reason });
|
|
1324
1340
|
return false;
|
|
1325
1341
|
}
|
|
1326
1342
|
|
|
@@ -1339,10 +1355,10 @@ fn mirrorMove(
|
|
|
1339
1355
|
side.last_selected_move = foe.last_used_move;
|
|
1340
1356
|
|
|
1341
1357
|
if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
|
|
1342
|
-
try options.log.fail(battle.active(player), .None);
|
|
1358
|
+
try options.log.fail(.{ battle.active(player), .None });
|
|
1343
1359
|
return null;
|
|
1344
1360
|
} else if (!showdown or foe.last_used_move != .Struggle) {
|
|
1345
|
-
incrementPP(side, mslot);
|
|
1361
|
+
incrementPP(side, player, mslot, options);
|
|
1346
1362
|
}
|
|
1347
1363
|
|
|
1348
1364
|
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
|
|
@@ -1363,7 +1379,7 @@ fn metronome(
|
|
|
1363
1379
|
var side = battle.side(player);
|
|
1364
1380
|
|
|
1365
1381
|
side.last_selected_move = try Rolls.metronome(battle, player, options);
|
|
1366
|
-
incrementPP(side, mslot);
|
|
1382
|
+
incrementPP(side, player, mslot, options);
|
|
1367
1383
|
|
|
1368
1384
|
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
|
|
1369
1385
|
return null;
|
|
@@ -1375,20 +1391,23 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
|
|
|
1375
1391
|
var immune = false;
|
|
1376
1392
|
var mist = false;
|
|
1377
1393
|
|
|
1378
|
-
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
|
+
}
|
|
1379
1398
|
|
|
1380
1399
|
assert(!immune);
|
|
1381
1400
|
if (mist) {
|
|
1382
1401
|
assert(!showdown);
|
|
1383
1402
|
const foe_ident = battle.active(player.foe());
|
|
1384
|
-
try options.log.activate(foe_ident, .Mist);
|
|
1385
|
-
try options.log.fail(foe_ident, .None);
|
|
1403
|
+
try options.log.activate(.{ foe_ident, .Mist });
|
|
1404
|
+
try options.log.fail(.{ foe_ident, .None });
|
|
1386
1405
|
} else {
|
|
1387
|
-
try options.
|
|
1388
|
-
try options.log.
|
|
1389
|
-
try options.log.miss(battle.active(player));
|
|
1406
|
+
try options.log.lastmiss(.{});
|
|
1407
|
+
try options.log.miss(.{battle.active(player)});
|
|
1390
1408
|
}
|
|
1391
1409
|
|
|
1410
|
+
if (!showdown) try options.chance.commit(player, .miss);
|
|
1392
1411
|
return false;
|
|
1393
1412
|
}
|
|
1394
1413
|
|
|
@@ -1399,7 +1418,7 @@ fn moveHit(
|
|
|
1399
1418
|
immune: *bool,
|
|
1400
1419
|
mist: *bool,
|
|
1401
1420
|
options: anytype,
|
|
1402
|
-
) bool {
|
|
1421
|
+
) !bool {
|
|
1403
1422
|
var side = battle.side(player);
|
|
1404
1423
|
const foe = battle.foe(player);
|
|
1405
1424
|
|
|
@@ -1438,7 +1457,7 @@ fn moveHit(
|
|
|
1438
1457
|
const overwritten = overwrite and state > 0;
|
|
1439
1458
|
assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
|
|
1440
1459
|
|
|
1441
|
-
var accuracy = if (overwritten) state else
|
|
1460
|
+
var accuracy: u16 = if (overwritten) state else move.accuracy;
|
|
1442
1461
|
var boost = BOOSTS[@as(u4, @intCast(@as(i8, side.active.boosts.accuracy) + 6))];
|
|
1443
1462
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1444
1463
|
boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
|
|
@@ -1450,7 +1469,7 @@ fn moveHit(
|
|
|
1450
1469
|
if (overwrite) side.active.volatiles.state = accuracy;
|
|
1451
1470
|
|
|
1452
1471
|
// GLITCH: max accuracy is 255 so 1/256 chance of miss
|
|
1453
|
-
break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
|
|
1472
|
+
break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
|
|
1454
1473
|
};
|
|
1455
1474
|
|
|
1456
1475
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1461,13 +1480,17 @@ fn moveHit(
|
|
|
1461
1480
|
|
|
1462
1481
|
if (!miss) return true;
|
|
1463
1482
|
if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
|
|
1483
|
+
|
|
1464
1484
|
side.active.volatiles.Binding = false;
|
|
1485
|
+
options.chance.observe(.binding, player, .None);
|
|
1486
|
+
|
|
1465
1487
|
return false;
|
|
1466
1488
|
}
|
|
1467
1489
|
|
|
1468
1490
|
fn checkFaint(
|
|
1469
1491
|
battle: anytype,
|
|
1470
1492
|
player: Player,
|
|
1493
|
+
cap: bool,
|
|
1471
1494
|
options: anytype,
|
|
1472
1495
|
) @TypeOf(options.log).Error!?Result {
|
|
1473
1496
|
const side = battle.side(player);
|
|
@@ -1477,33 +1500,32 @@ fn checkFaint(
|
|
|
1477
1500
|
const foe_fainted = foe.stored().hp == 0;
|
|
1478
1501
|
|
|
1479
1502
|
const player_out = findFirstAlive(side) == 0;
|
|
1480
|
-
const foe_out = findFirstAlive(foe) == 0;
|
|
1481
|
-
const
|
|
1482
|
-
const more = tie or player_out or foe_out;
|
|
1503
|
+
const foe_out = foe_fainted and findFirstAlive(foe) == 0;
|
|
1504
|
+
const more = player_out or foe_out;
|
|
1483
1505
|
|
|
1484
|
-
if (try faint(battle, player, !(more or foe_fainted), options)) |r| return r;
|
|
1485
|
-
if (foe_fainted) if (try faint(battle, player.foe(), !more, options)) |r| return r;
|
|
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;
|
|
1486
1508
|
|
|
1487
1509
|
assert(!side.active.volatiles.MultiHit);
|
|
1488
1510
|
assert(!foe.active.volatiles.MultiHit);
|
|
1489
1511
|
|
|
1490
|
-
if (
|
|
1491
|
-
try options.log.tie();
|
|
1512
|
+
if (player_out and foe_out) {
|
|
1513
|
+
try options.log.tie(.{});
|
|
1492
1514
|
return Result.Tie;
|
|
1493
1515
|
} else if (player_out) {
|
|
1494
|
-
try options.log.win(player.foe());
|
|
1516
|
+
try options.log.win(.{player.foe()});
|
|
1495
1517
|
return if (player == .P1) Result.Lose else Result.Win;
|
|
1496
1518
|
} else if (foe_out) {
|
|
1497
|
-
try options.log.win(player);
|
|
1519
|
+
try options.log.win(.{player});
|
|
1498
1520
|
return if (player == .P1) Result.Win else Result.Lose;
|
|
1499
1521
|
}
|
|
1500
1522
|
|
|
1501
1523
|
const foe_choice: Choice.Type = if (foe_fainted) .Switch else .Pass;
|
|
1502
|
-
if (player == .P1) return
|
|
1503
|
-
return
|
|
1524
|
+
if (player == .P1) return .{ .p1 = .Switch, .p2 = foe_choice };
|
|
1525
|
+
return .{ .p1 = foe_choice, .p2 = .Switch };
|
|
1504
1526
|
}
|
|
1505
1527
|
|
|
1506
|
-
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 {
|
|
1507
1529
|
var side = battle.side(player);
|
|
1508
1530
|
var foe = battle.foe(player);
|
|
1509
1531
|
assert(side.stored().hp == 0);
|
|
@@ -1536,8 +1558,10 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
|
|
|
1536
1558
|
side.stored().stats.spe;
|
|
1537
1559
|
}
|
|
1538
1560
|
|
|
1539
|
-
try options.log.faint(battle.active(player), done);
|
|
1540
|
-
|
|
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);
|
|
1541
1565
|
return null;
|
|
1542
1566
|
}
|
|
1543
1567
|
|
|
@@ -1548,21 +1572,19 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
|
|
|
1548
1572
|
var volatiles = &side.active.volatiles;
|
|
1549
1573
|
|
|
1550
1574
|
const brn = Status.is(stored.status, .BRN);
|
|
1551
|
-
if (brn or Status.is(stored.status, .PSN))
|
|
1575
|
+
if (brn or Status.is(stored.status, .PSN)) {
|
|
1552
1576
|
var damage = @max(stored.stats.hp / 16, 1);
|
|
1553
|
-
|
|
1554
1577
|
if (volatiles.Toxic) {
|
|
1555
1578
|
volatiles.toxic += 1;
|
|
1556
1579
|
damage *= volatiles.toxic;
|
|
1557
1580
|
}
|
|
1581
|
+
damage = @min(damage, stored.hp);
|
|
1558
1582
|
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
// Pokémon Showdown uses damageOf here but its not relevant in Generation I
|
|
1565
|
-
try options.log.damage(ident, stored, if (brn) Damage.Burn else Damage.Poison);
|
|
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
|
+
}
|
|
1566
1588
|
}
|
|
1567
1589
|
|
|
1568
1590
|
if (volatiles.LeechSeed) {
|
|
@@ -1586,14 +1608,14 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
|
|
|
1586
1608
|
const amount = @min(damage, stored.hp);
|
|
1587
1609
|
stored.hp -= amount;
|
|
1588
1610
|
|
|
1589
|
-
// As above, Pokémon Showdown uses
|
|
1590
|
-
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 });
|
|
1591
1613
|
|
|
1592
1614
|
const before = foe_stored.hp;
|
|
1593
1615
|
// Uncapped damage is added back to the foe
|
|
1594
1616
|
foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
|
|
1595
1617
|
// Pokémon Showdown uses the less specific heal here instead of drain... because reasons?
|
|
1596
|
-
if (foe_stored.hp > before) try options.log.heal(foe_ident, foe_stored, .Silent);
|
|
1618
|
+
if (foe_stored.hp > before) try options.log.heal(.{ foe_ident, foe_stored, .Silent });
|
|
1597
1619
|
}
|
|
1598
1620
|
}
|
|
1599
1621
|
|
|
@@ -1604,18 +1626,18 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
|
|
|
1604
1626
|
battle.turn += 1;
|
|
1605
1627
|
|
|
1606
1628
|
if (showdown and pkmn.options.ebc and checkEBC(battle)) {
|
|
1607
|
-
try options.log.tie();
|
|
1629
|
+
try options.log.tie(.{});
|
|
1608
1630
|
return Result.Tie;
|
|
1609
1631
|
}
|
|
1610
1632
|
|
|
1611
1633
|
if (showdown and battle.turn >= 1000) {
|
|
1612
|
-
try options.log.tie();
|
|
1634
|
+
try options.log.tie(.{});
|
|
1613
1635
|
return Result.Tie;
|
|
1614
1636
|
} else if (battle.turn >= 65535) {
|
|
1615
1637
|
return Result.Error;
|
|
1616
1638
|
}
|
|
1617
1639
|
|
|
1618
|
-
try options.log.turn(battle.turn);
|
|
1640
|
+
try options.log.turn(.{battle.turn});
|
|
1619
1641
|
|
|
1620
1642
|
return Result.Default;
|
|
1621
1643
|
}
|
|
@@ -1675,9 +1697,9 @@ fn checkEBC(battle: anytype) bool {
|
|
|
1675
1697
|
}
|
|
1676
1698
|
|
|
1677
1699
|
fn disabled(side: *Side, ident: ID, options: anytype) !bool {
|
|
1678
|
-
if (side.active.volatiles.
|
|
1700
|
+
if (side.active.volatiles.disable_move != 0) {
|
|
1679
1701
|
// A Pokémon that transforms after being disabled may end up with less move slots
|
|
1680
|
-
const m = side.active.moves[side.active.volatiles.
|
|
1702
|
+
const m = side.active.moves[side.active.volatiles.disable_move - 1].id;
|
|
1681
1703
|
// side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
|
|
1682
1704
|
const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
|
|
1683
1705
|
m
|
|
@@ -1685,32 +1707,21 @@ fn disabled(side: *Side, ident: ID, options: anytype) !bool {
|
|
|
1685
1707
|
side.last_selected_move;
|
|
1686
1708
|
if (m != .None and m == last) {
|
|
1687
1709
|
side.active.volatiles.Charging = false;
|
|
1688
|
-
try options.log.
|
|
1710
|
+
try options.log.cant(.{ ident, .Disable, last });
|
|
1689
1711
|
return true;
|
|
1690
1712
|
}
|
|
1691
1713
|
}
|
|
1692
1714
|
return false;
|
|
1693
1715
|
}
|
|
1694
1716
|
|
|
1695
|
-
|
|
1717
|
+
fn buildRage(battle: anytype, who: Player, options: anytype) !void {
|
|
1696
1718
|
const side = battle.side(who);
|
|
1697
1719
|
if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
|
|
1698
1720
|
try Effects.boost(battle, who, Move.get(.Rage), options);
|
|
1699
1721
|
}
|
|
1700
1722
|
}
|
|
1701
1723
|
|
|
1702
|
-
fn
|
|
1703
|
-
var volatiles = &active.volatiles;
|
|
1704
|
-
assert(volatiles.Thrashing);
|
|
1705
|
-
if (volatiles.attacks > 0) return false;
|
|
1706
|
-
|
|
1707
|
-
volatiles.Thrashing = false;
|
|
1708
|
-
volatiles.Confusion = true;
|
|
1709
|
-
volatiles.confusion = Rolls.confusionDuration(battle, player, true, options);
|
|
1710
|
-
return true;
|
|
1711
|
-
}
|
|
1712
|
-
|
|
1713
|
-
inline fn onBegin(
|
|
1724
|
+
fn onBegin(
|
|
1714
1725
|
battle: anytype,
|
|
1715
1726
|
player: Player,
|
|
1716
1727
|
move: Move.Data,
|
|
@@ -1718,7 +1729,6 @@ inline fn onBegin(
|
|
|
1718
1729
|
residual: *bool,
|
|
1719
1730
|
options: anytype,
|
|
1720
1731
|
) !void {
|
|
1721
|
-
assert(move.effect.onBegin());
|
|
1722
1732
|
return switch (move.effect) {
|
|
1723
1733
|
.Confusion => Effects.confusion(battle, player, move, options),
|
|
1724
1734
|
.Conversion => Effects.conversion(battle, player, options),
|
|
@@ -1740,8 +1750,7 @@ inline fn onBegin(
|
|
|
1740
1750
|
};
|
|
1741
1751
|
}
|
|
1742
1752
|
|
|
1743
|
-
|
|
1744
|
-
assert(move.effect.onEnd());
|
|
1753
|
+
fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1745
1754
|
return switch (move.effect) {
|
|
1746
1755
|
.Bide => Effects.bide(battle, player, options),
|
|
1747
1756
|
// zig fmt: off
|
|
@@ -1756,14 +1765,13 @@ inline fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anyty
|
|
|
1756
1765
|
};
|
|
1757
1766
|
}
|
|
1758
1767
|
|
|
1759
|
-
|
|
1768
|
+
fn alwaysHappens(
|
|
1760
1769
|
battle: anytype,
|
|
1761
1770
|
player: Player,
|
|
1762
1771
|
move: Move.Data,
|
|
1763
1772
|
residual: *bool,
|
|
1764
1773
|
options: anytype,
|
|
1765
1774
|
) !void {
|
|
1766
|
-
assert(move.effect.alwaysHappens());
|
|
1767
1775
|
return switch (move.effect) {
|
|
1768
1776
|
.DrainHP, .DreamEater => Effects.drainHP(battle, player, options),
|
|
1769
1777
|
.Explode => Effects.explode(battle, player),
|
|
@@ -1775,7 +1783,7 @@ inline fn alwaysHappens(
|
|
|
1775
1783
|
};
|
|
1776
1784
|
}
|
|
1777
1785
|
|
|
1778
|
-
|
|
1786
|
+
fn moveEffect(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1779
1787
|
return switch (move.effect) {
|
|
1780
1788
|
.BurnChance1, .BurnChance2 => Effects.burnChance(battle, player, move, options),
|
|
1781
1789
|
.ConfusionChance => Effects.confusion(battle, player, move, options),
|
|
@@ -1799,10 +1807,12 @@ pub const Effects = struct {
|
|
|
1799
1807
|
|
|
1800
1808
|
side.active.volatiles.Bide = true;
|
|
1801
1809
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1810
|
+
|
|
1802
1811
|
side.active.volatiles.state = 0;
|
|
1803
1812
|
side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
1813
|
+
options.chance.observe(.attacking, player, .started);
|
|
1804
1814
|
|
|
1805
|
-
try options.log.start(battle.active(player), .Bide);
|
|
1815
|
+
try options.log.start(.{ battle.active(player), .Bide });
|
|
1806
1816
|
}
|
|
1807
1817
|
|
|
1808
1818
|
fn burnChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -1816,11 +1826,11 @@ pub const Effects = struct {
|
|
|
1816
1826
|
// GLITCH: Freeze top move selection desync can occur if thawed Pokémon is slower
|
|
1817
1827
|
if (Status.is(foe_stored.status, .FRZ)) {
|
|
1818
1828
|
assert(move.type == .Fire);
|
|
1819
|
-
try options.log.curestatus(
|
|
1829
|
+
try options.log.curestatus(.{
|
|
1820
1830
|
battle.active(player.foe()),
|
|
1821
1831
|
foe_stored.status,
|
|
1822
1832
|
.Message,
|
|
1823
|
-
);
|
|
1833
|
+
});
|
|
1824
1834
|
foe_stored.status = 0;
|
|
1825
1835
|
}
|
|
1826
1836
|
return;
|
|
@@ -1834,7 +1844,7 @@ pub const Effects = struct {
|
|
|
1834
1844
|
foe_stored.status = Status.init(.BRN);
|
|
1835
1845
|
foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
|
|
1836
1846
|
|
|
1837
|
-
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 });
|
|
1838
1848
|
}
|
|
1839
1849
|
|
|
1840
1850
|
fn charge(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -1844,8 +1854,8 @@ pub const Effects = struct {
|
|
|
1844
1854
|
volatiles.Charging = true;
|
|
1845
1855
|
const move = side.last_selected_move;
|
|
1846
1856
|
if (move == .Fly or move == .Dig) volatiles.Invulnerable = true;
|
|
1847
|
-
try options.log.laststill();
|
|
1848
|
-
try options.log.prepare(battle.active(player), move);
|
|
1857
|
+
try options.log.laststill(.{});
|
|
1858
|
+
try options.log.prepare(.{ battle.active(player), move });
|
|
1849
1859
|
}
|
|
1850
1860
|
|
|
1851
1861
|
fn confusion(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -1856,42 +1866,38 @@ pub const Effects = struct {
|
|
|
1856
1866
|
if (!try Rolls.confusionChance(battle, player, options)) return;
|
|
1857
1867
|
} else {
|
|
1858
1868
|
if (showdown) {
|
|
1859
|
-
if (!try checkHit(battle, player, move, options))
|
|
1860
|
-
|
|
1861
|
-
} else if (sub) {
|
|
1862
|
-
return options.log.fail(battle.active(player.foe()), .None);
|
|
1863
|
-
}
|
|
1869
|
+
if (!try checkHit(battle, player, move, options)) return;
|
|
1870
|
+
if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
1864
1871
|
} else {
|
|
1865
|
-
if (sub) {
|
|
1866
|
-
|
|
1867
|
-
} else if (!try checkHit(battle, player, move, options)) {
|
|
1868
|
-
return;
|
|
1869
|
-
}
|
|
1872
|
+
if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
1873
|
+
if (!try checkHit(battle, player, move, options)) return;
|
|
1870
1874
|
}
|
|
1871
|
-
try options.chance.commit(player, .hit);
|
|
1872
1875
|
}
|
|
1873
1876
|
|
|
1874
1877
|
if (foe.active.volatiles.Confusion) return;
|
|
1875
1878
|
foe.active.volatiles.Confusion = true;
|
|
1876
|
-
foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
|
|
1877
1879
|
|
|
1878
|
-
|
|
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 });
|
|
1879
1884
|
}
|
|
1880
1885
|
|
|
1881
1886
|
fn conversion(battle: anytype, player: Player, options: anytype) !void {
|
|
1882
1887
|
const foe = battle.foe(player);
|
|
1883
1888
|
|
|
1884
1889
|
if (foe.active.volatiles.Invulnerable) {
|
|
1885
|
-
try options.log.lastmiss();
|
|
1886
|
-
return options.log.miss(battle.active(player));
|
|
1890
|
+
try options.log.lastmiss(.{});
|
|
1891
|
+
return options.log.miss(.{battle.active(player)});
|
|
1887
1892
|
}
|
|
1888
1893
|
|
|
1889
1894
|
battle.side(player).active.types = foe.active.types;
|
|
1890
|
-
return options.log.
|
|
1895
|
+
return options.log.start(.{
|
|
1891
1896
|
battle.active(player),
|
|
1897
|
+
.TypeChange,
|
|
1892
1898
|
foe.active.types,
|
|
1893
1899
|
battle.active(player.foe()),
|
|
1894
|
-
);
|
|
1900
|
+
});
|
|
1895
1901
|
}
|
|
1896
1902
|
|
|
1897
1903
|
fn disable(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
|
|
@@ -1900,13 +1906,10 @@ pub const Effects = struct {
|
|
|
1900
1906
|
const foe_ident = battle.active(player.foe());
|
|
1901
1907
|
|
|
1902
1908
|
// Pokémon Showdown handles hit/miss earlier in doMove
|
|
1903
|
-
if (!showdown)
|
|
1904
|
-
if (!try checkHit(battle, player, move, options)) return null;
|
|
1905
|
-
try options.chance.commit(player, .hit);
|
|
1906
|
-
}
|
|
1909
|
+
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
1907
1910
|
|
|
1908
|
-
if (volatiles.
|
|
1909
|
-
try options.log.fail(foe_ident, .None);
|
|
1911
|
+
if (volatiles.disable_move != 0) {
|
|
1912
|
+
try options.log.fail(.{ foe_ident, .None });
|
|
1910
1913
|
return null;
|
|
1911
1914
|
}
|
|
1912
1915
|
|
|
@@ -1922,7 +1925,7 @@ pub const Effects = struct {
|
|
|
1922
1925
|
// Technically this is still considered simply a "miss" on the cartridge,
|
|
1923
1926
|
// but diverging from Pokémon Showdown here would mostly just be pedantic
|
|
1924
1927
|
if (n == 0) {
|
|
1925
|
-
try options.log.fail(foe_ident, .None);
|
|
1928
|
+
try options.log.fail(.{ foe_ident, .None });
|
|
1926
1929
|
return null;
|
|
1927
1930
|
} else if (err) {
|
|
1928
1931
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
@@ -1930,18 +1933,19 @@ pub const Effects = struct {
|
|
|
1930
1933
|
return Result.Error;
|
|
1931
1934
|
}
|
|
1932
1935
|
|
|
1933
|
-
volatiles.
|
|
1936
|
+
volatiles.disable_move =
|
|
1934
1937
|
@intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
|
|
1935
|
-
volatiles.
|
|
1938
|
+
volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
|
|
1939
|
+
options.chance.observe(.disable, player.foe(), .started);
|
|
1936
1940
|
|
|
1937
|
-
const id = foe.active.move(volatiles.
|
|
1938
|
-
try options.log.
|
|
1941
|
+
const id = foe.active.move(volatiles.disable_move).id;
|
|
1942
|
+
try options.log.start(.{ foe_ident, .Disable, id });
|
|
1939
1943
|
return null;
|
|
1940
1944
|
}
|
|
1941
1945
|
|
|
1942
1946
|
fn drainHP(battle: anytype, player: Player, options: anytype) !void {
|
|
1943
|
-
var
|
|
1944
|
-
|
|
1947
|
+
var stored = battle.side(player).stored();
|
|
1948
|
+
const ident = battle.active(player);
|
|
1945
1949
|
|
|
1946
1950
|
if (battle.last_damage == 0) {
|
|
1947
1951
|
assert(showdown);
|
|
@@ -1954,7 +1958,7 @@ pub const Effects = struct {
|
|
|
1954
1958
|
if (stored.hp == stored.stats.hp) return;
|
|
1955
1959
|
stored.hp = @min(stored.stats.hp, stored.hp + drain);
|
|
1956
1960
|
|
|
1957
|
-
try options.log.
|
|
1961
|
+
try options.log.heal(.{ ident, stored, .Drain, battle.active(player.foe()) });
|
|
1958
1962
|
}
|
|
1959
1963
|
|
|
1960
1964
|
fn explode(battle: anytype, player: Player) !void {
|
|
@@ -1986,7 +1990,7 @@ pub const Effects = struct {
|
|
|
1986
1990
|
if (side.active.volatiles.FocusEnergy) return;
|
|
1987
1991
|
side.active.volatiles.FocusEnergy = true;
|
|
1988
1992
|
|
|
1989
|
-
try options.log.start(battle.active(player), .FocusEnergy);
|
|
1993
|
+
try options.log.start(.{ battle.active(player), .FocusEnergy });
|
|
1990
1994
|
}
|
|
1991
1995
|
|
|
1992
1996
|
fn freezeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2008,7 +2012,7 @@ pub const Effects = struct {
|
|
|
2008
2012
|
foe_stored.status = Status.init(.FRZ);
|
|
2009
2013
|
// GLITCH: Hyper Beam recharging status is not cleared
|
|
2010
2014
|
|
|
2011
|
-
try options.log.status(foe_ident, foe_stored.status, .None);
|
|
2015
|
+
try options.log.status(.{ foe_ident, foe_stored.status, .None });
|
|
2012
2016
|
}
|
|
2013
2017
|
|
|
2014
2018
|
fn haze(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -2016,7 +2020,7 @@ pub const Effects = struct {
|
|
|
2016
2020
|
var side = battle.side(player);
|
|
2017
2021
|
var foe = battle.foe(player);
|
|
2018
2022
|
|
|
2019
|
-
|
|
2023
|
+
const side_stored = side.stored();
|
|
2020
2024
|
var foe_stored = foe.stored();
|
|
2021
2025
|
|
|
2022
2026
|
const player_ident = battle.active(player);
|
|
@@ -2028,8 +2032,8 @@ pub const Effects = struct {
|
|
|
2028
2032
|
side.active.stats = side_stored.stats;
|
|
2029
2033
|
foe.active.stats = foe_stored.stats;
|
|
2030
2034
|
|
|
2031
|
-
try log.activate(player_ident, .Haze);
|
|
2032
|
-
try log.clearallboost();
|
|
2035
|
+
try log.activate(.{ player_ident, .Haze });
|
|
2036
|
+
try log.clearallboost(.{});
|
|
2033
2037
|
|
|
2034
2038
|
// Pokémon Showdown clears P1 then P2 instead of status -> side -> foe
|
|
2035
2039
|
if (showdown) {
|
|
@@ -2037,24 +2041,26 @@ pub const Effects = struct {
|
|
|
2037
2041
|
const p: Player = @enumFromInt(i);
|
|
2038
2042
|
// Pokémon Showdown incorrectly does not prevent sleep/freeze from moving
|
|
2039
2043
|
if (p != player and Status.any(s.stored().status)) {
|
|
2040
|
-
try log.curestatus(foe_ident, foe_stored.status, .Silent);
|
|
2044
|
+
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
2041
2045
|
s.stored().status = 0;
|
|
2046
|
+
options.chance.observe(.sleep, p, .None);
|
|
2042
2047
|
} else if (showdown and s.stored().status == Status.TOX) {
|
|
2043
2048
|
s.stored().status = Status.init(.PSN);
|
|
2044
|
-
try log.status(battle.active(p), s.stored().status, .None);
|
|
2049
|
+
try log.status(.{ battle.active(p), s.stored().status, .None });
|
|
2045
2050
|
}
|
|
2046
|
-
try clearVolatiles(battle, p,
|
|
2051
|
+
try clearVolatiles(battle, p, options);
|
|
2047
2052
|
}
|
|
2048
2053
|
} else {
|
|
2049
2054
|
if (Status.any(foe_stored.status)) {
|
|
2050
2055
|
if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
|
|
2051
2056
|
foe.last_selected_move = .SKIP_TURN;
|
|
2057
|
+
options.chance.observe(.sleep, player.foe(), .None);
|
|
2052
2058
|
}
|
|
2053
|
-
try log.curestatus(foe_ident, foe_stored.status, .Silent);
|
|
2059
|
+
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
2054
2060
|
foe_stored.status = 0;
|
|
2055
2061
|
}
|
|
2056
|
-
try clearVolatiles(battle, player,
|
|
2057
|
-
try clearVolatiles(battle, player.foe(),
|
|
2062
|
+
try clearVolatiles(battle, player, options);
|
|
2063
|
+
try clearVolatiles(battle, player.foe(), options);
|
|
2058
2064
|
}
|
|
2059
2065
|
}
|
|
2060
2066
|
|
|
@@ -2066,7 +2072,7 @@ pub const Effects = struct {
|
|
|
2066
2072
|
// GLITCH: HP recovery move failure glitches
|
|
2067
2073
|
const delta = stored.stats.hp - stored.hp;
|
|
2068
2074
|
if (delta == 0 or (delta & 255 == 255 and stored.hp % 256 != 0)) {
|
|
2069
|
-
return
|
|
2075
|
+
return options.log.fail(.{ ident, .None });
|
|
2070
2076
|
}
|
|
2071
2077
|
|
|
2072
2078
|
const rest = side.last_selected_move == .Rest;
|
|
@@ -2074,17 +2080,17 @@ pub const Effects = struct {
|
|
|
2074
2080
|
// Adding the sleep status runs the sleep condition handler to roll duration
|
|
2075
2081
|
if (showdown) battle.rng.advance(1);
|
|
2076
2082
|
stored.status = Status.slf(2);
|
|
2077
|
-
try options.log.
|
|
2083
|
+
try options.log.status(.{ ident, stored.status, .From, Move.Rest });
|
|
2078
2084
|
stored.hp = stored.stats.hp;
|
|
2079
2085
|
} else {
|
|
2080
2086
|
stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
|
|
2081
2087
|
}
|
|
2082
|
-
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 });
|
|
2083
2089
|
}
|
|
2084
2090
|
|
|
2085
2091
|
fn hyperBeam(battle: anytype, player: Player, options: anytype) !void {
|
|
2086
2092
|
battle.side(player).active.volatiles.Recharging = true;
|
|
2087
|
-
try options.log.mustrecharge(battle.active(player));
|
|
2093
|
+
try options.log.mustrecharge(.{battle.active(player)});
|
|
2088
2094
|
}
|
|
2089
2095
|
|
|
2090
2096
|
fn leechSeed(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2093,33 +2099,31 @@ pub const Effects = struct {
|
|
|
2093
2099
|
if (showdown) {
|
|
2094
2100
|
// Invulnerability trumps type immunity on Pokémon Showdown
|
|
2095
2101
|
if (!foe.active.volatiles.Invulnerable and foe.active.types.includes(.Grass)) {
|
|
2096
|
-
return options.log.immune(battle.active(player.foe()), .None);
|
|
2102
|
+
return options.log.immune(.{ battle.active(player.foe()), .None });
|
|
2097
2103
|
}
|
|
2098
2104
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2099
2105
|
if (foe.active.volatiles.LeechSeed) return;
|
|
2100
2106
|
} else {
|
|
2101
2107
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2102
2108
|
if (foe.active.types.includes(.Grass) or foe.active.volatiles.LeechSeed) {
|
|
2103
|
-
try options.log.lastmiss();
|
|
2104
|
-
return options.log.miss(battle.active(player));
|
|
2109
|
+
try options.log.lastmiss(.{});
|
|
2110
|
+
return options.log.miss(.{battle.active(player)});
|
|
2105
2111
|
}
|
|
2106
2112
|
}
|
|
2107
2113
|
|
|
2108
|
-
try options.chance.commit(player, .hit);
|
|
2109
2114
|
foe.active.volatiles.LeechSeed = true;
|
|
2110
2115
|
|
|
2111
|
-
try options.log.start(battle.active(player.foe()), .LeechSeed);
|
|
2116
|
+
try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
|
|
2112
2117
|
}
|
|
2113
2118
|
|
|
2114
2119
|
fn lightScreen(battle: anytype, player: Player, options: anytype) !void {
|
|
2115
2120
|
var side = battle.side(player);
|
|
2121
|
+
const ident = battle.active(player);
|
|
2116
2122
|
|
|
2117
|
-
if (side.active.volatiles.LightScreen) {
|
|
2118
|
-
return options.log.fail(battle.active(player), .None);
|
|
2119
|
-
}
|
|
2123
|
+
if (side.active.volatiles.LightScreen) return options.log.fail(.{ ident, .None });
|
|
2120
2124
|
side.active.volatiles.LightScreen = true;
|
|
2121
2125
|
|
|
2122
|
-
try options.log.start(
|
|
2126
|
+
try options.log.start(.{ ident, .LightScreen });
|
|
2123
2127
|
}
|
|
2124
2128
|
|
|
2125
2129
|
fn mimic(battle: anytype, player: Player, move: Move.Data, mslot: u8, options: anytype) !void {
|
|
@@ -2147,17 +2151,15 @@ pub const Effects = struct {
|
|
|
2147
2151
|
if (!has_mimic) {
|
|
2148
2152
|
// Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
|
|
2149
2153
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2150
|
-
|
|
2151
|
-
return try options.log.fail(battle.active(player.foe()), .None);
|
|
2154
|
+
return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
2152
2155
|
}
|
|
2153
2156
|
}
|
|
2154
2157
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2155
|
-
try options.chance.commit(player, .hit);
|
|
2156
2158
|
|
|
2157
2159
|
const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
|
|
2158
2160
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
2159
2161
|
|
|
2160
|
-
try options.log.
|
|
2162
|
+
try options.log.start(.{ battle.active(player), .Mimic, side.active.move(oslot).id });
|
|
2161
2163
|
}
|
|
2162
2164
|
|
|
2163
2165
|
fn mist(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -2166,7 +2168,7 @@ pub const Effects = struct {
|
|
|
2166
2168
|
if (side.active.volatiles.Mist) return;
|
|
2167
2169
|
side.active.volatiles.Mist = true;
|
|
2168
2170
|
|
|
2169
|
-
try options.log.start(battle.active(player), .Mist);
|
|
2171
|
+
try options.log.start(.{ battle.active(player), .Mist });
|
|
2170
2172
|
}
|
|
2171
2173
|
|
|
2172
2174
|
fn multiHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2192,25 +2194,25 @@ pub const Effects = struct {
|
|
|
2192
2194
|
|
|
2193
2195
|
if (showdown) {
|
|
2194
2196
|
// Invulnerability trumps type immunity on Pokémon Showdown
|
|
2195
|
-
if (immune and !foe.active.volatiles.Invulnerable)
|
|
2196
|
-
|
|
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;
|
|
2197
2200
|
}
|
|
2198
2201
|
if (Status.any(foe_stored.status)) {
|
|
2199
|
-
return log.fail(
|
|
2202
|
+
return log.fail(.{
|
|
2200
2203
|
foe_ident,
|
|
2201
|
-
if (Status.is(foe_stored.status, .PAR)) .Paralysis else .None,
|
|
2202
|
-
);
|
|
2204
|
+
if (Status.is(foe_stored.status, .PAR)) Fail.Paralysis else Fail.None,
|
|
2205
|
+
});
|
|
2203
2206
|
}
|
|
2204
2207
|
if (!showdown) {
|
|
2205
|
-
if (immune) return log.immune(foe_ident, .None);
|
|
2208
|
+
if (immune) return log.immune(.{ foe_ident, .None });
|
|
2206
2209
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2207
2210
|
}
|
|
2208
|
-
try options.chance.commit(player, .hit);
|
|
2209
2211
|
|
|
2210
2212
|
foe_stored.status = Status.init(.PAR);
|
|
2211
2213
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
2212
2214
|
|
|
2213
|
-
try log.status(foe_ident, foe_stored.status, .None);
|
|
2215
|
+
try log.status(.{ foe_ident, foe_stored.status, .None });
|
|
2214
2216
|
}
|
|
2215
2217
|
|
|
2216
2218
|
fn paralyzeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2232,12 +2234,12 @@ pub const Effects = struct {
|
|
|
2232
2234
|
foe_stored.status = Status.init(.PAR);
|
|
2233
2235
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
2234
2236
|
|
|
2235
|
-
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 });
|
|
2236
2238
|
}
|
|
2237
2239
|
|
|
2238
2240
|
fn payDay(battle: anytype, player: Player, options: anytype) !void {
|
|
2239
2241
|
if (!showdown or !battle.foe(player).active.volatiles.Substitute) {
|
|
2240
|
-
try options.log.fieldactivate();
|
|
2242
|
+
try options.log.fieldactivate(.{});
|
|
2241
2243
|
}
|
|
2242
2244
|
}
|
|
2243
2245
|
|
|
@@ -2252,28 +2254,27 @@ pub const Effects = struct {
|
|
|
2252
2254
|
return;
|
|
2253
2255
|
} else if (foe.active.volatiles.Substitute) {
|
|
2254
2256
|
if (move.effect != .Poison) return;
|
|
2255
|
-
return log.fail(foe_ident, .None);
|
|
2257
|
+
return log.fail(.{ foe_ident, .None });
|
|
2256
2258
|
} else if (Status.any(foe_stored.status)) {
|
|
2257
2259
|
if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
|
|
2258
2260
|
// Pokémon Showdown considers Toxic to be a status even in Generation I and so
|
|
2259
2261
|
// will not include a fail reason for Toxic vs. Poison or vice-versa...
|
|
2260
|
-
return log.fail(foe_ident, if (Status.is(foe_stored.status, .PSN))
|
|
2262
|
+
return log.fail(.{ foe_ident, if (Status.is(foe_stored.status, .PSN))
|
|
2261
2263
|
if (!showdown)
|
|
2262
|
-
.Poison
|
|
2264
|
+
Fail.Poison
|
|
2263
2265
|
else if (toxic == (foe_stored.status == Status.TOX))
|
|
2264
|
-
if (toxic) .Toxic else .Poison
|
|
2266
|
+
if (toxic) Fail.Toxic else Fail.Poison
|
|
2265
2267
|
else
|
|
2266
|
-
.None
|
|
2268
|
+
Fail.None
|
|
2267
2269
|
else
|
|
2268
|
-
.None);
|
|
2270
|
+
Fail.None });
|
|
2269
2271
|
} else if (foe.active.types.includes(.Poison)) {
|
|
2270
2272
|
if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
|
|
2271
|
-
return log.immune(foe_ident, .None);
|
|
2273
|
+
return log.immune(.{ foe_ident, .None });
|
|
2272
2274
|
}
|
|
2273
2275
|
|
|
2274
2276
|
if (move.effect == .Poison) {
|
|
2275
2277
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2276
|
-
try options.chance.commit(player, .hit);
|
|
2277
2278
|
} else {
|
|
2278
2279
|
if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
|
|
2279
2280
|
return;
|
|
@@ -2287,7 +2288,7 @@ pub const Effects = struct {
|
|
|
2287
2288
|
foe.active.volatiles.toxic = 0;
|
|
2288
2289
|
}
|
|
2289
2290
|
|
|
2290
|
-
try log.status(foe_ident, foe_stored.status, .None);
|
|
2291
|
+
try log.status(.{ foe_ident, foe_stored.status, .None });
|
|
2291
2292
|
}
|
|
2292
2293
|
|
|
2293
2294
|
fn rage(battle: anytype, player: Player) !void {
|
|
@@ -2307,22 +2308,23 @@ pub const Effects = struct {
|
|
|
2307
2308
|
@as(u8, if (side.last_selected_move == .Struggle) 2 else 4), 1));
|
|
2308
2309
|
stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
|
|
2309
2310
|
|
|
2310
|
-
try options.log.
|
|
2311
|
+
try options.log.damage(.{
|
|
2311
2312
|
battle.active(player),
|
|
2312
2313
|
stored,
|
|
2313
2314
|
.RecoilOf,
|
|
2314
2315
|
battle.active(player.foe()),
|
|
2315
|
-
);
|
|
2316
|
+
});
|
|
2316
2317
|
if (showdown and stored.hp == 0) residual.* = false;
|
|
2317
2318
|
}
|
|
2318
2319
|
|
|
2319
2320
|
fn reflect(battle: anytype, player: Player, options: anytype) !void {
|
|
2320
2321
|
var side = battle.side(player);
|
|
2322
|
+
const ident = battle.active(player);
|
|
2321
2323
|
|
|
2322
|
-
if (side.active.volatiles.Reflect) return options.log.fail(
|
|
2324
|
+
if (side.active.volatiles.Reflect) return options.log.fail(.{ ident, .None });
|
|
2323
2325
|
side.active.volatiles.Reflect = true;
|
|
2324
2326
|
|
|
2325
|
-
try options.log.start(
|
|
2327
|
+
try options.log.start(.{ ident, .Reflect });
|
|
2326
2328
|
}
|
|
2327
2329
|
|
|
2328
2330
|
fn sleep(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2336,14 +2338,13 @@ pub const Effects = struct {
|
|
|
2336
2338
|
// the Sleep Clause Mod might activate, causing us to not actually bypass
|
|
2337
2339
|
} else {
|
|
2338
2340
|
if (Status.any(foe_stored.status)) {
|
|
2339
|
-
return options.log.fail(
|
|
2341
|
+
return options.log.fail(.{
|
|
2340
2342
|
foe_ident,
|
|
2341
|
-
if (Status.is(foe_stored.status, .SLP)) .Sleep else .None,
|
|
2342
|
-
);
|
|
2343
|
+
if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
|
|
2344
|
+
});
|
|
2343
2345
|
}
|
|
2344
|
-
// If
|
|
2346
|
+
// If moveHit in doMove didn't return true Pokémon Showdown wouldn't be in here
|
|
2345
2347
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2346
|
-
try options.chance.commit(player, .hit);
|
|
2347
2348
|
}
|
|
2348
2349
|
|
|
2349
2350
|
// Sleep Clause Mod
|
|
@@ -2355,19 +2356,20 @@ pub const Effects = struct {
|
|
|
2355
2356
|
foe.active.volatiles.Recharging = false;
|
|
2356
2357
|
|
|
2357
2358
|
foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
|
|
2359
|
+
options.chance.observe(.sleep, player.foe(), .started);
|
|
2360
|
+
|
|
2358
2361
|
const last = battle.side(player).last_selected_move;
|
|
2359
|
-
try options.log.
|
|
2362
|
+
try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
|
|
2360
2363
|
}
|
|
2361
2364
|
|
|
2362
2365
|
fn splash(battle: anytype, player: Player, options: anytype) !void {
|
|
2363
|
-
try options.log.activate(battle.active(player), .Splash);
|
|
2366
|
+
try options.log.activate(.{ battle.active(player), .Splash });
|
|
2364
2367
|
}
|
|
2365
2368
|
|
|
2366
2369
|
fn substitute(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
|
|
2367
2370
|
var side = battle.side(player);
|
|
2368
2371
|
if (side.active.volatiles.Substitute) {
|
|
2369
|
-
|
|
2370
|
-
return;
|
|
2372
|
+
return options.log.fail(.{ battle.active(player), .Substitute });
|
|
2371
2373
|
}
|
|
2372
2374
|
|
|
2373
2375
|
assert(side.stored().stats.hp <= 1023);
|
|
@@ -2379,28 +2381,30 @@ pub const Effects = struct {
|
|
|
2379
2381
|
// avoid having to convert to floating point)
|
|
2380
2382
|
const required_hp = if (showdown) @divFloor(side.stored().stats.hp - 1, 4) + 1 else hp;
|
|
2381
2383
|
if (side.stored().hp < required_hp) {
|
|
2382
|
-
|
|
2383
|
-
return;
|
|
2384
|
+
return options.log.fail(.{ battle.active(player), .Weak });
|
|
2384
2385
|
}
|
|
2385
2386
|
|
|
2386
2387
|
// GLITCH: can leave the user with 0 HP (faints later) because didn't check '<=' above
|
|
2387
2388
|
side.stored().hp -= hp;
|
|
2388
2389
|
side.active.volatiles.substitute = hp + 1;
|
|
2389
2390
|
side.active.volatiles.Substitute = true;
|
|
2390
|
-
try options.log.start(battle.active(player), .Substitute);
|
|
2391
|
+
try options.log.start(.{ battle.active(player), .Substitute });
|
|
2391
2392
|
if (hp > 0) {
|
|
2392
|
-
try options.log.damage(battle.active(player), side.stored(), .None);
|
|
2393
|
+
try options.log.damage(.{ battle.active(player), side.stored(), .None });
|
|
2393
2394
|
if (showdown and side.stored().hp == 0) residual.* = false;
|
|
2394
2395
|
}
|
|
2395
2396
|
}
|
|
2396
2397
|
|
|
2397
2398
|
fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
2398
|
-
if (!showdown or battle.side(player).last_selected_move == .Teleport) return;
|
|
2399
|
-
|
|
2400
2399
|
// Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
|
|
2401
|
-
if (
|
|
2402
|
-
|
|
2403
|
-
|
|
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
|
+
}
|
|
2404
2408
|
}
|
|
2405
2409
|
|
|
2406
2410
|
fn thrashing(battle: anytype, player: Player, options: anytype) void {
|
|
@@ -2410,6 +2414,8 @@ pub const Effects = struct {
|
|
|
2410
2414
|
|
|
2411
2415
|
volatiles.Thrashing = true;
|
|
2412
2416
|
volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
2417
|
+
|
|
2418
|
+
options.chance.observe(.attacking, player, .started);
|
|
2413
2419
|
}
|
|
2414
2420
|
|
|
2415
2421
|
fn transform(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -2438,7 +2444,7 @@ pub const Effects = struct {
|
|
|
2438
2444
|
side.active.moves[i].pp = if (m.id != .None) 5 else 0;
|
|
2439
2445
|
}
|
|
2440
2446
|
|
|
2441
|
-
try options.log.transform(battle.active(player), foe_ident);
|
|
2447
|
+
try options.log.transform(.{ battle.active(player), foe_ident });
|
|
2442
2448
|
}
|
|
2443
2449
|
|
|
2444
2450
|
fn binding(battle: anytype, player: Player, rewrap: bool, options: anytype) !void {
|
|
@@ -2455,6 +2461,7 @@ pub const Effects = struct {
|
|
|
2455
2461
|
|
|
2456
2462
|
side.active.volatiles.attacks =
|
|
2457
2463
|
try Rolls.distribution(battle, .Binding, player, options) - 1;
|
|
2464
|
+
options.chance.observe(.binding, player, .started);
|
|
2458
2465
|
}
|
|
2459
2466
|
|
|
2460
2467
|
fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2468,87 +2475,87 @@ pub const Effects = struct {
|
|
|
2468
2475
|
switch (move.effect) {
|
|
2469
2476
|
.AttackUp1, .AttackUp2, .Rage => {
|
|
2470
2477
|
assert(boosts.atk >= -6 and boosts.atk <= 6);
|
|
2471
|
-
if (boosts.atk == 6) return
|
|
2478
|
+
if (boosts.atk == 6) return log.fail(.{ ident, .None });
|
|
2472
2479
|
const n: u2 = if (move.effect == .AttackUp2) 2 else 1;
|
|
2473
2480
|
boosts.atk = @as(i4, @intCast(@min(6, @as(i8, boosts.atk) + n)));
|
|
2474
|
-
const reason = if (move.effect == .Rage)
|
|
2481
|
+
const reason: Boost = if (move.effect == .Rage) .Rage else .Attack;
|
|
2475
2482
|
if (stats.atk == MAX_STAT_VALUE) {
|
|
2476
2483
|
boosts.atk -= 1;
|
|
2477
2484
|
if (showdown) {
|
|
2478
|
-
try log.boost(ident, reason, n);
|
|
2479
|
-
try log.boost(ident,
|
|
2485
|
+
try log.boost(.{ ident, reason, n });
|
|
2486
|
+
try log.boost(.{ ident, .Attack, -1 });
|
|
2480
2487
|
if (move.effect == .Rage) return;
|
|
2481
2488
|
}
|
|
2482
|
-
return
|
|
2489
|
+
return log.fail(.{ ident, .None });
|
|
2483
2490
|
}
|
|
2484
|
-
|
|
2491
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
|
|
2485
2492
|
const stat = unmodifiedStats(battle, side).atk;
|
|
2486
2493
|
stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2487
|
-
try log.boost(ident, reason, n);
|
|
2494
|
+
try log.boost(.{ ident, reason, n });
|
|
2488
2495
|
// Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
|
|
2489
2496
|
if (showdown and move.effect == .Rage) return;
|
|
2490
2497
|
},
|
|
2491
2498
|
.DefenseUp1, .DefenseUp2 => {
|
|
2492
2499
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
2493
|
-
if (boosts.def == 6) return
|
|
2500
|
+
if (boosts.def == 6) return log.fail(.{ ident, .None });
|
|
2494
2501
|
const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
|
|
2495
2502
|
boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
|
|
2496
2503
|
if (stats.def == MAX_STAT_VALUE) {
|
|
2497
2504
|
boosts.def -= 1;
|
|
2498
2505
|
if (showdown) {
|
|
2499
|
-
try log.boost(ident, .Defense, n);
|
|
2500
|
-
try log.boost(ident, .Defense, -1);
|
|
2506
|
+
try log.boost(.{ ident, .Defense, n });
|
|
2507
|
+
try log.boost(.{ ident, .Defense, -1 });
|
|
2501
2508
|
}
|
|
2502
|
-
return
|
|
2509
|
+
return log.fail(.{ ident, .None });
|
|
2503
2510
|
}
|
|
2504
|
-
|
|
2511
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
|
|
2505
2512
|
const stat = unmodifiedStats(battle, side).def;
|
|
2506
2513
|
stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2507
|
-
try log.boost(ident, .Defense, n);
|
|
2514
|
+
try log.boost(.{ ident, .Defense, n });
|
|
2508
2515
|
},
|
|
2509
2516
|
.SpeedUp2 => {
|
|
2510
2517
|
assert(boosts.spe >= -6 and boosts.spe <= 6);
|
|
2511
|
-
if (boosts.spe == 6) return
|
|
2518
|
+
if (boosts.spe == 6) return log.fail(.{ ident, .None });
|
|
2512
2519
|
boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
|
|
2513
2520
|
if (stats.spe == MAX_STAT_VALUE) {
|
|
2514
2521
|
boosts.spe -= 1;
|
|
2515
2522
|
if (showdown) {
|
|
2516
|
-
try log.boost(ident, .Speed, 2);
|
|
2517
|
-
try log.boost(ident, .Speed, -1);
|
|
2523
|
+
try log.boost(.{ ident, .Speed, 2 });
|
|
2524
|
+
try log.boost(.{ ident, .Speed, -1 });
|
|
2518
2525
|
}
|
|
2519
|
-
return
|
|
2526
|
+
return log.fail(.{ ident, .None });
|
|
2520
2527
|
}
|
|
2521
|
-
|
|
2528
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
|
|
2522
2529
|
const stat = unmodifiedStats(battle, side).spe;
|
|
2523
2530
|
stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2524
|
-
try log.boost(ident, .Speed, 2);
|
|
2531
|
+
try log.boost(.{ ident, .Speed, 2 });
|
|
2525
2532
|
},
|
|
2526
2533
|
.SpecialUp1, .SpecialUp2 => {
|
|
2527
2534
|
assert(boosts.spc >= -6 and boosts.spc <= 6);
|
|
2528
|
-
if (boosts.spc == 6) return
|
|
2535
|
+
if (boosts.spc == 6) return log.fail(.{ ident, .None });
|
|
2529
2536
|
const n: u2 = if (move.effect == .SpecialUp2) 2 else 1;
|
|
2530
2537
|
boosts.spc = @intCast(@min(6, @as(i8, boosts.spc) + n));
|
|
2531
2538
|
if (stats.spc == MAX_STAT_VALUE) {
|
|
2532
2539
|
boosts.spc -= 1;
|
|
2533
2540
|
if (showdown) {
|
|
2534
|
-
try log.boost(ident, .SpecialAttack, n);
|
|
2535
|
-
try log.boost(ident, .SpecialAttack, -1);
|
|
2536
|
-
try log.boost(ident, .SpecialDefense, n);
|
|
2537
|
-
try log.boost(ident, .SpecialDefense, -1);
|
|
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 });
|
|
2538
2545
|
}
|
|
2539
|
-
return
|
|
2546
|
+
return log.fail(.{ ident, .None });
|
|
2540
2547
|
}
|
|
2541
|
-
|
|
2548
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
|
|
2542
2549
|
const stat = unmodifiedStats(battle, side).spc;
|
|
2543
2550
|
stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2544
|
-
try log.boost(ident, .SpecialAttack, n);
|
|
2545
|
-
try log.boost(ident, .SpecialDefense, n);
|
|
2551
|
+
try log.boost(.{ ident, .SpecialAttack, n });
|
|
2552
|
+
try log.boost(.{ ident, .SpecialDefense, n });
|
|
2546
2553
|
},
|
|
2547
2554
|
.EvasionUp1 => {
|
|
2548
2555
|
assert(boosts.evasion >= -6 and boosts.evasion <= 6);
|
|
2549
|
-
if (boosts.evasion == 6) return
|
|
2556
|
+
if (boosts.evasion == 6) return log.fail(.{ ident, .None });
|
|
2550
2557
|
boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
|
|
2551
|
-
try log.boost(ident, .Evasion, 1);
|
|
2558
|
+
try log.boost(.{ ident, .Evasion, 1 });
|
|
2552
2559
|
},
|
|
2553
2560
|
else => unreachable,
|
|
2554
2561
|
}
|
|
@@ -2563,7 +2570,7 @@ pub const Effects = struct {
|
|
|
2563
2570
|
const foe_ident = battle.active(player.foe());
|
|
2564
2571
|
const secondary = move.effect.isStatDownChance();
|
|
2565
2572
|
|
|
2566
|
-
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 });
|
|
2567
2574
|
|
|
2568
2575
|
if (secondary) {
|
|
2569
2576
|
const proc = try Rolls.unboost(battle, player, options);
|
|
@@ -2571,7 +2578,6 @@ pub const Effects = struct {
|
|
|
2571
2578
|
} else {
|
|
2572
2579
|
// checkHit already checks for Invulnerable
|
|
2573
2580
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2574
|
-
try options.chance.commit(player, .hit);
|
|
2575
2581
|
}
|
|
2576
2582
|
|
|
2577
2583
|
var stats = &foe.active.stats;
|
|
@@ -2581,82 +2587,82 @@ pub const Effects = struct {
|
|
|
2581
2587
|
switch (move.effect) {
|
|
2582
2588
|
.AttackDown1, .AttackDownChance => {
|
|
2583
2589
|
assert(boosts.atk >= -6 and boosts.atk <= 6);
|
|
2584
|
-
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 });
|
|
2585
2591
|
boosts.atk = @intCast(@max(-6, @as(i8, boosts.atk) - 1));
|
|
2586
2592
|
if (stats.atk == 1) {
|
|
2587
2593
|
boosts.atk += 1;
|
|
2588
2594
|
if (showdown) {
|
|
2589
|
-
try log.boost(foe_ident, .Attack, -1);
|
|
2590
|
-
try log.boost(foe_ident, .Attack, 1);
|
|
2595
|
+
try log.boost(.{ foe_ident, .Attack, -1 });
|
|
2596
|
+
try log.boost(.{ foe_ident, .Attack, 1 });
|
|
2591
2597
|
}
|
|
2592
|
-
return
|
|
2598
|
+
return log.fail(.{ foe_ident, .None });
|
|
2593
2599
|
}
|
|
2594
|
-
|
|
2600
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
|
|
2595
2601
|
const stat = unmodifiedStats(battle, foe).atk;
|
|
2596
2602
|
stats.atk = @max(1, stat * mod[0] / mod[1]);
|
|
2597
|
-
try log.boost(foe_ident, .Attack, -1);
|
|
2603
|
+
try log.boost(.{ foe_ident, .Attack, -1 });
|
|
2598
2604
|
},
|
|
2599
2605
|
.DefenseDown1, .DefenseDown2, .DefenseDownChance => {
|
|
2600
2606
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
2601
|
-
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 });
|
|
2602
2608
|
const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
|
|
2603
2609
|
boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
|
|
2604
2610
|
if (stats.def == 1) {
|
|
2605
2611
|
boosts.def += 1;
|
|
2606
2612
|
if (showdown) {
|
|
2607
|
-
try log.boost(foe_ident, .Defense, -@as(i8, n));
|
|
2608
|
-
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 });
|
|
2609
2615
|
}
|
|
2610
|
-
return
|
|
2616
|
+
return log.fail(.{ foe_ident, .None });
|
|
2611
2617
|
}
|
|
2612
|
-
|
|
2618
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
|
|
2613
2619
|
const stat = unmodifiedStats(battle, foe).def;
|
|
2614
2620
|
stats.def = @max(1, stat * mod[0] / mod[1]);
|
|
2615
|
-
try log.boost(foe_ident, .Defense, -@as(i8, n));
|
|
2621
|
+
try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
|
|
2616
2622
|
},
|
|
2617
2623
|
.SpeedDown1, .SpeedDownChance => {
|
|
2618
2624
|
assert(boosts.spe >= -6 and boosts.spe <= 6);
|
|
2619
|
-
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 });
|
|
2620
2626
|
boosts.spe = @intCast(@max(-6, @as(i8, boosts.spe) - 1));
|
|
2621
2627
|
if (stats.spe == 1) {
|
|
2622
2628
|
boosts.spe += 1;
|
|
2623
2629
|
if (showdown) {
|
|
2624
|
-
try log.boost(foe_ident, .Speed, -1);
|
|
2625
|
-
try log.boost(foe_ident, .Speed, 1);
|
|
2630
|
+
try log.boost(.{ foe_ident, .Speed, -1 });
|
|
2631
|
+
try log.boost(.{ foe_ident, .Speed, 1 });
|
|
2626
2632
|
}
|
|
2627
|
-
return
|
|
2633
|
+
return log.fail(.{ foe_ident, .None });
|
|
2628
2634
|
}
|
|
2629
|
-
|
|
2635
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
|
|
2630
2636
|
const stat = unmodifiedStats(battle, foe).spe;
|
|
2631
2637
|
stats.spe = @max(1, stat * mod[0] / mod[1]);
|
|
2632
|
-
try log.boost(foe_ident, .Speed, -1);
|
|
2638
|
+
try log.boost(.{ foe_ident, .Speed, -1 });
|
|
2633
2639
|
assert(boosts.spe >= -6);
|
|
2634
2640
|
},
|
|
2635
2641
|
.SpecialDownChance => {
|
|
2636
2642
|
assert(boosts.spc >= -6 and boosts.spc <= 6);
|
|
2637
|
-
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 });
|
|
2638
2644
|
boosts.spc = @intCast(@max(-6, @as(i8, boosts.spc) - 1));
|
|
2639
2645
|
if (stats.spc == 1) {
|
|
2640
2646
|
boosts.spc += 1;
|
|
2641
2647
|
if (showdown) {
|
|
2642
|
-
try log.boost(foe_ident, .SpecialAttack, -1);
|
|
2643
|
-
try log.boost(foe_ident, .SpecialAttack, 1);
|
|
2644
|
-
try log.boost(foe_ident, .SpecialDefense, -1);
|
|
2645
|
-
try log.boost(foe_ident, .SpecialDefense, 1);
|
|
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 });
|
|
2646
2652
|
}
|
|
2647
|
-
return
|
|
2653
|
+
return log.fail(.{ foe_ident, .None });
|
|
2648
2654
|
}
|
|
2649
|
-
|
|
2655
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
|
|
2650
2656
|
const stat = unmodifiedStats(battle, foe).spc;
|
|
2651
2657
|
stats.spc = @max(1, stat * mod[0] / mod[1]);
|
|
2652
|
-
try log.boost(foe_ident, .SpecialAttack, -1);
|
|
2653
|
-
try log.boost(foe_ident, .SpecialDefense, -1);
|
|
2658
|
+
try log.boost(.{ foe_ident, .SpecialAttack, -1 });
|
|
2659
|
+
try log.boost(.{ foe_ident, .SpecialDefense, -1 });
|
|
2654
2660
|
},
|
|
2655
2661
|
.AccuracyDown1 => {
|
|
2656
2662
|
assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
|
|
2657
|
-
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 });
|
|
2658
2664
|
boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
|
|
2659
|
-
try log.boost(foe_ident, .Accuracy, -1);
|
|
2665
|
+
try log.boost(.{ foe_ident, .Accuracy, -1 });
|
|
2660
2666
|
},
|
|
2661
2667
|
else => unreachable,
|
|
2662
2668
|
}
|
|
@@ -2680,40 +2686,40 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
|
|
|
2680
2686
|
}
|
|
2681
2687
|
}
|
|
2682
2688
|
|
|
2683
|
-
|
|
2689
|
+
fn isForced(active: anytype) bool {
|
|
2684
2690
|
return active.volatiles.Recharging or active.volatiles.Rage or
|
|
2685
2691
|
active.volatiles.Thrashing or active.volatiles.Charging;
|
|
2686
2692
|
}
|
|
2687
2693
|
|
|
2688
|
-
fn clearVolatiles(battle: anytype, who: Player,
|
|
2694
|
+
fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
|
|
2689
2695
|
var log = options.log;
|
|
2690
2696
|
var side = battle.side(who);
|
|
2691
2697
|
var volatiles = &side.active.volatiles;
|
|
2692
2698
|
const ident = battle.active(who);
|
|
2693
2699
|
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
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 });
|
|
2700
2705
|
}
|
|
2701
2706
|
if (volatiles.Confusion) {
|
|
2702
2707
|
// volatiles.confusion is left unchanged
|
|
2703
2708
|
volatiles.Confusion = false;
|
|
2704
|
-
|
|
2709
|
+
options.chance.observe(.confusion, who, .None);
|
|
2710
|
+
try log.end(.{ ident, .ConfusionSilent });
|
|
2705
2711
|
}
|
|
2706
2712
|
if (volatiles.Mist) {
|
|
2707
2713
|
volatiles.Mist = false;
|
|
2708
|
-
try log.end(ident, .
|
|
2714
|
+
try log.end(.{ ident, .MistSilent });
|
|
2709
2715
|
}
|
|
2710
2716
|
if (volatiles.FocusEnergy) {
|
|
2711
2717
|
volatiles.FocusEnergy = false;
|
|
2712
|
-
try log.end(ident, .
|
|
2718
|
+
try log.end(.{ ident, .FocusEnergySilent });
|
|
2713
2719
|
}
|
|
2714
2720
|
if (volatiles.LeechSeed) {
|
|
2715
2721
|
volatiles.LeechSeed = false;
|
|
2716
|
-
try log.end(ident, .
|
|
2722
|
+
try log.end(.{ ident, .LeechSeedSilent });
|
|
2717
2723
|
}
|
|
2718
2724
|
if (!showdown and volatiles.Toxic) {
|
|
2719
2725
|
volatiles.Toxic = false;
|
|
@@ -2721,24 +2727,39 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
|
|
|
2721
2727
|
}
|
|
2722
2728
|
if (volatiles.LightScreen) {
|
|
2723
2729
|
volatiles.LightScreen = false;
|
|
2724
|
-
try log.end(ident, .
|
|
2730
|
+
try log.end(.{ ident, .LightScreenSilent });
|
|
2725
2731
|
}
|
|
2726
2732
|
if (volatiles.Reflect) {
|
|
2727
2733
|
volatiles.Reflect = false;
|
|
2728
|
-
try log.end(ident, .
|
|
2734
|
+
try log.end(.{ ident, .ReflectSilent });
|
|
2729
2735
|
}
|
|
2730
2736
|
if (showdown and volatiles.Toxic) {
|
|
2731
2737
|
volatiles.Toxic = false;
|
|
2732
2738
|
// Pokémon Showdown erroneously clears the toxic counter
|
|
2733
2739
|
volatiles.toxic = 0;
|
|
2734
|
-
try log.end(ident, .
|
|
2740
|
+
try log.end(.{ ident, .ToxicSilent });
|
|
2735
2741
|
}
|
|
2736
2742
|
}
|
|
2737
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
|
+
|
|
2738
2757
|
pub const Rolls = struct {
|
|
2739
|
-
|
|
2758
|
+
fn speedTie(battle: anytype, options: anytype) !bool {
|
|
2740
2759
|
const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
|
|
2741
2760
|
player == .P1
|
|
2761
|
+
else if (@hasDecl(@TypeOf(battle.rng), "speedTie"))
|
|
2762
|
+
battle.rng.speedTie()
|
|
2742
2763
|
else if (showdown)
|
|
2743
2764
|
battle.rng.range(u8, 0, 2) == 0
|
|
2744
2765
|
else
|
|
@@ -2748,21 +2769,31 @@ pub const Rolls = struct {
|
|
|
2748
2769
|
return p1;
|
|
2749
2770
|
}
|
|
2750
2771
|
|
|
2751
|
-
|
|
2772
|
+
fn criticalHit(
|
|
2773
|
+
battle: anytype,
|
|
2774
|
+
player: Player,
|
|
2775
|
+
rate: u8,
|
|
2776
|
+
spurious: bool,
|
|
2777
|
+
options: anytype,
|
|
2778
|
+
) !bool {
|
|
2752
2779
|
const crit = if (options.calc.overridden(player, .critical_hit)) |val|
|
|
2753
2780
|
val == .true
|
|
2781
|
+
else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
|
|
2782
|
+
battle.rng.criticalHit(player, rate)
|
|
2754
2783
|
else if (showdown)
|
|
2755
2784
|
battle.rng.chance(u8, rate, 256)
|
|
2756
2785
|
else
|
|
2757
2786
|
std.math.rotl(u8, battle.rng.next(), 3) < rate;
|
|
2758
2787
|
|
|
2759
|
-
try options.chance.criticalHit(player, crit, rate);
|
|
2788
|
+
if (!spurious) try options.chance.criticalHit(player, crit, rate);
|
|
2760
2789
|
return crit;
|
|
2761
2790
|
}
|
|
2762
2791
|
|
|
2763
|
-
|
|
2792
|
+
fn damage(battle: anytype, player: Player, options: anytype) !u8 {
|
|
2764
2793
|
const roll = if (options.calc.overridden(player, .damage)) |val|
|
|
2765
2794
|
val
|
|
2795
|
+
else if (@hasDecl(@TypeOf(battle.rng), "damage"))
|
|
2796
|
+
battle.rng.damage(player)
|
|
2766
2797
|
else roll: {
|
|
2767
2798
|
if (showdown) break :roll battle.rng.range(u8, 217, 256);
|
|
2768
2799
|
while (true) {
|
|
@@ -2776,21 +2807,27 @@ pub const Rolls = struct {
|
|
|
2776
2807
|
return roll;
|
|
2777
2808
|
}
|
|
2778
2809
|
|
|
2779
|
-
|
|
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;
|
|
2780
2813
|
const ok = if (options.calc.overridden(player, .hit)) |val|
|
|
2781
2814
|
val == .true
|
|
2815
|
+
else if (@hasDecl(@TypeOf(battle.rng), "hit"))
|
|
2816
|
+
battle.rng.hit(player, acc)
|
|
2782
2817
|
else if (showdown)
|
|
2783
|
-
battle.rng.chance(
|
|
2818
|
+
battle.rng.chance(uN, acc, 256)
|
|
2784
2819
|
else
|
|
2785
|
-
battle.rng.next() <
|
|
2820
|
+
battle.rng.next() < acc;
|
|
2786
2821
|
|
|
2787
|
-
options.chance.hit(ok,
|
|
2822
|
+
try options.chance.hit(player, ok, acc);
|
|
2788
2823
|
return ok;
|
|
2789
2824
|
}
|
|
2790
2825
|
|
|
2791
|
-
|
|
2826
|
+
fn confused(battle: anytype, player: Player, options: anytype) !bool {
|
|
2792
2827
|
const cfz = if (options.calc.overridden(player, .confused)) |val|
|
|
2793
2828
|
val == .true
|
|
2829
|
+
else if (@hasDecl(@TypeOf(battle.rng), "confused"))
|
|
2830
|
+
battle.rng.confused(player)
|
|
2794
2831
|
else if (showdown)
|
|
2795
2832
|
!battle.rng.chance(u8, 128, 256)
|
|
2796
2833
|
else
|
|
@@ -2800,9 +2837,11 @@ pub const Rolls = struct {
|
|
|
2800
2837
|
return cfz;
|
|
2801
2838
|
}
|
|
2802
2839
|
|
|
2803
|
-
|
|
2840
|
+
fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
|
|
2804
2841
|
const par = if (options.calc.overridden(player, .paralyzed)) |val|
|
|
2805
2842
|
val == .true
|
|
2843
|
+
else if (@hasDecl(@TypeOf(battle.rng), "paralyzed"))
|
|
2844
|
+
battle.rng.paralyzed(player)
|
|
2806
2845
|
else if (showdown)
|
|
2807
2846
|
battle.rng.chance(u8, 63, 256)
|
|
2808
2847
|
else
|
|
@@ -2812,9 +2851,11 @@ pub const Rolls = struct {
|
|
|
2812
2851
|
return par;
|
|
2813
2852
|
}
|
|
2814
2853
|
|
|
2815
|
-
|
|
2854
|
+
fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
|
|
2816
2855
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2817
2856
|
val == .true
|
|
2857
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2858
|
+
battle.rng.secondaryChance(player, 25)
|
|
2818
2859
|
else if (showdown)
|
|
2819
2860
|
battle.rng.chance(u8, 25, 256)
|
|
2820
2861
|
else
|
|
@@ -2824,11 +2865,13 @@ pub const Rolls = struct {
|
|
|
2824
2865
|
return proc;
|
|
2825
2866
|
}
|
|
2826
2867
|
|
|
2827
|
-
|
|
2868
|
+
fn secondaryChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
|
|
2828
2869
|
const rate: u8 = if (low) 26 else 77;
|
|
2829
2870
|
|
|
2830
2871
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2831
2872
|
val == .true
|
|
2873
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2874
|
+
battle.rng.secondaryChance(player, rate)
|
|
2832
2875
|
else if (showdown)
|
|
2833
2876
|
battle.rng.chance(u8, rate, 256)
|
|
2834
2877
|
else
|
|
@@ -2838,11 +2881,13 @@ pub const Rolls = struct {
|
|
|
2838
2881
|
return proc;
|
|
2839
2882
|
}
|
|
2840
2883
|
|
|
2841
|
-
|
|
2884
|
+
fn poisonChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
|
|
2842
2885
|
const rate: u8 = if (low) 52 else 103;
|
|
2843
2886
|
|
|
2844
2887
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2845
2888
|
val == .true
|
|
2889
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2890
|
+
battle.rng.secondaryChance(player, rate)
|
|
2846
2891
|
else if (showdown)
|
|
2847
2892
|
battle.rng.chance(u8, rate, 256)
|
|
2848
2893
|
else
|
|
@@ -2852,9 +2897,11 @@ pub const Rolls = struct {
|
|
|
2852
2897
|
return proc;
|
|
2853
2898
|
}
|
|
2854
2899
|
|
|
2855
|
-
|
|
2900
|
+
fn unboost(battle: anytype, player: Player, options: anytype) !bool {
|
|
2856
2901
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2857
2902
|
val == .true
|
|
2903
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2904
|
+
battle.rng.secondaryChance(player, 85)
|
|
2858
2905
|
else if (showdown)
|
|
2859
2906
|
battle.rng.chance(u8, 85, 256)
|
|
2860
2907
|
else
|
|
@@ -2864,14 +2911,14 @@ pub const Rolls = struct {
|
|
|
2864
2911
|
return proc;
|
|
2865
2912
|
}
|
|
2866
2913
|
|
|
2867
|
-
|
|
2914
|
+
fn metronome(battle: anytype, player: Player, options: anytype) !Move {
|
|
2868
2915
|
const move: Move = if (options.calc.overridden(player, .metronome)) |val|
|
|
2869
2916
|
val
|
|
2870
|
-
else if (
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
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: {
|
|
2875
2922
|
while (true) {
|
|
2876
2923
|
const r = battle.rng.next();
|
|
2877
2924
|
if (r == 0 or r == @intFromEnum(Move.Metronome)) continue;
|
|
@@ -2884,9 +2931,11 @@ pub const Rolls = struct {
|
|
|
2884
2931
|
return move;
|
|
2885
2932
|
}
|
|
2886
2933
|
|
|
2887
|
-
|
|
2934
|
+
fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
|
|
2888
2935
|
const power = if (options.calc.overridden(player, .psywave)) |val|
|
|
2889
2936
|
val - 1
|
|
2937
|
+
else if (@hasDecl(@TypeOf(battle.rng), "psywave"))
|
|
2938
|
+
battle.rng.psywave(player)
|
|
2890
2939
|
else if (showdown)
|
|
2891
2940
|
battle.rng.range(u8, 0, max)
|
|
2892
2941
|
else power: {
|
|
@@ -2901,11 +2950,13 @@ pub const Rolls = struct {
|
|
|
2901
2950
|
return power;
|
|
2902
2951
|
}
|
|
2903
2952
|
|
|
2904
|
-
|
|
2953
|
+
fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2905
2954
|
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2906
2955
|
@intCast(val)
|
|
2956
|
+
else if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
|
|
2957
|
+
battle.rng.sleepDuration(player)
|
|
2907
2958
|
else if (showdown)
|
|
2908
|
-
|
|
2959
|
+
battle.rng.range(u3, 1, 8)
|
|
2909
2960
|
else duration: {
|
|
2910
2961
|
while (true) {
|
|
2911
2962
|
const r = battle.rng.next() & 7;
|
|
@@ -2914,50 +2965,58 @@ pub const Rolls = struct {
|
|
|
2914
2965
|
};
|
|
2915
2966
|
|
|
2916
2967
|
assert(duration >= 1 and duration <= 7);
|
|
2917
|
-
options.chance.duration(
|
|
2968
|
+
options.chance.duration(player, duration);
|
|
2918
2969
|
return duration;
|
|
2919
2970
|
}
|
|
2920
2971
|
|
|
2921
|
-
|
|
2972
|
+
fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
|
|
2922
2973
|
const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
|
|
2923
2974
|
val
|
|
2975
|
+
else if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
|
|
2976
|
+
battle.rng.disableDuration(player)
|
|
2924
2977
|
else if (showdown)
|
|
2925
|
-
|
|
2978
|
+
battle.rng.range(u4, 1, 9)
|
|
2926
2979
|
else
|
|
2927
2980
|
@intCast((battle.rng.next() & 7) + 1);
|
|
2928
2981
|
|
|
2929
2982
|
assert(duration >= 1 and duration <= 8);
|
|
2930
|
-
options.chance.duration(
|
|
2983
|
+
options.chance.duration(player, duration);
|
|
2931
2984
|
return duration;
|
|
2932
2985
|
}
|
|
2933
2986
|
|
|
2934
|
-
|
|
2987
|
+
fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2935
2988
|
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2936
2989
|
@intCast(val)
|
|
2990
|
+
else if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
|
|
2991
|
+
battle.rng.confusionDuration(player)
|
|
2937
2992
|
else if (showdown)
|
|
2938
|
-
|
|
2993
|
+
battle.rng.range(u3, 2, 6)
|
|
2939
2994
|
else
|
|
2940
2995
|
@intCast((battle.rng.next() & 3) + 2);
|
|
2941
2996
|
|
|
2942
2997
|
assert(duration >= 2 and duration <= 5);
|
|
2943
|
-
options.chance.duration(
|
|
2998
|
+
options.chance.duration(player, duration);
|
|
2944
2999
|
return duration;
|
|
2945
3000
|
}
|
|
2946
3001
|
|
|
2947
|
-
|
|
3002
|
+
fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2948
3003
|
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2949
3004
|
@intCast(val)
|
|
3005
|
+
else if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
|
|
3006
|
+
battle.rng.attackingDuration(player)
|
|
2950
3007
|
else if (showdown)
|
|
2951
|
-
|
|
3008
|
+
battle.rng.range(u3, 2, 4)
|
|
2952
3009
|
else
|
|
2953
3010
|
@intCast((battle.rng.next() & 1) + 2);
|
|
2954
3011
|
|
|
2955
3012
|
assert(duration >= 2 and duration <= 3);
|
|
2956
|
-
options.chance.duration(
|
|
3013
|
+
options.chance.duration(player, duration);
|
|
2957
3014
|
return duration;
|
|
2958
3015
|
}
|
|
2959
3016
|
|
|
2960
|
-
|
|
3017
|
+
const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
|
|
3018
|
+
|
|
3019
|
+
fn distribution(
|
|
2961
3020
|
battle: anytype,
|
|
2962
3021
|
comptime effect: Move.Effect,
|
|
2963
3022
|
player: Player,
|
|
@@ -2966,8 +3025,10 @@ pub const Rolls = struct {
|
|
|
2966
3025
|
const roll = if (effect == .Binding) .duration else .multi_hit;
|
|
2967
3026
|
const n: u3 = if (options.calc.overridden(player, roll)) |val|
|
|
2968
3027
|
@intCast(val)
|
|
3028
|
+
else if (@hasDecl(@TypeOf(battle.rng), "distribution"))
|
|
3029
|
+
battle.rng.distribution(player)
|
|
2969
3030
|
else if (showdown)
|
|
2970
|
-
DISTRIBUTION[battle.rng.range(
|
|
3031
|
+
DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
|
|
2971
3032
|
else n: {
|
|
2972
3033
|
const r = (battle.rng.next() & 3);
|
|
2973
3034
|
break :n @intCast((if (r < 2) r else battle.rng.next() & 3) + 2);
|
|
@@ -2975,7 +3036,7 @@ pub const Rolls = struct {
|
|
|
2975
3036
|
|
|
2976
3037
|
assert(n >= 2 and n <= 5);
|
|
2977
3038
|
if (effect == .Binding) {
|
|
2978
|
-
options.chance.duration(
|
|
3039
|
+
options.chance.duration(player, n);
|
|
2979
3040
|
} else {
|
|
2980
3041
|
try options.chance.multiHit(player, n);
|
|
2981
3042
|
}
|
|
@@ -2998,7 +3059,9 @@ pub const Rolls = struct {
|
|
|
2998
3059
|
else
|
|
2999
3060
|
null;
|
|
3000
3061
|
|
|
3001
|
-
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: {
|
|
3002
3065
|
if (showdown) {
|
|
3003
3066
|
if (check_pp == 0) {
|
|
3004
3067
|
var i: usize = moves.len;
|
|
@@ -3014,10 +3077,9 @@ pub const Rolls = struct {
|
|
|
3014
3077
|
while (i < moves.len and r > 0) : (i += 1) {
|
|
3015
3078
|
if (moves[i].pp > 0) {
|
|
3016
3079
|
r -= 1;
|
|
3017
|
-
if (r == 0) break;
|
|
3080
|
+
if (r == 0) break :slot @intCast(i + 1);
|
|
3018
3081
|
}
|
|
3019
3082
|
}
|
|
3020
|
-
break :slot @intCast(i + 1);
|
|
3021
3083
|
}
|
|
3022
3084
|
}
|
|
3023
3085
|
|
|
@@ -3039,11 +3101,11 @@ test "RNG agreement" {
|
|
|
3039
3101
|
expected[i] = @intCast(i * 0x1000000);
|
|
3040
3102
|
}
|
|
3041
3103
|
|
|
3042
|
-
var spe
|
|
3043
|
-
var cfz
|
|
3044
|
-
var par
|
|
3045
|
-
var brn
|
|
3046
|
-
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 };
|
|
3047
3109
|
|
|
3048
3110
|
for (0..expected.len) |i| {
|
|
3049
3111
|
try expectEqual(spe.range(u8, 0, 2) == 0, i < Gen12.percent(50) + 1);
|
|
@@ -3079,11 +3141,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3079
3141
|
const side = battle.side(player);
|
|
3080
3142
|
const foe = battle.foe(player);
|
|
3081
3143
|
|
|
3082
|
-
|
|
3144
|
+
const active = &side.active;
|
|
3083
3145
|
const stored = side.stored();
|
|
3084
3146
|
|
|
3085
3147
|
// While players are not given any input options on the cartridge in these cases,
|
|
3086
|
-
// 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
|
|
3087
3149
|
//
|
|
3088
3150
|
// Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
|
|
3089
3151
|
// here when not in compatibility mode, but the engine needs to be able to differentiate
|
|
@@ -3091,7 +3153,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3091
3153
|
// passing due to being forced into using a move. Instead of introducing another option
|
|
3092
3154
|
// we simply repurpose Move with no move slot, even though pedantically this is not
|
|
3093
3155
|
// strictly correct as the player would not have been presented the option to move or
|
|
3094
|
-
// switch at all
|
|
3156
|
+
// switch at all
|
|
3095
3157
|
if (isForced(active)) {
|
|
3096
3158
|
out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
|
|
3097
3159
|
n += 1;
|
|
@@ -3110,7 +3172,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3110
3172
|
// On the cartridge, all of these happen after "FIGHT" (indicating you are not
|
|
3111
3173
|
// switching) but before you are allowed to select a move. Pokémon Showdown instead
|
|
3112
3174
|
// either disables all other moves in the case of limited or requires you to select a
|
|
3113
|
-
// move normally if sleeping/frozen/bound
|
|
3175
|
+
// move normally if sleeping/frozen/bound
|
|
3114
3176
|
if (!showdown and (limited or foe.active.volatiles.Binding or
|
|
3115
3177
|
Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
|
|
3116
3178
|
{
|
|
@@ -3123,7 +3185,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3123
3185
|
// Pokémon Showdown handles Bide and Binding moves by checking if the move in question
|
|
3124
3186
|
// is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
|
|
3125
3187
|
// Move will not result in forcing the subsequent use unless the user also had the
|
|
3126
|
-
// proc-ed move in their moveset) and disabling all other moves
|
|
3188
|
+
// proc-ed move in their moveset) and disabling all other moves
|
|
3127
3189
|
if (limited) {
|
|
3128
3190
|
assert(showdown);
|
|
3129
3191
|
assert(side.last_selected_move != .None);
|
|
@@ -3133,7 +3195,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3133
3195
|
if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
|
|
3134
3196
|
// Pokémon Showdown displays Struggle if limited to Bide but unable to pick
|
|
3135
3197
|
const struggle =
|
|
3136
|
-
m.id == .Bide and (m.pp == 0 or active.volatiles.
|
|
3198
|
+
m.id == .Bide and (m.pp == 0 or active.volatiles.disable_move == slot);
|
|
3137
3199
|
const s = if (struggle) 0 else slot;
|
|
3138
3200
|
out[n] = .{ .type = .Move, .data = s };
|
|
3139
3201
|
n += 1;
|
|
@@ -3148,7 +3210,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3148
3210
|
const m = active.moves[slot - 1];
|
|
3149
3211
|
if (m.id == .None) break;
|
|
3150
3212
|
if (m.pp == 0) continue;
|
|
3151
|
-
if (active.volatiles.
|
|
3213
|
+
if (active.volatiles.disable_move == slot) continue;
|
|
3152
3214
|
out[n] = .{ .type = .Move, .data = slot };
|
|
3153
3215
|
n += 1;
|
|
3154
3216
|
}
|