@pkmn/engine 0.1.0-dev.30c63015 → 0.1.0-dev.3158fa12
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 -18
- package/build/pkg/addon/browser.d.ts +3 -0
- package/build/pkg/addon/browser.js +44 -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 +97 -0
- package/build/pkg/addon/node.js.map +1 -0
- package/build/pkg/addon/wasm.d.ts +2 -0
- package/build/pkg/addon/wasm.js +53 -0
- package/build/pkg/addon/wasm.js.map +1 -0
- package/build/pkg/addon.d.ts +25 -3
- package/build/pkg/addon.js +39 -71
- package/build/pkg/addon.js.map +1 -1
- package/build/pkg/data/layout.json +15 -2
- package/build/pkg/data/protocol.json +23 -9
- package/build/pkg/data.d.ts +27 -5
- package/build/pkg/data.js.map +1 -1
- package/build/pkg/gen1.d.ts +13 -17
- package/build/pkg/gen1.js +6 -10
- package/build/pkg/gen1.js.map +1 -1
- package/build/pkg/index.d.ts +40 -91
- package/build/pkg/index.js +6 -63
- package/build/pkg/index.js.map +1 -1
- package/build/pkg/protocol.d.ts +20 -1
- package/build/pkg/protocol.js +109 -107
- package/build/pkg/protocol.js.map +1 -1
- package/build/tools/debug.js +36 -71
- package/build/tools/debug.js.map +1 -1
- package/build/tools/display/debug.jsx +141 -0
- package/build/tools/display/debug.jsx.map +1 -0
- package/build/tools/display/index.js +142 -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 +287 -89
- package/package.json +58 -132
- package/src/bin/install-pkmn-engine +30 -20
- package/src/bin/pkmn-debug +1 -1
- package/src/lib/{binding → bindings}/node.zig +8 -21
- package/src/lib/bindings/wasm.zig +59 -0
- package/src/lib/common/array.zig +77 -0
- package/src/lib/common/data.zig +4 -4
- package/src/lib/common/debug.zig +39 -8
- package/src/lib/common/js.zig +4 -2
- package/src/lib/common/optional.zig +14 -10
- package/src/lib/common/options.zig +6 -0
- package/src/lib/common/protocol.zig +223 -215
- package/src/lib/common/rational.zig +128 -32
- package/src/lib/common/rng.zig +7 -9
- package/src/lib/common/util.zig +50 -1
- package/src/lib/gen1/calc.zig +686 -159
- package/src/lib/gen1/chance.zig +405 -280
- package/src/lib/gen1/data/moves.zig +2 -4
- package/src/lib/gen1/data/species.zig +2 -4
- package/src/lib/gen1/data/types.zig +1 -2
- package/src/lib/gen1/data.zig +24 -27
- package/src/lib/gen1/helpers.zig +92 -548
- package/src/lib/gen1/mechanics.zig +517 -411
- package/src/lib/gen2/calc.zig +37 -27
- package/src/lib/gen2/chance.zig +84 -496
- package/src/lib/gen2/data/items.zig +1 -3
- package/src/lib/gen2/data/moves.zig +2 -4
- package/src/lib/gen2/data/species.zig +1 -3
- package/src/lib/gen2/data/types.zig +1 -2
- package/src/lib/gen2/data.zig +19 -22
- package/src/lib/gen2/generated.zig +19 -26
- package/src/lib/gen2/helpers.zig +46 -25
- package/src/lib/gen2/mechanics.zig +367 -336
- package/src/lib/node.zig +18 -0
- package/src/lib/pkmn.zig +37 -15
- package/src/lib/wasm.zig +17 -0
- package/src/tools/display/dom.ts +96 -0
- package/src/tools/display/ui.css +354 -0
- package/build/test/display.js +0 -400
- package/build/test/display.js.map +0 -1
- package/src/lib/binding/wasm.zig +0 -12
- package/src/lib/gen1/README.md +0 -894
- package/src/lib/gen2/README.md +0 -329
- package/src/test/display/pkmn.html.tmpl +0 -368
|
@@ -1,41 +1,29 @@
|
|
|
1
|
-
const
|
|
2
|
-
|
|
3
|
-
const pkmn = @import("../pkmn.zig");
|
|
1
|
+
const DEBUG = @import("../common/debug.zig").print;
|
|
4
2
|
|
|
3
|
+
const builtin = @import("builtin");
|
|
4
|
+
const chance = @import("chance.zig");
|
|
5
5
|
const common = @import("../common/data.zig");
|
|
6
|
-
const
|
|
6
|
+
const data = @import("data.zig");
|
|
7
|
+
const pkmn = @import("../pkmn.zig");
|
|
7
8
|
const protocol = @import("../common/protocol.zig");
|
|
8
9
|
const rng = @import("../common/rng.zig");
|
|
9
|
-
|
|
10
|
-
const data = @import("data.zig");
|
|
10
|
+
const std = @import("std");
|
|
11
11
|
|
|
12
12
|
const assert = std.debug.assert;
|
|
13
|
-
|
|
14
|
-
const expectEqual = std.testing.expectEqual;
|
|
15
|
-
|
|
16
|
-
const showdown = pkmn.options.showdown;
|
|
17
|
-
|
|
18
|
-
const Choice = common.Choice;
|
|
19
|
-
const ID = common.ID;
|
|
20
|
-
const Player = common.Player;
|
|
21
|
-
const Result = common.Result;
|
|
22
|
-
|
|
23
13
|
const Boost = protocol.Boost;
|
|
24
|
-
const
|
|
25
|
-
const CureStatus = protocol.CureStatus;
|
|
14
|
+
const Choice = common.Choice;
|
|
26
15
|
const Damage = protocol.Damage;
|
|
27
|
-
const
|
|
16
|
+
const Effectiveness = data.Effectiveness;
|
|
17
|
+
const expectEqual = std.testing.expectEqual;
|
|
28
18
|
const Fail = protocol.Fail;
|
|
29
|
-
const Heal = protocol.Heal;
|
|
30
|
-
const Immune = protocol.Immune;
|
|
31
|
-
|
|
32
19
|
const Gen12 = rng.Gen12;
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
const Effectiveness = data.Effectiveness;
|
|
20
|
+
const Heal = protocol.Heal;
|
|
21
|
+
const ID = common.ID;
|
|
36
22
|
const Move = data.Move;
|
|
37
23
|
const MoveSlot = data.MoveSlot;
|
|
38
|
-
const
|
|
24
|
+
const Player = common.Player;
|
|
25
|
+
const Result = common.Result;
|
|
26
|
+
const showdown = pkmn.options.showdown;
|
|
39
27
|
const Side = data.Side;
|
|
40
28
|
const Species = data.Species;
|
|
41
29
|
const Stats = data.Stats;
|
|
@@ -75,8 +63,8 @@ pub fn update(battle: anytype, c1: Choice, c2: Choice, options: anytype) !Result
|
|
|
75
63
|
var p1 = battle.side(.P1);
|
|
76
64
|
var p2 = battle.side(.P2);
|
|
77
65
|
|
|
78
|
-
|
|
79
|
-
|
|
66
|
+
const r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
|
|
67
|
+
const r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
|
|
80
68
|
|
|
81
69
|
if (try turnOrder(battle, c1, c2, options) == .P1) {
|
|
82
70
|
if (try doTurn(battle, .P1, c1, r1, s1, .P2, c2, r2, s2, options)) |r| return r;
|
|
@@ -94,11 +82,11 @@ fn start(battle: anytype, options: anytype) !Result {
|
|
|
94
82
|
const p1 = battle.side(.P1);
|
|
95
83
|
const p2 = battle.side(.P2);
|
|
96
84
|
|
|
97
|
-
|
|
85
|
+
const p1_slot = findFirstAlive(p1);
|
|
98
86
|
assert(!showdown or p1_slot == 1);
|
|
99
87
|
if (p1_slot == 0) return if (findFirstAlive(p2) == 0) Result.Tie else Result.Lose;
|
|
100
88
|
|
|
101
|
-
|
|
89
|
+
const p2_slot = findFirstAlive(p2);
|
|
102
90
|
assert(!showdown or p2_slot == 1);
|
|
103
91
|
if (p2_slot == 0) return Result.Win;
|
|
104
92
|
|
|
@@ -170,7 +158,7 @@ fn selectMove(
|
|
|
170
158
|
// to satisfy the conditions of the Desync Clause Mod. However, because Binding is still
|
|
171
159
|
// set the selected move will not actually be used, it will just be reported as having
|
|
172
160
|
// been used (this differs from how Pokémon Showdown works, but its impossible to
|
|
173
|
-
// replicate the incorrect behavior with the correct mechanisms)
|
|
161
|
+
// replicate the incorrect behavior with the correct mechanisms)
|
|
174
162
|
saveMove(battle, player, choice);
|
|
175
163
|
} else {
|
|
176
164
|
assert(choice.data == 0);
|
|
@@ -178,7 +166,10 @@ fn selectMove(
|
|
|
178
166
|
if (foe_choice.type == .Switch) {
|
|
179
167
|
const last = side.active.move(battle.lastMove(player).index);
|
|
180
168
|
if (last.id == .Metronome) side.last_selected_move = last.id;
|
|
181
|
-
if (last.id == .MirrorMove)
|
|
169
|
+
if (last.id == .MirrorMove) {
|
|
170
|
+
if (!pkmn.options.mod) return Result.Error;
|
|
171
|
+
side.last_selected_move = last.id;
|
|
172
|
+
}
|
|
182
173
|
}
|
|
183
174
|
}
|
|
184
175
|
return null;
|
|
@@ -257,17 +248,17 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: a
|
|
|
257
248
|
active.moves = incoming.moves;
|
|
258
249
|
|
|
259
250
|
statusModify(incoming.status, &active.stats);
|
|
251
|
+
options.chance.switched(player, slot);
|
|
260
252
|
|
|
261
253
|
foe.active.volatiles.Binding = false;
|
|
262
254
|
|
|
263
255
|
try options.log.switched(.{ battle.active(player), incoming });
|
|
264
|
-
options.chance.switched(player, side.order[0], out);
|
|
265
256
|
|
|
266
257
|
if (showdown and incoming.status == Status.TOX) {
|
|
267
258
|
incoming.status = Status.init(.PSN);
|
|
268
259
|
// Technically, Pokémon Showdown adds these after *both* Pokémon have switched, but we'd
|
|
269
260
|
// rather not clutter up turnOrder just for this (incorrect) log message
|
|
270
|
-
try options.log.status(.{ battle.active(player), incoming.status,
|
|
261
|
+
try options.log.status(.{ battle.active(player), incoming.status, .Silent });
|
|
271
262
|
}
|
|
272
263
|
}
|
|
273
264
|
|
|
@@ -351,13 +342,13 @@ fn doTurn(
|
|
|
351
342
|
)) |r| return r;
|
|
352
343
|
if (!replace) {
|
|
353
344
|
if (player_choice.type != .Switch) {
|
|
354
|
-
if (try checkFaint(battle, foe_player, options)) |r| return r;
|
|
345
|
+
if (try checkFaint(battle, foe_player, true, options)) |r| return r;
|
|
355
346
|
}
|
|
356
347
|
if (residual) try handleResidual(battle, player, options);
|
|
357
|
-
if (try checkFaint(battle, player, options)) |r| return r;
|
|
348
|
+
if (try checkFaint(battle, player, false, options)) |r| return r;
|
|
358
349
|
} else if (foe_choice.type == .Pass) return null;
|
|
359
350
|
|
|
360
|
-
options.chance.clearPending();
|
|
351
|
+
if (!showdown) options.chance.clearPending();
|
|
361
352
|
|
|
362
353
|
residual = true;
|
|
363
354
|
replace = battle.side(foe_player).stored().hp == 0;
|
|
@@ -373,10 +364,10 @@ fn doTurn(
|
|
|
373
364
|
)) |r| return r;
|
|
374
365
|
if (!replace) {
|
|
375
366
|
if (!calc and foe_choice.type != .Switch) {
|
|
376
|
-
if (try checkFaint(battle, player, options)) |r| return r;
|
|
367
|
+
if (try checkFaint(battle, player, true, options)) |r| return r;
|
|
377
368
|
}
|
|
378
369
|
if (residual) try handleResidual(battle, foe_player, options);
|
|
379
|
-
if (try checkFaint(battle, foe_player, options)) |r| return r;
|
|
370
|
+
if (try checkFaint(battle, foe_player, false, options)) |r| return r;
|
|
380
371
|
}
|
|
381
372
|
|
|
382
373
|
// Flinch is bugged on Pokémon Showdown because it gets implemented with a duration which causes
|
|
@@ -411,7 +402,7 @@ fn executeMove(
|
|
|
411
402
|
if (side.last_selected_move == .SKIP_TURN) {
|
|
412
403
|
assert(!showdown);
|
|
413
404
|
if (battle.foe(player).active.volatiles.Binding) {
|
|
414
|
-
try options.log.cant(.{ battle.active(player),
|
|
405
|
+
try options.log.cant(.{ battle.active(player), .Bound });
|
|
415
406
|
}
|
|
416
407
|
return null;
|
|
417
408
|
}
|
|
@@ -429,13 +420,13 @@ fn executeMove(
|
|
|
429
420
|
mslot = @intCast(battle.lastMove(player).index);
|
|
430
421
|
const stored = side.stored();
|
|
431
422
|
// GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
|
|
432
|
-
auto = isForced(&side.active) or
|
|
423
|
+
auto = pkmn.options.mod or isForced(&side.active) or
|
|
433
424
|
side.active.volatiles.Binding or side.active.volatiles.Bide or
|
|
434
425
|
side.last_selected_move == .HyperBeam or
|
|
435
426
|
Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
|
|
436
427
|
// If it wasn't Hyper Beam or the continuation of a move effect then we must have just
|
|
437
428
|
// thawed, in which case we will desync unless the last_selected_move happened to be at
|
|
438
|
-
// index 1 and the current Pokémon has the same move in its first slot
|
|
429
|
+
// index 1 and the current Pokémon has the same move in its first slot
|
|
439
430
|
if (!auto) {
|
|
440
431
|
// side.active.moves(slot) is safe to check even though the slot in question might not
|
|
441
432
|
// technically be from this Pokémon because it must be exactly 1 to not desync and
|
|
@@ -462,6 +453,25 @@ fn executeMove(
|
|
|
462
453
|
.err => return @as(?Result, Result.Error),
|
|
463
454
|
}
|
|
464
455
|
|
|
456
|
+
// Pokémon Showdown incorrectly implements PP deduction when handling the Hyper Beam automatic
|
|
457
|
+
// selection glitch - if the move was proced due to Metronome / Mirror Move it either subtracts
|
|
458
|
+
// from the Hyper Beam slot if present or skips PP deduction entirely...
|
|
459
|
+
if (showdown and !skip_pp and side.last_selected_move == .HyperBeam and
|
|
460
|
+
side.last_selected_move != side.active.move(mslot).id)
|
|
461
|
+
{
|
|
462
|
+
assert(mslot == 1);
|
|
463
|
+
const has_beam = has_beam: {
|
|
464
|
+
for (side.active.moves, 0..) |m, i| {
|
|
465
|
+
if (m.id == .HyperBeam) {
|
|
466
|
+
mslot = @intCast(i + 1);
|
|
467
|
+
break :has_beam true;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
break :has_beam false;
|
|
471
|
+
};
|
|
472
|
+
if (!has_beam) skip_pp = true;
|
|
473
|
+
}
|
|
474
|
+
|
|
465
475
|
const can = skip_can or
|
|
466
476
|
try canMove(battle, player, mslot, auto, skip_pp, .None, residual, options);
|
|
467
477
|
if (!can) return null;
|
|
@@ -488,36 +498,41 @@ fn beforeMove(
|
|
|
488
498
|
|
|
489
499
|
if (Status.is(stored.status, .SLP)) {
|
|
490
500
|
const before = stored.status;
|
|
501
|
+
const slf = Status.is(stored.status, .EXT);
|
|
491
502
|
// Even if the EXT bit is set this will still correctly modify the sleep duration
|
|
492
|
-
if (options.calc.
|
|
493
|
-
|
|
503
|
+
if (options.calc.overridden(player, .sleep)) |obs| switch (obs) {
|
|
504
|
+
.started, .ended => stored.status = 0,
|
|
505
|
+
.continuing => if (Status.duration(stored.status) > 1) {
|
|
506
|
+
stored.status -= 1;
|
|
507
|
+
},
|
|
508
|
+
else => unreachable,
|
|
494
509
|
} else {
|
|
495
510
|
stored.status -= 1;
|
|
496
511
|
}
|
|
497
512
|
|
|
498
513
|
const duration = Status.duration(stored.status);
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
514
|
+
try options.chance.sleep(player, if (slf)
|
|
515
|
+
.None
|
|
516
|
+
else if (duration == 0) .ended else .continuing);
|
|
502
517
|
|
|
503
518
|
if (duration == 0) {
|
|
504
|
-
try log.curestatus(.{ ident, before,
|
|
519
|
+
try log.curestatus(.{ ident, before, .Message });
|
|
505
520
|
stored.status = 0; // clears EXT if present
|
|
506
521
|
} else {
|
|
507
|
-
try log.cant(.{ ident,
|
|
522
|
+
try log.cant(.{ ident, .Sleep });
|
|
508
523
|
}
|
|
509
524
|
side.last_used_move = .None;
|
|
510
525
|
return .done;
|
|
511
526
|
}
|
|
512
527
|
|
|
513
528
|
if (Status.is(stored.status, .FRZ)) {
|
|
514
|
-
try log.cant(.{ ident,
|
|
529
|
+
try log.cant(.{ ident, .Freeze });
|
|
515
530
|
side.last_used_move = .None;
|
|
516
531
|
return .done;
|
|
517
532
|
}
|
|
518
533
|
|
|
519
534
|
if (skip or foe.active.volatiles.Binding) {
|
|
520
|
-
try log.cant(.{ ident,
|
|
535
|
+
try log.cant(.{ ident, .Bound });
|
|
521
536
|
return .done;
|
|
522
537
|
}
|
|
523
538
|
|
|
@@ -525,27 +540,27 @@ fn beforeMove(
|
|
|
525
540
|
// Pokémon Showdown doesn't clear Flinch until its imaginary "residual" phase, meaning
|
|
526
541
|
// Pokémon can sometimes flinch multiple times from the same original hit
|
|
527
542
|
if (!showdown) volatiles.Flinch = false;
|
|
528
|
-
try log.cant(.{ ident,
|
|
543
|
+
try log.cant(.{ ident, .Flinch });
|
|
529
544
|
return .done;
|
|
530
545
|
}
|
|
531
546
|
|
|
532
547
|
if (volatiles.Recharging) {
|
|
533
548
|
volatiles.Recharging = false;
|
|
534
|
-
try log.cant(.{ ident,
|
|
549
|
+
try log.cant(.{ ident, .Recharge });
|
|
535
550
|
return .done;
|
|
536
551
|
}
|
|
537
552
|
|
|
538
553
|
if (volatiles.disable_duration > 0) {
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
554
|
+
volatiles.disable_duration =
|
|
555
|
+
decrement(.disable, player, options, volatiles.disable_duration);
|
|
556
|
+
try options.chance.disable(
|
|
557
|
+
player,
|
|
558
|
+
if (volatiles.disable_duration == 0) .ended else .continuing,
|
|
559
|
+
);
|
|
545
560
|
|
|
546
561
|
if (volatiles.disable_duration == 0) {
|
|
547
562
|
volatiles.disable_move = 0;
|
|
548
|
-
try log.end(.{ ident,
|
|
563
|
+
try log.end(.{ ident, .Disable });
|
|
549
564
|
}
|
|
550
565
|
}
|
|
551
566
|
// Pokémon Showdown's disable condition has a single onBeforeMove handler
|
|
@@ -554,25 +569,30 @@ fn beforeMove(
|
|
|
554
569
|
// This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
|
|
555
570
|
// before the side had a selected a move - we simply need to assume this leads to a desync
|
|
556
571
|
if (side.last_selected_move == .None) {
|
|
557
|
-
assert(!
|
|
572
|
+
assert(!pkmn.options.mod);
|
|
558
573
|
return .err;
|
|
559
574
|
}
|
|
560
575
|
|
|
561
576
|
if (volatiles.Confusion) {
|
|
562
577
|
assert(volatiles.confusion > 0);
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
if (
|
|
578
|
+
if (options.calc.overridden(player, .confusion)) |obs| switch (obs) {
|
|
579
|
+
.started, .ended => volatiles.confusion = 0,
|
|
580
|
+
.continuing => if (volatiles.confusion > 1) {
|
|
581
|
+
volatiles.confusion -= 1;
|
|
582
|
+
},
|
|
583
|
+
.overwritten => {},
|
|
584
|
+
else => unreachable,
|
|
566
585
|
} else {
|
|
567
586
|
volatiles.confusion -= 1;
|
|
568
587
|
}
|
|
569
|
-
|
|
588
|
+
|
|
589
|
+
try options.chance.confusion(player, if (volatiles.confusion == 0) .ended else .continuing);
|
|
570
590
|
|
|
571
591
|
if (volatiles.confusion == 0) {
|
|
572
592
|
volatiles.Confusion = false;
|
|
573
|
-
try log.end(.{ ident,
|
|
593
|
+
try log.end(.{ ident, .Confusion });
|
|
574
594
|
} else {
|
|
575
|
-
try log.activate(ident, .Confusion);
|
|
595
|
+
try log.activate(.{ ident, .Confusion });
|
|
576
596
|
|
|
577
597
|
if (try Rolls.confused(battle, player, options)) {
|
|
578
598
|
assert(!volatiles.MultiHit);
|
|
@@ -584,7 +604,9 @@ fn beforeMove(
|
|
|
584
604
|
volatiles.Charging = false;
|
|
585
605
|
volatiles.Binding = false;
|
|
586
606
|
volatiles.Invulnerable = false;
|
|
587
|
-
|
|
607
|
+
|
|
608
|
+
options.chance.observe(.attacking, player, .None);
|
|
609
|
+
options.chance.observe(.binding, player, .None);
|
|
588
610
|
{
|
|
589
611
|
// This feels (and is) disgusting but the cartridge literally just overwrites
|
|
590
612
|
// the opponent's defense with the user's defense and resets it after. As a
|
|
@@ -592,7 +614,9 @@ fn beforeMove(
|
|
|
592
614
|
const def = foe.active.stats.def;
|
|
593
615
|
foe.active.stats.def = active.stats.def;
|
|
594
616
|
defer foe.active.stats.def = def;
|
|
595
|
-
if (!calcDamage(battle, player, player.foe(), null, false))
|
|
617
|
+
if (!calcDamage(battle, player, player.foe(), null, false, options)) {
|
|
618
|
+
return .err;
|
|
619
|
+
}
|
|
596
620
|
}
|
|
597
621
|
// Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
|
|
598
622
|
// on the targeting behavior of the confused Pokémon's selected move which results
|
|
@@ -621,9 +645,12 @@ fn beforeMove(
|
|
|
621
645
|
volatiles.Thrashing = false;
|
|
622
646
|
volatiles.Charging = false;
|
|
623
647
|
volatiles.Binding = false;
|
|
624
|
-
|
|
648
|
+
|
|
649
|
+
options.chance.observe(.attacking, player, .None);
|
|
650
|
+
options.chance.observe(.binding, player, .None);
|
|
651
|
+
|
|
625
652
|
// GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
|
|
626
|
-
try log.cant(.{ ident,
|
|
653
|
+
try log.cant(.{ ident, .Paralysis });
|
|
627
654
|
return .done;
|
|
628
655
|
}
|
|
629
656
|
|
|
@@ -639,28 +666,22 @@ fn beforeMove(
|
|
|
639
666
|
volatiles.state +%= battle.last_damage;
|
|
640
667
|
}
|
|
641
668
|
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
if (options.calc.modify(player, .attacking)) |extend| {
|
|
645
|
-
if (!extend) volatiles.attacks = 0;
|
|
646
|
-
} else {
|
|
647
|
-
volatiles.attacks -= 1;
|
|
648
|
-
}
|
|
649
|
-
try options.chance.attacking(player, volatiles.attacks);
|
|
669
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
670
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
650
671
|
|
|
651
672
|
if (volatiles.attacks != 0) {
|
|
652
|
-
try log.activate(ident, .Bide);
|
|
673
|
+
try log.activate(.{ ident, .Bide });
|
|
653
674
|
return .done;
|
|
654
675
|
}
|
|
655
676
|
|
|
656
677
|
volatiles.Bide = false;
|
|
657
|
-
try log.end(.{ ident,
|
|
678
|
+
try log.end(.{ ident, .Bide });
|
|
658
679
|
|
|
659
680
|
battle.last_damage = volatiles.state *% 2;
|
|
660
681
|
volatiles.state = 0;
|
|
661
682
|
|
|
662
683
|
if (battle.last_damage == 0) {
|
|
663
|
-
try log.fail(.{ ident,
|
|
684
|
+
try log.fail(.{ ident, .None });
|
|
664
685
|
return .done;
|
|
665
686
|
}
|
|
666
687
|
|
|
@@ -680,39 +701,40 @@ fn beforeMove(
|
|
|
680
701
|
}
|
|
681
702
|
|
|
682
703
|
if (volatiles.Thrashing) {
|
|
683
|
-
|
|
704
|
+
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
684
705
|
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
} else {
|
|
688
|
-
volatiles.attacks -= 1;
|
|
689
|
-
}
|
|
690
|
-
try options.chance.attacking(player, volatiles.attacks);
|
|
706
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
707
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
691
708
|
|
|
692
|
-
if (
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
709
|
+
if (volatiles.attacks == 0) {
|
|
710
|
+
const overwritten = volatiles.Confusion;
|
|
711
|
+
|
|
712
|
+
volatiles.Thrashing = false;
|
|
713
|
+
volatiles.Confusion = true;
|
|
714
|
+
volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
715
|
+
|
|
716
|
+
// On Pokémon Showdown this could leak information to the opponent that thrashing has
|
|
717
|
+
// ended (the engine's protocol is always from the omniscient perspective and clients
|
|
718
|
+
// can be expected to recognize this pattern and pass the information only to the
|
|
719
|
+
// correct player)
|
|
720
|
+
try log.start(.{ battle.active(player), .ConfusionSilent });
|
|
721
|
+
options.chance.observe(.confusion, player, if (overwritten)
|
|
722
|
+
if (pkmn.options.overwrite) .overwritten else .continuing
|
|
723
|
+
else
|
|
724
|
+
.started);
|
|
725
|
+
if (!overwritten) options.calc.confusion(player);
|
|
703
726
|
}
|
|
727
|
+
|
|
728
|
+
// This shouldn't actually set last_used_move, but Pokémon Showdown sets last
|
|
729
|
+
// used in useMove and doesn't have the notion of skipping canMove semantics
|
|
730
|
+
if (showdown) side.last_used_move = side.last_selected_move;
|
|
731
|
+
|
|
704
732
|
return .skip_can;
|
|
705
733
|
}
|
|
706
734
|
|
|
707
735
|
if (volatiles.Binding) {
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
if (options.calc.modify(player, .binding)) |extend| {
|
|
711
|
-
if (!extend) volatiles.attacks = 0;
|
|
712
|
-
} else {
|
|
713
|
-
volatiles.attacks -= 1;
|
|
714
|
-
}
|
|
715
|
-
try options.chance.binding(player, volatiles.attacks);
|
|
736
|
+
volatiles.attacks = decrement(.binding, player, options, volatiles.attacks);
|
|
737
|
+
try options.chance.binding(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
716
738
|
|
|
717
739
|
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
718
740
|
if (showdown or battle.last_damage != 0) {
|
|
@@ -742,7 +764,6 @@ fn canMove(
|
|
|
742
764
|
const player_ident = battle.active(player);
|
|
743
765
|
const move = Move.get(side.last_selected_move);
|
|
744
766
|
|
|
745
|
-
var skip = skip_pp;
|
|
746
767
|
if (side.active.volatiles.Charging) {
|
|
747
768
|
side.active.volatiles.Charging = false;
|
|
748
769
|
side.active.volatiles.Invulnerable = false;
|
|
@@ -754,7 +775,7 @@ fn canMove(
|
|
|
754
775
|
}
|
|
755
776
|
|
|
756
777
|
side.last_used_move = side.last_selected_move;
|
|
757
|
-
if (!
|
|
778
|
+
if (!skip_pp) decrementPP(side, mslot, auto);
|
|
758
779
|
|
|
759
780
|
const target = if (move.target == .Self) player else player.foe();
|
|
760
781
|
try options.log.move(.{ player_ident, side.last_selected_move, battle.active(target), from });
|
|
@@ -767,9 +788,14 @@ fn canMove(
|
|
|
767
788
|
|
|
768
789
|
if (move.effect == .Thrashing) {
|
|
769
790
|
Effects.thrashing(battle, player, options);
|
|
770
|
-
} else if (
|
|
771
|
-
// Pokémon Showdown handles this after hit/miss checks and damage calculation
|
|
772
|
-
|
|
791
|
+
} else if (move.effect == .Binding) {
|
|
792
|
+
// Pokémon Showdown handles this after hit/miss checks and damage calculation, though clears
|
|
793
|
+
// Recharging in onTryMove (required for the Hyper Beam automatic selection glitch)
|
|
794
|
+
if (showdown) {
|
|
795
|
+
battle.foe(player).active.volatiles.Recharging = false;
|
|
796
|
+
} else {
|
|
797
|
+
try Effects.binding(battle, player, false, options);
|
|
798
|
+
}
|
|
773
799
|
}
|
|
774
800
|
|
|
775
801
|
return true;
|
|
@@ -806,7 +832,7 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
|
|
|
806
832
|
assert(active.move(mslot).pp == side.stored().move(mslot).pp);
|
|
807
833
|
}
|
|
808
834
|
|
|
809
|
-
fn incrementPP(side: *Side, mslot: u4) void {
|
|
835
|
+
fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
|
|
810
836
|
var active = &side.active;
|
|
811
837
|
const volatiles = &active.volatiles;
|
|
812
838
|
|
|
@@ -814,8 +840,11 @@ fn incrementPP(side: *Side, mslot: u4) void {
|
|
|
814
840
|
// GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
|
|
815
841
|
if (showdown and volatiles.Transform) return;
|
|
816
842
|
|
|
843
|
+
const n = options.calc.overridden(player, .pp) orelse 1;
|
|
844
|
+
if (volatiles.Transform and !options.chance.pp(player, n)) return;
|
|
845
|
+
|
|
817
846
|
assert(mslot > 0 and mslot <= 4);
|
|
818
|
-
side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +%
|
|
847
|
+
side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% n;
|
|
819
848
|
}
|
|
820
849
|
|
|
821
850
|
// Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
|
|
@@ -863,13 +892,13 @@ fn doMove(
|
|
|
863
892
|
break :miss true;
|
|
864
893
|
}
|
|
865
894
|
break :miss (move.target != .Self and
|
|
866
|
-
!moveHit(battle, player, move, &immune, &mist, options));
|
|
895
|
+
!try moveHit(battle, player, move, &immune, &mist, options));
|
|
867
896
|
};
|
|
868
897
|
assert(!immune or miss or (showdown and move.effect == .Binding));
|
|
869
898
|
|
|
870
899
|
var late = showdown and move.effect != .Explode;
|
|
871
900
|
const skip = status or immune;
|
|
872
|
-
if ((!showdown or (!skip or counter)) and !miss)
|
|
901
|
+
if ((!showdown or (!skip or counter)) and !miss) damage: {
|
|
873
902
|
if (showdown and move.effect.isMulti()) {
|
|
874
903
|
try Effects.multiHit(battle, player, move, options);
|
|
875
904
|
hits = side.active.volatiles.attacks;
|
|
@@ -891,8 +920,9 @@ fn doMove(
|
|
|
891
920
|
ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
|
|
892
921
|
// This can overflow after adjustDamage, but will still be sufficient to OHKO
|
|
893
922
|
battle.last_damage = if (ohko) 65535 else 0;
|
|
894
|
-
if (showdown) break :
|
|
895
|
-
} else if (!calcDamage(battle, player, player.foe(), move, crit)) {
|
|
923
|
+
if (showdown) break :damage; // skip adjustDamage / randomizeDamage
|
|
924
|
+
} else if (!calcDamage(battle, player, player.foe(), move, crit, options)) {
|
|
925
|
+
if (!showdown) try options.chance.commit(player, .err);
|
|
896
926
|
return @as(?Result, Result.Error);
|
|
897
927
|
}
|
|
898
928
|
if (battle.last_damage == 0) {
|
|
@@ -906,11 +936,12 @@ fn doMove(
|
|
|
906
936
|
}
|
|
907
937
|
}
|
|
908
938
|
|
|
909
|
-
var
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
939
|
+
var missed: bool = undefined;
|
|
940
|
+
const zero = battle.last_damage == 0;
|
|
941
|
+
if (!showdown and !skip) {
|
|
942
|
+
missed = !try moveHit(battle, player, move, &immune, &mist, options);
|
|
943
|
+
miss = missed or zero;
|
|
944
|
+
}
|
|
914
945
|
|
|
915
946
|
assert(showdown or miss or battle.last_damage > 0 or skip);
|
|
916
947
|
assert((!showdown and miss) or !(ohko and immune));
|
|
@@ -934,14 +965,35 @@ fn doMove(
|
|
|
934
965
|
ohko = (!showdown or (!immune and !invulnerable)) and
|
|
935
966
|
move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
|
|
936
967
|
if (ohko) {
|
|
937
|
-
try log.immune(.{ foe_ident,
|
|
968
|
+
try log.immune(.{ foe_ident, .OHKO });
|
|
938
969
|
} else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
|
|
939
|
-
try log.immune(.{ foe_ident,
|
|
970
|
+
try log.immune(.{ foe_ident, .None });
|
|
971
|
+
if (!showdown) {
|
|
972
|
+
if (move.effect == .Binding) {
|
|
973
|
+
// Rather obnoxiously, the cartridge sets the Binding volatile in canMove and
|
|
974
|
+
// then simply clears it later on if the move happens to miss in moveHit.
|
|
975
|
+
// However, moveHit can return true and we'll still end up in the outer miss
|
|
976
|
+
// handling block because attacking into an immune Pokemon does zero damage
|
|
977
|
+
// (considered missing)
|
|
978
|
+
assert(zero);
|
|
979
|
+
// Thus we still need to actually save the hit result (but not the crit roll).
|
|
980
|
+
// Using .miss for this commit would accomplish what we want, but is somewhat
|
|
981
|
+
// misleading as its entirely possible the move actually did hit (and the target
|
|
982
|
+
// is now trapped) so we use the .binding value instead which is effectively
|
|
983
|
+
// identical, just doesn't claim to know about the actual results of the hit
|
|
984
|
+
// roll)
|
|
985
|
+
try options.chance.commit(player, .binding);
|
|
986
|
+
} else {
|
|
987
|
+
// The .glitch option allows for conditionally committing if we are in a
|
|
988
|
+
// division-by-zero freeze alternative timeline scenario
|
|
989
|
+
try options.chance.commit(player, .glitch);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
940
992
|
} else if (mist) {
|
|
941
|
-
if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
|
|
942
|
-
try log.fail(.{ foe_ident,
|
|
993
|
+
if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
|
|
994
|
+
try log.fail(.{ foe_ident, .None });
|
|
943
995
|
} else {
|
|
944
|
-
if (showdown
|
|
996
|
+
if (!showdown) try options.chance.commit(player, if (zero and !missed) .hit else .miss);
|
|
945
997
|
try log.lastmiss(.{});
|
|
946
998
|
try log.miss(.{battle.active(player)});
|
|
947
999
|
}
|
|
@@ -956,17 +1008,15 @@ fn doMove(
|
|
|
956
1008
|
try buildRage(battle, player.foe(), options);
|
|
957
1009
|
} else if (showdown and move.effect == .Disable) {
|
|
958
1010
|
try buildRage(battle, player.foe(), options);
|
|
959
|
-
} else if (!showdown and immune and move.effect == .Binding) {
|
|
960
|
-
try options.chance.commit(player, .binding);
|
|
961
1011
|
}
|
|
962
1012
|
return null;
|
|
963
|
-
} else {
|
|
1013
|
+
} else if (!showdown) {
|
|
964
1014
|
try options.chance.commit(player, .hit);
|
|
965
1015
|
}
|
|
966
1016
|
|
|
967
1017
|
// On the cartridge MultiHit doesn't get set up until after damage has been applied for the
|
|
968
1018
|
// first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
|
|
969
|
-
// it up above before damage calculation)
|
|
1019
|
+
// it up above before damage calculation)
|
|
970
1020
|
if (!showdown and move.effect.isMulti()) {
|
|
971
1021
|
try Effects.multiHit(battle, player, move, options);
|
|
972
1022
|
hits = side.active.volatiles.attacks;
|
|
@@ -1005,32 +1055,30 @@ fn doMove(
|
|
|
1005
1055
|
try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
1006
1056
|
}
|
|
1007
1057
|
try log.hitcount(.{ battle.active(player.foe()), hit });
|
|
1008
|
-
} else if (showdown) {
|
|
1058
|
+
} else if (showdown and move.effect == .Binding) {
|
|
1009
1059
|
// This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
try buildRage(battle, player.foe(), options);
|
|
1021
|
-
}
|
|
1022
|
-
return null;
|
|
1060
|
+
try Effects.binding(battle, player, rewrap, options);
|
|
1061
|
+
if (immune) {
|
|
1062
|
+
battle.last_damage = 0;
|
|
1063
|
+
assert(foe.stored().hp > 0);
|
|
1064
|
+
// Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
|
|
1065
|
+
if (sub) {
|
|
1066
|
+
try log.activate(.{ battle.active(player.foe()), .Substitute });
|
|
1067
|
+
} else {
|
|
1068
|
+
try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
|
|
1069
|
+
try buildRage(battle, player.foe(), options);
|
|
1023
1070
|
}
|
|
1071
|
+
return null;
|
|
1024
1072
|
}
|
|
1025
1073
|
}
|
|
1026
1074
|
|
|
1027
1075
|
// Substitute being broken nullifies the move's effect completely so even
|
|
1028
|
-
// if an effect was intended to "always happen" it will still get skipped
|
|
1076
|
+
// if an effect was intended to "always happen" it will still get skipped
|
|
1029
1077
|
if (nullified) return null;
|
|
1030
1078
|
|
|
1031
1079
|
// On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
|
|
1032
1080
|
// but this is only done to setup the MultiHit looping in the first place. Moving the MultiHit
|
|
1033
|
-
// setup before the loop means we can avoid having to waste time doing no-op handler searches
|
|
1081
|
+
// setup before the loop means we can avoid having to waste time doing no-op handler searches
|
|
1034
1082
|
if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
|
|
1035
1083
|
|
|
1036
1084
|
if (foe.stored().hp == 0) return null;
|
|
@@ -1045,7 +1093,7 @@ fn doMove(
|
|
|
1045
1093
|
// below). For Twineedle we change the data to that of one with PoisonChance1 given its
|
|
1046
1094
|
// MultiHit behavior is complete after the loop above, though Pokémon Showdown handles the
|
|
1047
1095
|
// Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
|
|
1048
|
-
// puts the |-status| message before |-hitcount| instead of after
|
|
1096
|
+
// puts the |-status| message before |-hitcount| instead of after
|
|
1049
1097
|
if (move.effect == .Twineedle) {
|
|
1050
1098
|
if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
1051
1099
|
} else if (move.effect == .Disable) {
|
|
@@ -1064,12 +1112,14 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
|
|
|
1064
1112
|
const side = battle.side(player);
|
|
1065
1113
|
|
|
1066
1114
|
// Base speed is used for the critical hit calculation, even when Transform-ed
|
|
1067
|
-
var rate =
|
|
1115
|
+
var rate: u16 = Species.chance(side.stored().species);
|
|
1068
1116
|
// GLITCH: Focus Energy reduces critical hit rate instead of increasing it
|
|
1069
1117
|
rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
|
|
1070
1118
|
rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
|
|
1071
1119
|
|
|
1072
|
-
|
|
1120
|
+
const spurious =
|
|
1121
|
+
!showdown and ((move.bp == 0 and move.effect != .OHKO) or move.target == .Depends);
|
|
1122
|
+
return Rolls.criticalHit(battle, player, @intCast(rate), spurious, options);
|
|
1073
1123
|
}
|
|
1074
1124
|
|
|
1075
1125
|
fn calcDamage(
|
|
@@ -1078,6 +1128,7 @@ fn calcDamage(
|
|
|
1078
1128
|
target_player: Player,
|
|
1079
1129
|
m: ?Move.Data,
|
|
1080
1130
|
crit: bool,
|
|
1131
|
+
options: anytype,
|
|
1081
1132
|
) bool {
|
|
1082
1133
|
// Confusion (indicated when m == null) just needs a 40 BP physical move
|
|
1083
1134
|
const cfz = m == null;
|
|
@@ -1089,28 +1140,8 @@ fn calcDamage(
|
|
|
1089
1140
|
|
|
1090
1141
|
const special = move.type.special();
|
|
1091
1142
|
|
|
1092
|
-
|
|
1093
|
-
var
|
|
1094
|
-
if (crit)
|
|
1095
|
-
if (special) side.stored().stats.spc
|
|
1096
|
-
else side.stored().stats.atk
|
|
1097
|
-
else
|
|
1098
|
-
if (special) side.active.stats.spc
|
|
1099
|
-
else side.active.stats.atk;
|
|
1100
|
-
var def: u32 =
|
|
1101
|
-
if (crit)
|
|
1102
|
-
if (special) target.stored().stats.spc
|
|
1103
|
-
else target.stored().stats.def
|
|
1104
|
-
else
|
|
1105
|
-
// GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
|
|
1106
|
-
if (special)
|
|
1107
|
-
target.active.stats.spc *
|
|
1108
|
-
@as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
|
|
1109
|
-
// Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
|
|
1110
|
-
else
|
|
1111
|
-
target.active.stats.def *
|
|
1112
|
-
@as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
|
|
1113
|
-
// zig fmt: on
|
|
1143
|
+
var atk = attack(side, crit, special);
|
|
1144
|
+
var def = defense(target, crit, special, cfz);
|
|
1114
1145
|
|
|
1115
1146
|
// Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
|
|
1116
1147
|
// will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
|
|
@@ -1118,12 +1149,12 @@ fn calcDamage(
|
|
|
1118
1149
|
if ((!showdown or !cfz) and (atk > 255 or def > 255)) {
|
|
1119
1150
|
atk = @max((atk / 4) & 255, 1);
|
|
1120
1151
|
// GLITCH: not adjusted to be a min of 1 on cartridge (can lead to division-by-zero freeze)
|
|
1121
|
-
def = @max((def / 4) & 255, if (
|
|
1152
|
+
def = @max((def / 4) & 255, if (pkmn.options.mod) 1 else 0);
|
|
1122
1153
|
}
|
|
1123
1154
|
|
|
1124
|
-
const lvl =
|
|
1155
|
+
const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
|
|
1125
1156
|
|
|
1126
|
-
def =
|
|
1157
|
+
def = if (move.effect == .Explode) @max(def / 2, 1) else def;
|
|
1127
1158
|
|
|
1128
1159
|
if (def == 0) return false;
|
|
1129
1160
|
|
|
@@ -1135,11 +1166,41 @@ fn calcDamage(
|
|
|
1135
1166
|
d = @min(997, d);
|
|
1136
1167
|
d += 2;
|
|
1137
1168
|
|
|
1169
|
+
// Whether or not a critical hit occurred can change whether a division-by-zero freeze is
|
|
1170
|
+
// encountered so we need to make sure that in the event of a miss we commit the status of the
|
|
1171
|
+
// crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
|
|
1172
|
+
// that results in counter intuitive probabilities in the common case
|
|
1173
|
+
if (pkmn.options.chance and !pkmn.options.mod and move.effect != .Explode) {
|
|
1174
|
+
atk = attack(side, !crit, special);
|
|
1175
|
+
def = defense(target, !crit, special, false);
|
|
1176
|
+
if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1138
1179
|
battle.last_damage = @intCast(d);
|
|
1139
1180
|
|
|
1140
1181
|
return true;
|
|
1141
1182
|
}
|
|
1142
1183
|
|
|
1184
|
+
fn attack(side: *Side, crit: bool, special: bool) u32 {
|
|
1185
|
+
return if (crit)
|
|
1186
|
+
if (special) side.stored().stats.spc else side.stored().stats.atk
|
|
1187
|
+
else if (special) side.active.stats.spc else side.active.stats.atk;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
fn defense(target: *Side, crit: bool, special: bool, cfz: bool) u32 {
|
|
1191
|
+
return if (crit)
|
|
1192
|
+
if (special) target.stored().stats.spc else target.stored().stats.def
|
|
1193
|
+
else
|
|
1194
|
+
// GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
|
|
1195
|
+
if (special)
|
|
1196
|
+
target.active.stats.spc *
|
|
1197
|
+
@as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
|
|
1198
|
+
else
|
|
1199
|
+
// Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
|
|
1200
|
+
target.active.stats.def *
|
|
1201
|
+
@as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1143
1204
|
fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
1144
1205
|
const side = battle.side(player);
|
|
1145
1206
|
const foe = battle.foe(player);
|
|
@@ -1184,7 +1245,6 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1184
1245
|
const foe = battle.foe(player);
|
|
1185
1246
|
|
|
1186
1247
|
if (!try checkHit(battle, player, move, options)) return null;
|
|
1187
|
-
try options.chance.commit(player, .hit);
|
|
1188
1248
|
|
|
1189
1249
|
battle.last_damage = switch (side.last_selected_move) {
|
|
1190
1250
|
.SuperFang => @max(foe.stored().hp / 2, 1),
|
|
@@ -1195,13 +1255,13 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1195
1255
|
.Psywave => power: {
|
|
1196
1256
|
const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
|
|
1197
1257
|
// GLITCH: Psywave infinite glitch loop
|
|
1198
|
-
if (!
|
|
1258
|
+
if (!pkmn.options.mod and max <= 1) return Result.Error;
|
|
1199
1259
|
break :power try Rolls.psywave(battle, player, max, options);
|
|
1200
1260
|
},
|
|
1201
1261
|
else => unreachable,
|
|
1202
1262
|
};
|
|
1203
1263
|
|
|
1204
|
-
if (battle.last_damage == 0) return if (
|
|
1264
|
+
if (battle.last_damage == 0) return if (pkmn.options.mod) null else Result.Error;
|
|
1205
1265
|
|
|
1206
1266
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1207
1267
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
@@ -1214,7 +1274,7 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1214
1274
|
const foe = battle.foe(player);
|
|
1215
1275
|
|
|
1216
1276
|
if (battle.last_damage == 0) {
|
|
1217
|
-
try options.log.fail(.{ battle.active(player),
|
|
1277
|
+
try options.log.fail(.{ battle.active(player), .None });
|
|
1218
1278
|
return null;
|
|
1219
1279
|
}
|
|
1220
1280
|
|
|
@@ -1232,22 +1292,25 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1232
1292
|
foe_last_selected_move.type == .Fighting);
|
|
1233
1293
|
|
|
1234
1294
|
if (!used and !selected) {
|
|
1235
|
-
try options.log.fail(.{ battle.active(player),
|
|
1295
|
+
try options.log.fail(.{ battle.active(player), .None });
|
|
1236
1296
|
return null;
|
|
1237
1297
|
}
|
|
1238
1298
|
|
|
1239
1299
|
if (!used or !selected) {
|
|
1240
1300
|
// GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
|
|
1241
|
-
if (!
|
|
1242
|
-
try options.log.fail(.{ battle.active(player),
|
|
1301
|
+
if (!pkmn.options.mod) return Result.Error;
|
|
1302
|
+
try options.log.fail(.{ battle.active(player), .None });
|
|
1243
1303
|
return null;
|
|
1244
1304
|
}
|
|
1245
1305
|
|
|
1246
|
-
|
|
1306
|
+
// NOTE: llvm/llvm-project#58557
|
|
1307
|
+
battle.last_damage = if (comptime builtin.target.isWasm())
|
|
1308
|
+
@max(battle.last_damage *% 2, std.math.maxInt(u16))
|
|
1309
|
+
else
|
|
1310
|
+
battle.last_damage *| 2;
|
|
1247
1311
|
|
|
1248
|
-
// Pokémon Showdown calls
|
|
1312
|
+
// Pokémon Showdown calls moveHit before Counter
|
|
1249
1313
|
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
1250
|
-
try options.chance.commit(player, .hit);
|
|
1251
1314
|
|
|
1252
1315
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1253
1316
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
@@ -1275,13 +1338,13 @@ fn applyDamage(
|
|
|
1275
1338
|
subbed.active.volatiles.substitute = 0;
|
|
1276
1339
|
subbed.active.volatiles.Substitute = false;
|
|
1277
1340
|
// battle.last_damage is not updated with the amount of HP the Substitute had
|
|
1278
|
-
try options.log.end(.{ battle.active(sub_player),
|
|
1279
|
-
options.calc.capped(
|
|
1341
|
+
try options.log.end(.{ battle.active(sub_player), .Substitute });
|
|
1342
|
+
if (reason != .Confusion) options.calc.capped(sub_player);
|
|
1280
1343
|
return true;
|
|
1281
1344
|
} else {
|
|
1282
1345
|
// Safe to truncate since less than subbed.volatiles.substitute which is a u8
|
|
1283
1346
|
subbed.active.volatiles.substitute -= @intCast(battle.last_damage);
|
|
1284
|
-
try options.log.activate(battle.active(sub_player), .Substitute);
|
|
1347
|
+
try options.log.activate(.{ battle.active(sub_player), .Substitute });
|
|
1285
1348
|
return false;
|
|
1286
1349
|
}
|
|
1287
1350
|
}
|
|
@@ -1307,10 +1370,10 @@ fn mirrorMove(
|
|
|
1307
1370
|
side.last_selected_move = foe.last_used_move;
|
|
1308
1371
|
|
|
1309
1372
|
if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
|
|
1310
|
-
try options.log.fail(.{ battle.active(player),
|
|
1373
|
+
try options.log.fail(.{ battle.active(player), .None });
|
|
1311
1374
|
return null;
|
|
1312
1375
|
} else if (!showdown or foe.last_used_move != .Struggle) {
|
|
1313
|
-
incrementPP(side, mslot);
|
|
1376
|
+
incrementPP(side, player, mslot, options);
|
|
1314
1377
|
}
|
|
1315
1378
|
|
|
1316
1379
|
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
|
|
@@ -1331,7 +1394,7 @@ fn metronome(
|
|
|
1331
1394
|
var side = battle.side(player);
|
|
1332
1395
|
|
|
1333
1396
|
side.last_selected_move = try Rolls.metronome(battle, player, options);
|
|
1334
|
-
incrementPP(side, mslot);
|
|
1397
|
+
incrementPP(side, player, mslot, options);
|
|
1335
1398
|
|
|
1336
1399
|
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
|
|
1337
1400
|
return null;
|
|
@@ -1343,20 +1406,23 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
|
|
|
1343
1406
|
var immune = false;
|
|
1344
1407
|
var mist = false;
|
|
1345
1408
|
|
|
1346
|
-
if (moveHit(battle, player, move, &immune, &mist, options))
|
|
1409
|
+
if (try moveHit(battle, player, move, &immune, &mist, options)) {
|
|
1410
|
+
if (!showdown) try options.chance.commit(player, .hit);
|
|
1411
|
+
return true;
|
|
1412
|
+
}
|
|
1347
1413
|
|
|
1348
1414
|
assert(!immune);
|
|
1349
1415
|
if (mist) {
|
|
1350
1416
|
assert(!showdown);
|
|
1351
1417
|
const foe_ident = battle.active(player.foe());
|
|
1352
|
-
try options.log.activate(foe_ident, .Mist);
|
|
1353
|
-
try options.log.fail(.{ foe_ident,
|
|
1418
|
+
try options.log.activate(.{ foe_ident, .Mist });
|
|
1419
|
+
try options.log.fail(.{ foe_ident, .None });
|
|
1354
1420
|
} else {
|
|
1355
|
-
try options.chance.commit(player, .miss);
|
|
1356
1421
|
try options.log.lastmiss(.{});
|
|
1357
1422
|
try options.log.miss(.{battle.active(player)});
|
|
1358
1423
|
}
|
|
1359
1424
|
|
|
1425
|
+
if (!showdown) try options.chance.commit(player, .miss);
|
|
1360
1426
|
return false;
|
|
1361
1427
|
}
|
|
1362
1428
|
|
|
@@ -1367,7 +1433,7 @@ fn moveHit(
|
|
|
1367
1433
|
immune: *bool,
|
|
1368
1434
|
mist: *bool,
|
|
1369
1435
|
options: anytype,
|
|
1370
|
-
) bool {
|
|
1436
|
+
) !bool {
|
|
1371
1437
|
var side = battle.side(player);
|
|
1372
1438
|
const foe = battle.foe(player);
|
|
1373
1439
|
|
|
@@ -1406,7 +1472,7 @@ fn moveHit(
|
|
|
1406
1472
|
const overwritten = overwrite and state > 0;
|
|
1407
1473
|
assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
|
|
1408
1474
|
|
|
1409
|
-
var accuracy = if (overwritten) state else
|
|
1475
|
+
var accuracy: u16 = if (overwritten) state else move.accuracy;
|
|
1410
1476
|
var boost = BOOSTS[@as(u4, @intCast(@as(i8, side.active.boosts.accuracy) + 6))];
|
|
1411
1477
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1412
1478
|
boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
|
|
@@ -1418,7 +1484,7 @@ fn moveHit(
|
|
|
1418
1484
|
if (overwrite) side.active.volatiles.state = accuracy;
|
|
1419
1485
|
|
|
1420
1486
|
// GLITCH: max accuracy is 255 so 1/256 chance of miss
|
|
1421
|
-
break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
|
|
1487
|
+
break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
|
|
1422
1488
|
};
|
|
1423
1489
|
|
|
1424
1490
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1429,13 +1495,17 @@ fn moveHit(
|
|
|
1429
1495
|
|
|
1430
1496
|
if (!miss) return true;
|
|
1431
1497
|
if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
|
|
1498
|
+
|
|
1432
1499
|
side.active.volatiles.Binding = false;
|
|
1500
|
+
options.chance.observe(.binding, player, .None);
|
|
1501
|
+
|
|
1433
1502
|
return false;
|
|
1434
1503
|
}
|
|
1435
1504
|
|
|
1436
1505
|
fn checkFaint(
|
|
1437
1506
|
battle: anytype,
|
|
1438
1507
|
player: Player,
|
|
1508
|
+
cap: bool,
|
|
1439
1509
|
options: anytype,
|
|
1440
1510
|
) @TypeOf(options.log).Error!?Result {
|
|
1441
1511
|
const side = battle.side(player);
|
|
@@ -1448,8 +1518,8 @@ fn checkFaint(
|
|
|
1448
1518
|
const foe_out = foe_fainted and findFirstAlive(foe) == 0;
|
|
1449
1519
|
const more = player_out or foe_out;
|
|
1450
1520
|
|
|
1451
|
-
if (try faint(battle, player, !(more or foe_fainted), options)) |r| return r;
|
|
1452
|
-
if (foe_fainted) if (try faint(battle, player.foe(), !more, options)) |r| return r;
|
|
1521
|
+
if (try faint(battle, player, !(more or foe_fainted), cap, options)) |r| return r;
|
|
1522
|
+
if (foe_fainted) if (try faint(battle, player.foe(), !more, false, options)) |r| return r;
|
|
1453
1523
|
|
|
1454
1524
|
assert(!side.active.volatiles.MultiHit);
|
|
1455
1525
|
assert(!foe.active.volatiles.MultiHit);
|
|
@@ -1470,7 +1540,7 @@ fn checkFaint(
|
|
|
1470
1540
|
return .{ .p1 = foe_choice, .p2 = .Switch };
|
|
1471
1541
|
}
|
|
1472
1542
|
|
|
1473
|
-
fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
|
|
1543
|
+
fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytype) !?Result {
|
|
1474
1544
|
var side = battle.side(player);
|
|
1475
1545
|
var foe = battle.foe(player);
|
|
1476
1546
|
assert(side.stored().hp == 0);
|
|
@@ -1479,7 +1549,7 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
|
|
|
1479
1549
|
assert(!foe_volatiles.MultiHit);
|
|
1480
1550
|
if (foe_volatiles.Bide) {
|
|
1481
1551
|
assert(!foe_volatiles.Thrashing and !foe_volatiles.Rage);
|
|
1482
|
-
foe_volatiles.state = if (
|
|
1552
|
+
foe_volatiles.state = if (pkmn.options.mod) 0 else foe_volatiles.state & 255;
|
|
1483
1553
|
if (foe_volatiles.state != 0) return Result.Error;
|
|
1484
1554
|
}
|
|
1485
1555
|
|
|
@@ -1504,7 +1574,9 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
|
|
|
1504
1574
|
}
|
|
1505
1575
|
|
|
1506
1576
|
try options.log.faint(.{ battle.active(player), done });
|
|
1507
|
-
|
|
1577
|
+
// We can only safely set capped if the faint occured as the direct result of an opponent's
|
|
1578
|
+
// move (as opposed to due to residual damage, recoil, Explosion, etc)
|
|
1579
|
+
if (cap) options.calc.capped(player);
|
|
1508
1580
|
return null;
|
|
1509
1581
|
}
|
|
1510
1582
|
|
|
@@ -1552,13 +1624,13 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
|
|
|
1552
1624
|
stored.hp -= amount;
|
|
1553
1625
|
|
|
1554
1626
|
// As above, Pokémon Showdown uses damage [of] but its not relevant
|
|
1555
|
-
if (amount > 0) try options.log.damage(.{ ident, stored,
|
|
1627
|
+
if (amount > 0) try options.log.damage(.{ ident, stored, .LeechSeed });
|
|
1556
1628
|
|
|
1557
1629
|
const before = foe_stored.hp;
|
|
1558
1630
|
// Uncapped damage is added back to the foe
|
|
1559
1631
|
foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
|
|
1560
1632
|
// Pokémon Showdown uses the less specific heal here instead of drain... because reasons?
|
|
1561
|
-
if (foe_stored.hp > before) try options.log.heal(.{ foe_ident, foe_stored,
|
|
1633
|
+
if (foe_stored.hp > before) try options.log.heal(.{ foe_ident, foe_stored, .Silent });
|
|
1562
1634
|
}
|
|
1563
1635
|
}
|
|
1564
1636
|
|
|
@@ -1568,12 +1640,12 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
|
|
|
1568
1640
|
|
|
1569
1641
|
battle.turn += 1;
|
|
1570
1642
|
|
|
1571
|
-
if (
|
|
1643
|
+
if (pkmn.options.mod and pkmn.options.ebc and checkEBC(battle)) {
|
|
1572
1644
|
try options.log.tie(.{});
|
|
1573
1645
|
return Result.Tie;
|
|
1574
1646
|
}
|
|
1575
1647
|
|
|
1576
|
-
if (
|
|
1648
|
+
if (pkmn.options.mod and battle.turn >= 1000) {
|
|
1577
1649
|
try options.log.tie(.{});
|
|
1578
1650
|
return Result.Tie;
|
|
1579
1651
|
} else if (battle.turn >= 65535) {
|
|
@@ -1650,7 +1722,7 @@ fn disabled(side: *Side, ident: ID, options: anytype) !bool {
|
|
|
1650
1722
|
side.last_selected_move;
|
|
1651
1723
|
if (m != .None and m == last) {
|
|
1652
1724
|
side.active.volatiles.Charging = false;
|
|
1653
|
-
try options.log.cant(.{ ident,
|
|
1725
|
+
try options.log.cant(.{ ident, .Disable, last });
|
|
1654
1726
|
return true;
|
|
1655
1727
|
}
|
|
1656
1728
|
}
|
|
@@ -1664,17 +1736,6 @@ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
|
|
|
1664
1736
|
}
|
|
1665
1737
|
}
|
|
1666
1738
|
|
|
1667
|
-
fn handleThrashing(battle: anytype, active: *ActivePokemon, player: Player, options: anytype) bool {
|
|
1668
|
-
var volatiles = &active.volatiles;
|
|
1669
|
-
assert(volatiles.Thrashing);
|
|
1670
|
-
if (volatiles.attacks > 0) return false;
|
|
1671
|
-
|
|
1672
|
-
volatiles.Thrashing = false;
|
|
1673
|
-
volatiles.Confusion = true;
|
|
1674
|
-
volatiles.confusion = Rolls.confusionDuration(battle, player, true, options);
|
|
1675
|
-
return true;
|
|
1676
|
-
}
|
|
1677
|
-
|
|
1678
1739
|
fn onBegin(
|
|
1679
1740
|
battle: anytype,
|
|
1680
1741
|
player: Player,
|
|
@@ -1683,7 +1744,6 @@ fn onBegin(
|
|
|
1683
1744
|
residual: *bool,
|
|
1684
1745
|
options: anytype,
|
|
1685
1746
|
) !void {
|
|
1686
|
-
assert(move.effect.onBegin());
|
|
1687
1747
|
return switch (move.effect) {
|
|
1688
1748
|
.Confusion => Effects.confusion(battle, player, move, options),
|
|
1689
1749
|
.Conversion => Effects.conversion(battle, player, options),
|
|
@@ -1706,7 +1766,6 @@ fn onBegin(
|
|
|
1706
1766
|
}
|
|
1707
1767
|
|
|
1708
1768
|
fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1709
|
-
assert(move.effect.onEnd());
|
|
1710
1769
|
return switch (move.effect) {
|
|
1711
1770
|
.Bide => Effects.bide(battle, player, options),
|
|
1712
1771
|
// zig fmt: off
|
|
@@ -1728,7 +1787,6 @@ fn alwaysHappens(
|
|
|
1728
1787
|
residual: *bool,
|
|
1729
1788
|
options: anytype,
|
|
1730
1789
|
) !void {
|
|
1731
|
-
assert(move.effect.alwaysHappens());
|
|
1732
1790
|
return switch (move.effect) {
|
|
1733
1791
|
.DrainHP, .DreamEater => Effects.drainHP(battle, player, options),
|
|
1734
1792
|
.Explode => Effects.explode(battle, player),
|
|
@@ -1764,10 +1822,12 @@ pub const Effects = struct {
|
|
|
1764
1822
|
|
|
1765
1823
|
side.active.volatiles.Bide = true;
|
|
1766
1824
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1825
|
+
|
|
1767
1826
|
side.active.volatiles.state = 0;
|
|
1768
1827
|
side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
1828
|
+
options.chance.observe(.attacking, player, .started);
|
|
1769
1829
|
|
|
1770
|
-
try options.log.start(battle.active(player), .Bide);
|
|
1830
|
+
try options.log.start(.{ battle.active(player), .Bide });
|
|
1771
1831
|
}
|
|
1772
1832
|
|
|
1773
1833
|
fn burnChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -1784,7 +1844,7 @@ pub const Effects = struct {
|
|
|
1784
1844
|
try options.log.curestatus(.{
|
|
1785
1845
|
battle.active(player.foe()),
|
|
1786
1846
|
foe_stored.status,
|
|
1787
|
-
|
|
1847
|
+
.Message,
|
|
1788
1848
|
});
|
|
1789
1849
|
foe_stored.status = 0;
|
|
1790
1850
|
}
|
|
@@ -1799,8 +1859,7 @@ pub const Effects = struct {
|
|
|
1799
1859
|
foe_stored.status = Status.init(.BRN);
|
|
1800
1860
|
foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
|
|
1801
1861
|
|
|
1802
|
-
|
|
1803
|
-
try options.log.status(.{ battle.active(player.foe()), foe_stored.status, reason });
|
|
1862
|
+
try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
|
|
1804
1863
|
}
|
|
1805
1864
|
|
|
1806
1865
|
fn charge(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -1822,26 +1881,22 @@ pub const Effects = struct {
|
|
|
1822
1881
|
if (!try Rolls.confusionChance(battle, player, options)) return;
|
|
1823
1882
|
} else {
|
|
1824
1883
|
if (showdown) {
|
|
1825
|
-
if (!try checkHit(battle, player, move, options))
|
|
1826
|
-
|
|
1827
|
-
} else if (sub) {
|
|
1828
|
-
return options.log.fail(.{ battle.active(player.foe()), Fail.None });
|
|
1829
|
-
}
|
|
1884
|
+
if (!try checkHit(battle, player, move, options)) return;
|
|
1885
|
+
if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
1830
1886
|
} else {
|
|
1831
|
-
if (sub) {
|
|
1832
|
-
|
|
1833
|
-
} else if (!try checkHit(battle, player, move, options)) {
|
|
1834
|
-
return;
|
|
1835
|
-
}
|
|
1887
|
+
if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
1888
|
+
if (!try checkHit(battle, player, move, options)) return;
|
|
1836
1889
|
}
|
|
1837
|
-
try options.chance.commit(player, .hit);
|
|
1838
1890
|
}
|
|
1839
1891
|
|
|
1840
1892
|
if (foe.active.volatiles.Confusion) return;
|
|
1841
1893
|
foe.active.volatiles.Confusion = true;
|
|
1842
|
-
foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
|
|
1843
1894
|
|
|
1844
|
-
|
|
1895
|
+
foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
1896
|
+
options.chance.observe(.confusion, player.foe(), .started);
|
|
1897
|
+
options.calc.confusion(player.foe());
|
|
1898
|
+
|
|
1899
|
+
try options.log.start(.{ battle.active(player.foe()), .Confusion });
|
|
1845
1900
|
}
|
|
1846
1901
|
|
|
1847
1902
|
fn conversion(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -1853,11 +1908,12 @@ pub const Effects = struct {
|
|
|
1853
1908
|
}
|
|
1854
1909
|
|
|
1855
1910
|
battle.side(player).active.types = foe.active.types;
|
|
1856
|
-
return options.log.
|
|
1911
|
+
return options.log.start(.{
|
|
1857
1912
|
battle.active(player),
|
|
1913
|
+
.TypeChange,
|
|
1858
1914
|
foe.active.types,
|
|
1859
1915
|
battle.active(player.foe()),
|
|
1860
|
-
);
|
|
1916
|
+
});
|
|
1861
1917
|
}
|
|
1862
1918
|
|
|
1863
1919
|
fn disable(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
|
|
@@ -1866,13 +1922,10 @@ pub const Effects = struct {
|
|
|
1866
1922
|
const foe_ident = battle.active(player.foe());
|
|
1867
1923
|
|
|
1868
1924
|
// Pokémon Showdown handles hit/miss earlier in doMove
|
|
1869
|
-
if (!showdown)
|
|
1870
|
-
if (!try checkHit(battle, player, move, options)) return null;
|
|
1871
|
-
try options.chance.commit(player, .hit);
|
|
1872
|
-
}
|
|
1925
|
+
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
1873
1926
|
|
|
1874
1927
|
if (volatiles.disable_move != 0) {
|
|
1875
|
-
try options.log.fail(.{ foe_ident,
|
|
1928
|
+
try options.log.fail(.{ foe_ident, .None });
|
|
1876
1929
|
return null;
|
|
1877
1930
|
}
|
|
1878
1931
|
|
|
@@ -1887,21 +1940,21 @@ pub const Effects = struct {
|
|
|
1887
1940
|
|
|
1888
1941
|
// Technically this is still considered simply a "miss" on the cartridge,
|
|
1889
1942
|
// but diverging from Pokémon Showdown here would mostly just be pedantic
|
|
1890
|
-
if (n == 0) {
|
|
1891
|
-
try options.log.fail(.{ foe_ident,
|
|
1943
|
+
if (n == 0 or (pkmn.options.mod and err)) {
|
|
1944
|
+
try options.log.fail(.{ foe_ident, .None });
|
|
1892
1945
|
return null;
|
|
1893
1946
|
} else if (err) {
|
|
1894
1947
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
1895
|
-
assert(!showdown);
|
|
1896
1948
|
return Result.Error;
|
|
1897
1949
|
}
|
|
1898
1950
|
|
|
1899
1951
|
volatiles.disable_move =
|
|
1900
1952
|
@intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
|
|
1901
1953
|
volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
|
|
1954
|
+
options.chance.observe(.disable, player.foe(), .started);
|
|
1902
1955
|
|
|
1903
1956
|
const id = foe.active.move(volatiles.disable_move).id;
|
|
1904
|
-
try options.log.
|
|
1957
|
+
try options.log.start(.{ foe_ident, .Disable, id });
|
|
1905
1958
|
return null;
|
|
1906
1959
|
}
|
|
1907
1960
|
|
|
@@ -1920,7 +1973,7 @@ pub const Effects = struct {
|
|
|
1920
1973
|
if (stored.hp == stored.stats.hp) return;
|
|
1921
1974
|
stored.hp = @min(stored.stats.hp, stored.hp + drain);
|
|
1922
1975
|
|
|
1923
|
-
try options.log.heal(.{ ident, stored,
|
|
1976
|
+
try options.log.heal(.{ ident, stored, .Drain, battle.active(player.foe()) });
|
|
1924
1977
|
}
|
|
1925
1978
|
|
|
1926
1979
|
fn explode(battle: anytype, player: Player) !void {
|
|
@@ -1952,7 +2005,7 @@ pub const Effects = struct {
|
|
|
1952
2005
|
if (side.active.volatiles.FocusEnergy) return;
|
|
1953
2006
|
side.active.volatiles.FocusEnergy = true;
|
|
1954
2007
|
|
|
1955
|
-
try options.log.start(battle.active(player), .FocusEnergy);
|
|
2008
|
+
try options.log.start(.{ battle.active(player), .FocusEnergy });
|
|
1956
2009
|
}
|
|
1957
2010
|
|
|
1958
2011
|
fn freezeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -1969,12 +2022,12 @@ pub const Effects = struct {
|
|
|
1969
2022
|
if (foe.active.types.includes(move.type)) return;
|
|
1970
2023
|
if (!try Rolls.secondaryChance(battle, player, true, options)) return;
|
|
1971
2024
|
// Freeze Clause Mod
|
|
1972
|
-
if (
|
|
2025
|
+
if (pkmn.options.mod) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
1973
2026
|
|
|
1974
2027
|
foe_stored.status = Status.init(.FRZ);
|
|
1975
2028
|
// GLITCH: Hyper Beam recharging status is not cleared
|
|
1976
2029
|
|
|
1977
|
-
try options.log.status(.{ foe_ident, foe_stored.status,
|
|
2030
|
+
try options.log.status(.{ foe_ident, foe_stored.status, .None });
|
|
1978
2031
|
}
|
|
1979
2032
|
|
|
1980
2033
|
fn haze(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -1982,7 +2035,7 @@ pub const Effects = struct {
|
|
|
1982
2035
|
var side = battle.side(player);
|
|
1983
2036
|
var foe = battle.foe(player);
|
|
1984
2037
|
|
|
1985
|
-
|
|
2038
|
+
const side_stored = side.stored();
|
|
1986
2039
|
var foe_stored = foe.stored();
|
|
1987
2040
|
|
|
1988
2041
|
const player_ident = battle.active(player);
|
|
@@ -1994,7 +2047,7 @@ pub const Effects = struct {
|
|
|
1994
2047
|
side.active.stats = side_stored.stats;
|
|
1995
2048
|
foe.active.stats = foe_stored.stats;
|
|
1996
2049
|
|
|
1997
|
-
try log.activate(player_ident, .Haze);
|
|
2050
|
+
try log.activate(.{ player_ident, .Haze });
|
|
1998
2051
|
try log.clearallboost(.{});
|
|
1999
2052
|
|
|
2000
2053
|
// Pokémon Showdown clears P1 then P2 instead of status -> side -> foe
|
|
@@ -2003,24 +2056,26 @@ pub const Effects = struct {
|
|
|
2003
2056
|
const p: Player = @enumFromInt(i);
|
|
2004
2057
|
// Pokémon Showdown incorrectly does not prevent sleep/freeze from moving
|
|
2005
2058
|
if (p != player and Status.any(s.stored().status)) {
|
|
2006
|
-
try log.curestatus(.{ foe_ident, foe_stored.status,
|
|
2059
|
+
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
2007
2060
|
s.stored().status = 0;
|
|
2061
|
+
options.chance.observe(.sleep, p, .None);
|
|
2008
2062
|
} else if (showdown and s.stored().status == Status.TOX) {
|
|
2009
2063
|
s.stored().status = Status.init(.PSN);
|
|
2010
|
-
try log.status(.{ battle.active(p), s.stored().status,
|
|
2064
|
+
try log.status(.{ battle.active(p), s.stored().status, .None });
|
|
2011
2065
|
}
|
|
2012
|
-
try clearVolatiles(battle, p,
|
|
2066
|
+
try clearVolatiles(battle, p, options);
|
|
2013
2067
|
}
|
|
2014
2068
|
} else {
|
|
2015
2069
|
if (Status.any(foe_stored.status)) {
|
|
2016
2070
|
if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
|
|
2017
2071
|
foe.last_selected_move = .SKIP_TURN;
|
|
2072
|
+
options.chance.observe(.sleep, player.foe(), .None);
|
|
2018
2073
|
}
|
|
2019
|
-
try log.curestatus(.{ foe_ident, foe_stored.status,
|
|
2074
|
+
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
2020
2075
|
foe_stored.status = 0;
|
|
2021
2076
|
}
|
|
2022
|
-
try clearVolatiles(battle, player,
|
|
2023
|
-
try clearVolatiles(battle, player.foe(),
|
|
2077
|
+
try clearVolatiles(battle, player, options);
|
|
2078
|
+
try clearVolatiles(battle, player.foe(), options);
|
|
2024
2079
|
}
|
|
2025
2080
|
}
|
|
2026
2081
|
|
|
@@ -2032,7 +2087,7 @@ pub const Effects = struct {
|
|
|
2032
2087
|
// GLITCH: HP recovery move failure glitches
|
|
2033
2088
|
const delta = stored.stats.hp - stored.hp;
|
|
2034
2089
|
if (delta == 0 or (delta & 255 == 255 and stored.hp % 256 != 0)) {
|
|
2035
|
-
return options.log.fail(.{ ident,
|
|
2090
|
+
return options.log.fail(.{ ident, .None });
|
|
2036
2091
|
}
|
|
2037
2092
|
|
|
2038
2093
|
const rest = side.last_selected_move == .Rest;
|
|
@@ -2040,7 +2095,7 @@ pub const Effects = struct {
|
|
|
2040
2095
|
// Adding the sleep status runs the sleep condition handler to roll duration
|
|
2041
2096
|
if (showdown) battle.rng.advance(1);
|
|
2042
2097
|
stored.status = Status.slf(2);
|
|
2043
|
-
try options.log.status(.{ ident, stored.status,
|
|
2098
|
+
try options.log.status(.{ ident, stored.status, .From, Move.Rest });
|
|
2044
2099
|
stored.hp = stored.stats.hp;
|
|
2045
2100
|
} else {
|
|
2046
2101
|
stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
|
|
@@ -2059,7 +2114,7 @@ pub const Effects = struct {
|
|
|
2059
2114
|
if (showdown) {
|
|
2060
2115
|
// Invulnerability trumps type immunity on Pokémon Showdown
|
|
2061
2116
|
if (!foe.active.volatiles.Invulnerable and foe.active.types.includes(.Grass)) {
|
|
2062
|
-
return options.log.immune(.{ battle.active(player.foe()),
|
|
2117
|
+
return options.log.immune(.{ battle.active(player.foe()), .None });
|
|
2063
2118
|
}
|
|
2064
2119
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2065
2120
|
if (foe.active.volatiles.LeechSeed) return;
|
|
@@ -2071,21 +2126,19 @@ pub const Effects = struct {
|
|
|
2071
2126
|
}
|
|
2072
2127
|
}
|
|
2073
2128
|
|
|
2074
|
-
try options.chance.commit(player, .hit);
|
|
2075
2129
|
foe.active.volatiles.LeechSeed = true;
|
|
2076
2130
|
|
|
2077
|
-
try options.log.start(battle.active(player.foe()), .LeechSeed);
|
|
2131
|
+
try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
|
|
2078
2132
|
}
|
|
2079
2133
|
|
|
2080
2134
|
fn lightScreen(battle: anytype, player: Player, options: anytype) !void {
|
|
2081
2135
|
var side = battle.side(player);
|
|
2136
|
+
const ident = battle.active(player);
|
|
2082
2137
|
|
|
2083
|
-
if (side.active.volatiles.LightScreen) {
|
|
2084
|
-
return options.log.fail(.{ battle.active(player), Fail.None });
|
|
2085
|
-
}
|
|
2138
|
+
if (side.active.volatiles.LightScreen) return options.log.fail(.{ ident, .None });
|
|
2086
2139
|
side.active.volatiles.LightScreen = true;
|
|
2087
2140
|
|
|
2088
|
-
try options.log.start(
|
|
2141
|
+
try options.log.start(.{ ident, .LightScreen });
|
|
2089
2142
|
}
|
|
2090
2143
|
|
|
2091
2144
|
fn mimic(battle: anytype, player: Player, move: Move.Data, mslot: u8, options: anytype) !void {
|
|
@@ -2113,17 +2166,15 @@ pub const Effects = struct {
|
|
|
2113
2166
|
if (!has_mimic) {
|
|
2114
2167
|
// Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
|
|
2115
2168
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2116
|
-
|
|
2117
|
-
return options.log.fail(.{ battle.active(player.foe()), Fail.None });
|
|
2169
|
+
return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
2118
2170
|
}
|
|
2119
2171
|
}
|
|
2120
2172
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2121
|
-
try options.chance.commit(player, .hit);
|
|
2122
2173
|
|
|
2123
2174
|
const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
|
|
2124
2175
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
2125
2176
|
|
|
2126
|
-
try options.log.
|
|
2177
|
+
try options.log.start(.{ battle.active(player), .Mimic, side.active.move(oslot).id });
|
|
2127
2178
|
}
|
|
2128
2179
|
|
|
2129
2180
|
fn mist(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -2132,7 +2183,7 @@ pub const Effects = struct {
|
|
|
2132
2183
|
if (side.active.volatiles.Mist) return;
|
|
2133
2184
|
side.active.volatiles.Mist = true;
|
|
2134
2185
|
|
|
2135
|
-
try options.log.start(battle.active(player), .Mist);
|
|
2186
|
+
try options.log.start(.{ battle.active(player), .Mist });
|
|
2136
2187
|
}
|
|
2137
2188
|
|
|
2138
2189
|
fn multiHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2159,7 +2210,7 @@ pub const Effects = struct {
|
|
|
2159
2210
|
if (showdown) {
|
|
2160
2211
|
// Invulnerability trumps type immunity on Pokémon Showdown
|
|
2161
2212
|
if (immune and !foe.active.volatiles.Invulnerable) {
|
|
2162
|
-
return log.immune(.{ foe_ident,
|
|
2213
|
+
return log.immune(.{ foe_ident, .None });
|
|
2163
2214
|
} else if (!try checkHit(battle, player, move, options)) return;
|
|
2164
2215
|
}
|
|
2165
2216
|
if (Status.any(foe_stored.status)) {
|
|
@@ -2169,15 +2220,14 @@ pub const Effects = struct {
|
|
|
2169
2220
|
});
|
|
2170
2221
|
}
|
|
2171
2222
|
if (!showdown) {
|
|
2172
|
-
if (immune) return log.immune(.{ foe_ident,
|
|
2223
|
+
if (immune) return log.immune(.{ foe_ident, .None });
|
|
2173
2224
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2174
2225
|
}
|
|
2175
|
-
try options.chance.commit(player, .hit);
|
|
2176
2226
|
|
|
2177
2227
|
foe_stored.status = Status.init(.PAR);
|
|
2178
2228
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
2179
2229
|
|
|
2180
|
-
try log.status(.{ foe_ident, foe_stored.status,
|
|
2230
|
+
try log.status(.{ foe_ident, foe_stored.status, .None });
|
|
2181
2231
|
}
|
|
2182
2232
|
|
|
2183
2233
|
fn paralyzeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2199,8 +2249,7 @@ pub const Effects = struct {
|
|
|
2199
2249
|
foe_stored.status = Status.init(.PAR);
|
|
2200
2250
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
2201
2251
|
|
|
2202
|
-
|
|
2203
|
-
try options.log.status(.{ battle.active(player.foe()), foe_stored.status, reason });
|
|
2252
|
+
try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
|
|
2204
2253
|
}
|
|
2205
2254
|
|
|
2206
2255
|
fn payDay(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -2220,7 +2269,7 @@ pub const Effects = struct {
|
|
|
2220
2269
|
return;
|
|
2221
2270
|
} else if (foe.active.volatiles.Substitute) {
|
|
2222
2271
|
if (move.effect != .Poison) return;
|
|
2223
|
-
return log.fail(.{ foe_ident,
|
|
2272
|
+
return log.fail(.{ foe_ident, .None });
|
|
2224
2273
|
} else if (Status.any(foe_stored.status)) {
|
|
2225
2274
|
if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
|
|
2226
2275
|
// Pokémon Showdown considers Toxic to be a status even in Generation I and so
|
|
@@ -2236,12 +2285,11 @@ pub const Effects = struct {
|
|
|
2236
2285
|
Fail.None });
|
|
2237
2286
|
} else if (foe.active.types.includes(.Poison)) {
|
|
2238
2287
|
if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
|
|
2239
|
-
return log.immune(.{ foe_ident,
|
|
2288
|
+
return log.immune(.{ foe_ident, .None });
|
|
2240
2289
|
}
|
|
2241
2290
|
|
|
2242
2291
|
if (move.effect == .Poison) {
|
|
2243
2292
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2244
|
-
try options.chance.commit(player, .hit);
|
|
2245
2293
|
} else {
|
|
2246
2294
|
if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
|
|
2247
2295
|
return;
|
|
@@ -2255,7 +2303,7 @@ pub const Effects = struct {
|
|
|
2255
2303
|
foe.active.volatiles.toxic = 0;
|
|
2256
2304
|
}
|
|
2257
2305
|
|
|
2258
|
-
try log.status(.{ foe_ident, foe_stored.status,
|
|
2306
|
+
try log.status(.{ foe_ident, foe_stored.status, .None });
|
|
2259
2307
|
}
|
|
2260
2308
|
|
|
2261
2309
|
fn rage(battle: anytype, player: Player) !void {
|
|
@@ -2278,7 +2326,7 @@ pub const Effects = struct {
|
|
|
2278
2326
|
try options.log.damage(.{
|
|
2279
2327
|
battle.active(player),
|
|
2280
2328
|
stored,
|
|
2281
|
-
|
|
2329
|
+
.RecoilOf,
|
|
2282
2330
|
battle.active(player.foe()),
|
|
2283
2331
|
});
|
|
2284
2332
|
if (showdown and stored.hp == 0) residual.* = false;
|
|
@@ -2286,13 +2334,12 @@ pub const Effects = struct {
|
|
|
2286
2334
|
|
|
2287
2335
|
fn reflect(battle: anytype, player: Player, options: anytype) !void {
|
|
2288
2336
|
var side = battle.side(player);
|
|
2337
|
+
const ident = battle.active(player);
|
|
2289
2338
|
|
|
2290
|
-
if (side.active.volatiles.Reflect) {
|
|
2291
|
-
return options.log.fail(.{ battle.active(player), Fail.None });
|
|
2292
|
-
}
|
|
2339
|
+
if (side.active.volatiles.Reflect) return options.log.fail(.{ ident, .None });
|
|
2293
2340
|
side.active.volatiles.Reflect = true;
|
|
2294
2341
|
|
|
2295
|
-
try options.log.start(
|
|
2342
|
+
try options.log.start(.{ ident, .Reflect });
|
|
2296
2343
|
}
|
|
2297
2344
|
|
|
2298
2345
|
fn sleep(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2311,13 +2358,12 @@ pub const Effects = struct {
|
|
|
2311
2358
|
if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
|
|
2312
2359
|
});
|
|
2313
2360
|
}
|
|
2314
|
-
// If
|
|
2361
|
+
// If moveHit in doMove didn't return true Pokémon Showdown wouldn't be in here
|
|
2315
2362
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2316
|
-
try options.chance.commit(player, .hit);
|
|
2317
2363
|
}
|
|
2318
2364
|
|
|
2319
2365
|
// Sleep Clause Mod
|
|
2320
|
-
if (
|
|
2366
|
+
if (pkmn.options.mod) {
|
|
2321
2367
|
for (foe.pokemon) |p| {
|
|
2322
2368
|
if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
|
|
2323
2369
|
}
|
|
@@ -2325,18 +2371,20 @@ pub const Effects = struct {
|
|
|
2325
2371
|
foe.active.volatiles.Recharging = false;
|
|
2326
2372
|
|
|
2327
2373
|
foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
|
|
2374
|
+
options.chance.observe(.sleep, player.foe(), .started);
|
|
2375
|
+
|
|
2328
2376
|
const last = battle.side(player).last_selected_move;
|
|
2329
|
-
try options.log.status(.{ foe_ident, foe_stored.status,
|
|
2377
|
+
try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
|
|
2330
2378
|
}
|
|
2331
2379
|
|
|
2332
2380
|
fn splash(battle: anytype, player: Player, options: anytype) !void {
|
|
2333
|
-
try options.log.activate(battle.active(player), .Splash);
|
|
2381
|
+
try options.log.activate(.{ battle.active(player), .Splash });
|
|
2334
2382
|
}
|
|
2335
2383
|
|
|
2336
2384
|
fn substitute(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
|
|
2337
2385
|
var side = battle.side(player);
|
|
2338
2386
|
if (side.active.volatiles.Substitute) {
|
|
2339
|
-
return options.log.fail(.{ battle.active(player),
|
|
2387
|
+
return options.log.fail(.{ battle.active(player), .Substitute });
|
|
2340
2388
|
}
|
|
2341
2389
|
|
|
2342
2390
|
assert(side.stored().stats.hp <= 1023);
|
|
@@ -2348,16 +2396,16 @@ pub const Effects = struct {
|
|
|
2348
2396
|
// avoid having to convert to floating point)
|
|
2349
2397
|
const required_hp = if (showdown) @divFloor(side.stored().stats.hp - 1, 4) + 1 else hp;
|
|
2350
2398
|
if (side.stored().hp < required_hp) {
|
|
2351
|
-
return options.log.fail(.{ battle.active(player),
|
|
2399
|
+
return options.log.fail(.{ battle.active(player), .Weak });
|
|
2352
2400
|
}
|
|
2353
2401
|
|
|
2354
2402
|
// GLITCH: can leave the user with 0 HP (faints later) because didn't check '<=' above
|
|
2355
2403
|
side.stored().hp -= hp;
|
|
2356
2404
|
side.active.volatiles.substitute = hp + 1;
|
|
2357
2405
|
side.active.volatiles.Substitute = true;
|
|
2358
|
-
try options.log.start(battle.active(player), .Substitute);
|
|
2406
|
+
try options.log.start(.{ battle.active(player), .Substitute });
|
|
2359
2407
|
if (hp > 0) {
|
|
2360
|
-
try options.log.damage(.{ battle.active(player), side.stored(),
|
|
2408
|
+
try options.log.damage(.{ battle.active(player), side.stored(), .None });
|
|
2361
2409
|
if (showdown and side.stored().hp == 0) residual.* = false;
|
|
2362
2410
|
}
|
|
2363
2411
|
}
|
|
@@ -2366,12 +2414,10 @@ pub const Effects = struct {
|
|
|
2366
2414
|
// Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
|
|
2367
2415
|
if (showdown) {
|
|
2368
2416
|
if (battle.side(player).last_selected_move == .Teleport) return;
|
|
2369
|
-
|
|
2370
|
-
try options.chance.commit(player, .hit);
|
|
2371
|
-
}
|
|
2417
|
+
_ = try checkHit(battle, player, move, options);
|
|
2372
2418
|
battle.last_damage = 0;
|
|
2373
2419
|
} else {
|
|
2374
|
-
try options.log.fail(.{ battle.active(player),
|
|
2420
|
+
try options.log.fail(.{ battle.active(player), .None });
|
|
2375
2421
|
try options.log.laststill(.{});
|
|
2376
2422
|
}
|
|
2377
2423
|
}
|
|
@@ -2383,6 +2429,8 @@ pub const Effects = struct {
|
|
|
2383
2429
|
|
|
2384
2430
|
volatiles.Thrashing = true;
|
|
2385
2431
|
volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
2432
|
+
|
|
2433
|
+
options.chance.observe(.attacking, player, .started);
|
|
2386
2434
|
}
|
|
2387
2435
|
|
|
2388
2436
|
fn transform(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -2421,13 +2469,14 @@ pub const Effects = struct {
|
|
|
2421
2469
|
if (side.active.volatiles.Binding) return;
|
|
2422
2470
|
side.active.volatiles.Binding = true;
|
|
2423
2471
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2424
|
-
// (Pokémon Showdown
|
|
2472
|
+
// (Pokémon Showdown clears Recharging in onTryMove instead)
|
|
2425
2473
|
if (!showdown) {
|
|
2426
2474
|
foe.active.volatiles.Recharging = false;
|
|
2427
2475
|
} else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
|
|
2428
2476
|
|
|
2429
2477
|
side.active.volatiles.attacks =
|
|
2430
2478
|
try Rolls.distribution(battle, .Binding, player, options) - 1;
|
|
2479
|
+
options.chance.observe(.binding, player, .started);
|
|
2431
2480
|
}
|
|
2432
2481
|
|
|
2433
2482
|
fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2441,20 +2490,20 @@ pub const Effects = struct {
|
|
|
2441
2490
|
switch (move.effect) {
|
|
2442
2491
|
.AttackUp1, .AttackUp2, .Rage => {
|
|
2443
2492
|
assert(boosts.atk >= -6 and boosts.atk <= 6);
|
|
2444
|
-
if (boosts.atk == 6) return log.fail(.{ ident,
|
|
2493
|
+
if (boosts.atk == 6) return log.fail(.{ ident, .None });
|
|
2445
2494
|
const n: u2 = if (move.effect == .AttackUp2) 2 else 1;
|
|
2446
2495
|
boosts.atk = @as(i4, @intCast(@min(6, @as(i8, boosts.atk) + n)));
|
|
2447
|
-
const reason = if (move.effect == .Rage)
|
|
2496
|
+
const reason: Boost = if (move.effect == .Rage) .Rage else .Attack;
|
|
2448
2497
|
if (stats.atk == MAX_STAT_VALUE) {
|
|
2449
2498
|
boosts.atk -= 1;
|
|
2450
2499
|
if (showdown) {
|
|
2451
2500
|
try log.boost(.{ ident, reason, n });
|
|
2452
|
-
try log.boost(.{ ident,
|
|
2501
|
+
try log.boost(.{ ident, .Attack, -1 });
|
|
2453
2502
|
if (move.effect == .Rage) return;
|
|
2454
2503
|
}
|
|
2455
|
-
return log.fail(.{ ident,
|
|
2504
|
+
return log.fail(.{ ident, .None });
|
|
2456
2505
|
}
|
|
2457
|
-
|
|
2506
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
|
|
2458
2507
|
const stat = unmodifiedStats(battle, side).atk;
|
|
2459
2508
|
stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2460
2509
|
try log.boost(.{ ident, reason, n });
|
|
@@ -2463,65 +2512,65 @@ pub const Effects = struct {
|
|
|
2463
2512
|
},
|
|
2464
2513
|
.DefenseUp1, .DefenseUp2 => {
|
|
2465
2514
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
2466
|
-
if (boosts.def == 6) return log.fail(.{ ident,
|
|
2515
|
+
if (boosts.def == 6) return log.fail(.{ ident, .None });
|
|
2467
2516
|
const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
|
|
2468
2517
|
boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
|
|
2469
2518
|
if (stats.def == MAX_STAT_VALUE) {
|
|
2470
2519
|
boosts.def -= 1;
|
|
2471
2520
|
if (showdown) {
|
|
2472
|
-
try log.boost(.{ ident,
|
|
2473
|
-
try log.boost(.{ ident,
|
|
2521
|
+
try log.boost(.{ ident, .Defense, n });
|
|
2522
|
+
try log.boost(.{ ident, .Defense, -1 });
|
|
2474
2523
|
}
|
|
2475
|
-
return log.fail(.{ ident,
|
|
2524
|
+
return log.fail(.{ ident, .None });
|
|
2476
2525
|
}
|
|
2477
|
-
|
|
2526
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
|
|
2478
2527
|
const stat = unmodifiedStats(battle, side).def;
|
|
2479
2528
|
stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2480
|
-
try log.boost(.{ ident,
|
|
2529
|
+
try log.boost(.{ ident, .Defense, n });
|
|
2481
2530
|
},
|
|
2482
2531
|
.SpeedUp2 => {
|
|
2483
2532
|
assert(boosts.spe >= -6 and boosts.spe <= 6);
|
|
2484
|
-
if (boosts.spe == 6) return log.fail(.{ ident,
|
|
2533
|
+
if (boosts.spe == 6) return log.fail(.{ ident, .None });
|
|
2485
2534
|
boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
|
|
2486
2535
|
if (stats.spe == MAX_STAT_VALUE) {
|
|
2487
2536
|
boosts.spe -= 1;
|
|
2488
2537
|
if (showdown) {
|
|
2489
|
-
try log.boost(.{ ident,
|
|
2490
|
-
try log.boost(.{ ident,
|
|
2538
|
+
try log.boost(.{ ident, .Speed, 2 });
|
|
2539
|
+
try log.boost(.{ ident, .Speed, -1 });
|
|
2491
2540
|
}
|
|
2492
|
-
return log.fail(.{ ident,
|
|
2541
|
+
return log.fail(.{ ident, .None });
|
|
2493
2542
|
}
|
|
2494
|
-
|
|
2543
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
|
|
2495
2544
|
const stat = unmodifiedStats(battle, side).spe;
|
|
2496
2545
|
stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2497
|
-
try log.boost(.{ ident,
|
|
2546
|
+
try log.boost(.{ ident, .Speed, 2 });
|
|
2498
2547
|
},
|
|
2499
2548
|
.SpecialUp1, .SpecialUp2 => {
|
|
2500
2549
|
assert(boosts.spc >= -6 and boosts.spc <= 6);
|
|
2501
|
-
if (boosts.spc == 6) return log.fail(.{ ident,
|
|
2550
|
+
if (boosts.spc == 6) return log.fail(.{ ident, .None });
|
|
2502
2551
|
const n: u2 = if (move.effect == .SpecialUp2) 2 else 1;
|
|
2503
2552
|
boosts.spc = @intCast(@min(6, @as(i8, boosts.spc) + n));
|
|
2504
2553
|
if (stats.spc == MAX_STAT_VALUE) {
|
|
2505
2554
|
boosts.spc -= 1;
|
|
2506
2555
|
if (showdown) {
|
|
2507
|
-
try log.boost(.{ ident,
|
|
2508
|
-
try log.boost(.{ ident,
|
|
2509
|
-
try log.boost(.{ ident,
|
|
2510
|
-
try log.boost(.{ ident,
|
|
2556
|
+
try log.boost(.{ ident, .SpecialAttack, n });
|
|
2557
|
+
try log.boost(.{ ident, .SpecialAttack, -1 });
|
|
2558
|
+
try log.boost(.{ ident, .SpecialDefense, n });
|
|
2559
|
+
try log.boost(.{ ident, .SpecialDefense, -1 });
|
|
2511
2560
|
}
|
|
2512
|
-
return log.fail(.{ ident,
|
|
2561
|
+
return log.fail(.{ ident, .None });
|
|
2513
2562
|
}
|
|
2514
|
-
|
|
2563
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
|
|
2515
2564
|
const stat = unmodifiedStats(battle, side).spc;
|
|
2516
2565
|
stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2517
|
-
try log.boost(.{ ident,
|
|
2518
|
-
try log.boost(.{ ident,
|
|
2566
|
+
try log.boost(.{ ident, .SpecialAttack, n });
|
|
2567
|
+
try log.boost(.{ ident, .SpecialDefense, n });
|
|
2519
2568
|
},
|
|
2520
2569
|
.EvasionUp1 => {
|
|
2521
2570
|
assert(boosts.evasion >= -6 and boosts.evasion <= 6);
|
|
2522
|
-
if (boosts.evasion == 6) return log.fail(.{ ident,
|
|
2571
|
+
if (boosts.evasion == 6) return log.fail(.{ ident, .None });
|
|
2523
2572
|
boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
|
|
2524
|
-
try log.boost(.{ ident,
|
|
2573
|
+
try log.boost(.{ ident, .Evasion, 1 });
|
|
2525
2574
|
},
|
|
2526
2575
|
else => unreachable,
|
|
2527
2576
|
}
|
|
@@ -2536,9 +2585,7 @@ pub const Effects = struct {
|
|
|
2536
2585
|
const foe_ident = battle.active(player.foe());
|
|
2537
2586
|
const secondary = move.effect.isStatDownChance();
|
|
2538
2587
|
|
|
2539
|
-
if (foe.active.volatiles.Substitute) {
|
|
2540
|
-
return if (!secondary) log.fail(.{ foe_ident, Fail.None });
|
|
2541
|
-
}
|
|
2588
|
+
if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(.{ foe_ident, .None });
|
|
2542
2589
|
|
|
2543
2590
|
if (secondary) {
|
|
2544
2591
|
const proc = try Rolls.unboost(battle, player, options);
|
|
@@ -2546,7 +2593,6 @@ pub const Effects = struct {
|
|
|
2546
2593
|
} else {
|
|
2547
2594
|
// checkHit already checks for Invulnerable
|
|
2548
2595
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2549
|
-
try options.chance.commit(player, .hit);
|
|
2550
2596
|
}
|
|
2551
2597
|
|
|
2552
2598
|
var stats = &foe.active.stats;
|
|
@@ -2556,82 +2602,82 @@ pub const Effects = struct {
|
|
|
2556
2602
|
switch (move.effect) {
|
|
2557
2603
|
.AttackDown1, .AttackDownChance => {
|
|
2558
2604
|
assert(boosts.atk >= -6 and boosts.atk <= 6);
|
|
2559
|
-
if (boosts.atk == -6) return if (fail) try log.fail(.{ foe_ident,
|
|
2605
|
+
if (boosts.atk == -6) return if (fail) try log.fail(.{ foe_ident, .None });
|
|
2560
2606
|
boosts.atk = @intCast(@max(-6, @as(i8, boosts.atk) - 1));
|
|
2561
2607
|
if (stats.atk == 1) {
|
|
2562
2608
|
boosts.atk += 1;
|
|
2563
2609
|
if (showdown) {
|
|
2564
|
-
try log.boost(.{ foe_ident,
|
|
2565
|
-
try log.boost(.{ foe_ident,
|
|
2610
|
+
try log.boost(.{ foe_ident, .Attack, -1 });
|
|
2611
|
+
try log.boost(.{ foe_ident, .Attack, 1 });
|
|
2566
2612
|
}
|
|
2567
|
-
return log.fail(.{ foe_ident,
|
|
2613
|
+
return log.fail(.{ foe_ident, .None });
|
|
2568
2614
|
}
|
|
2569
|
-
|
|
2615
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
|
|
2570
2616
|
const stat = unmodifiedStats(battle, foe).atk;
|
|
2571
2617
|
stats.atk = @max(1, stat * mod[0] / mod[1]);
|
|
2572
|
-
try log.boost(.{ foe_ident,
|
|
2618
|
+
try log.boost(.{ foe_ident, .Attack, -1 });
|
|
2573
2619
|
},
|
|
2574
2620
|
.DefenseDown1, .DefenseDown2, .DefenseDownChance => {
|
|
2575
2621
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
2576
|
-
if (boosts.def == -6) return if (fail) try log.fail(.{ foe_ident,
|
|
2622
|
+
if (boosts.def == -6) return if (fail) try log.fail(.{ foe_ident, .None });
|
|
2577
2623
|
const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
|
|
2578
2624
|
boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
|
|
2579
2625
|
if (stats.def == 1) {
|
|
2580
2626
|
boosts.def += 1;
|
|
2581
2627
|
if (showdown) {
|
|
2582
|
-
try log.boost(.{ foe_ident,
|
|
2583
|
-
try log.boost(.{ foe_ident,
|
|
2628
|
+
try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
|
|
2629
|
+
try log.boost(.{ foe_ident, .Defense, 1 });
|
|
2584
2630
|
}
|
|
2585
|
-
return log.fail(.{ foe_ident,
|
|
2631
|
+
return log.fail(.{ foe_ident, .None });
|
|
2586
2632
|
}
|
|
2587
|
-
|
|
2633
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
|
|
2588
2634
|
const stat = unmodifiedStats(battle, foe).def;
|
|
2589
2635
|
stats.def = @max(1, stat * mod[0] / mod[1]);
|
|
2590
|
-
try log.boost(.{ foe_ident,
|
|
2636
|
+
try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
|
|
2591
2637
|
},
|
|
2592
2638
|
.SpeedDown1, .SpeedDownChance => {
|
|
2593
2639
|
assert(boosts.spe >= -6 and boosts.spe <= 6);
|
|
2594
|
-
if (boosts.spe == -6) return if (fail) try log.fail(.{ foe_ident,
|
|
2640
|
+
if (boosts.spe == -6) return if (fail) try log.fail(.{ foe_ident, .None });
|
|
2595
2641
|
boosts.spe = @intCast(@max(-6, @as(i8, boosts.spe) - 1));
|
|
2596
2642
|
if (stats.spe == 1) {
|
|
2597
2643
|
boosts.spe += 1;
|
|
2598
2644
|
if (showdown) {
|
|
2599
|
-
try log.boost(.{ foe_ident,
|
|
2600
|
-
try log.boost(.{ foe_ident,
|
|
2645
|
+
try log.boost(.{ foe_ident, .Speed, -1 });
|
|
2646
|
+
try log.boost(.{ foe_ident, .Speed, 1 });
|
|
2601
2647
|
}
|
|
2602
|
-
return log.fail(.{ foe_ident,
|
|
2648
|
+
return log.fail(.{ foe_ident, .None });
|
|
2603
2649
|
}
|
|
2604
|
-
|
|
2650
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
|
|
2605
2651
|
const stat = unmodifiedStats(battle, foe).spe;
|
|
2606
2652
|
stats.spe = @max(1, stat * mod[0] / mod[1]);
|
|
2607
|
-
try log.boost(.{ foe_ident,
|
|
2653
|
+
try log.boost(.{ foe_ident, .Speed, -1 });
|
|
2608
2654
|
assert(boosts.spe >= -6);
|
|
2609
2655
|
},
|
|
2610
2656
|
.SpecialDownChance => {
|
|
2611
2657
|
assert(boosts.spc >= -6 and boosts.spc <= 6);
|
|
2612
|
-
if (boosts.spc == -6) return if (fail) try log.fail(.{ foe_ident,
|
|
2658
|
+
if (boosts.spc == -6) return if (fail) try log.fail(.{ foe_ident, .None });
|
|
2613
2659
|
boosts.spc = @intCast(@max(-6, @as(i8, boosts.spc) - 1));
|
|
2614
2660
|
if (stats.spc == 1) {
|
|
2615
2661
|
boosts.spc += 1;
|
|
2616
2662
|
if (showdown) {
|
|
2617
|
-
try log.boost(.{ foe_ident,
|
|
2618
|
-
try log.boost(.{ foe_ident,
|
|
2619
|
-
try log.boost(.{ foe_ident,
|
|
2620
|
-
try log.boost(.{ foe_ident,
|
|
2663
|
+
try log.boost(.{ foe_ident, .SpecialAttack, -1 });
|
|
2664
|
+
try log.boost(.{ foe_ident, .SpecialAttack, 1 });
|
|
2665
|
+
try log.boost(.{ foe_ident, .SpecialDefense, -1 });
|
|
2666
|
+
try log.boost(.{ foe_ident, .SpecialDefense, 1 });
|
|
2621
2667
|
}
|
|
2622
|
-
return log.fail(.{ foe_ident,
|
|
2668
|
+
return log.fail(.{ foe_ident, .None });
|
|
2623
2669
|
}
|
|
2624
|
-
|
|
2670
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
|
|
2625
2671
|
const stat = unmodifiedStats(battle, foe).spc;
|
|
2626
2672
|
stats.spc = @max(1, stat * mod[0] / mod[1]);
|
|
2627
|
-
try log.boost(.{ foe_ident,
|
|
2628
|
-
try log.boost(.{ foe_ident,
|
|
2673
|
+
try log.boost(.{ foe_ident, .SpecialAttack, -1 });
|
|
2674
|
+
try log.boost(.{ foe_ident, .SpecialDefense, -1 });
|
|
2629
2675
|
},
|
|
2630
2676
|
.AccuracyDown1 => {
|
|
2631
2677
|
assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
|
|
2632
|
-
if (boosts.accuracy == -6) return if (fail) try log.fail(.{ foe_ident,
|
|
2678
|
+
if (boosts.accuracy == -6) return if (fail) try log.fail(.{ foe_ident, .None });
|
|
2633
2679
|
boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
|
|
2634
|
-
try log.boost(.{ foe_ident,
|
|
2680
|
+
try log.boost(.{ foe_ident, .Accuracy, -1 });
|
|
2635
2681
|
},
|
|
2636
2682
|
else => unreachable,
|
|
2637
2683
|
}
|
|
@@ -2660,35 +2706,35 @@ fn isForced(active: anytype) bool {
|
|
|
2660
2706
|
active.volatiles.Thrashing or active.volatiles.Charging;
|
|
2661
2707
|
}
|
|
2662
2708
|
|
|
2663
|
-
fn clearVolatiles(battle: anytype, who: Player,
|
|
2709
|
+
fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
|
|
2664
2710
|
var log = options.log;
|
|
2665
2711
|
var side = battle.side(who);
|
|
2666
2712
|
var volatiles = &side.active.volatiles;
|
|
2667
2713
|
const ident = battle.active(who);
|
|
2668
2714
|
|
|
2669
|
-
options.chance.clearDurations(who, clear);
|
|
2670
|
-
|
|
2671
2715
|
if (volatiles.disable_move != 0) {
|
|
2672
2716
|
volatiles.disable_move = 0;
|
|
2673
2717
|
volatiles.disable_duration = 0;
|
|
2674
|
-
|
|
2718
|
+
options.chance.observe(.disable, who, .None);
|
|
2719
|
+
try log.end(.{ ident, .DisableSilent });
|
|
2675
2720
|
}
|
|
2676
2721
|
if (volatiles.Confusion) {
|
|
2677
2722
|
// volatiles.confusion is left unchanged
|
|
2678
2723
|
volatiles.Confusion = false;
|
|
2679
|
-
|
|
2724
|
+
options.chance.observe(.confusion, who, .None);
|
|
2725
|
+
try log.end(.{ ident, .ConfusionSilent });
|
|
2680
2726
|
}
|
|
2681
2727
|
if (volatiles.Mist) {
|
|
2682
2728
|
volatiles.Mist = false;
|
|
2683
|
-
try log.end(.{ ident,
|
|
2729
|
+
try log.end(.{ ident, .MistSilent });
|
|
2684
2730
|
}
|
|
2685
2731
|
if (volatiles.FocusEnergy) {
|
|
2686
2732
|
volatiles.FocusEnergy = false;
|
|
2687
|
-
try log.end(.{ ident,
|
|
2733
|
+
try log.end(.{ ident, .FocusEnergySilent });
|
|
2688
2734
|
}
|
|
2689
2735
|
if (volatiles.LeechSeed) {
|
|
2690
2736
|
volatiles.LeechSeed = false;
|
|
2691
|
-
try log.end(.{ ident,
|
|
2737
|
+
try log.end(.{ ident, .LeechSeedSilent });
|
|
2692
2738
|
}
|
|
2693
2739
|
if (!showdown and volatiles.Toxic) {
|
|
2694
2740
|
volatiles.Toxic = false;
|
|
@@ -2696,24 +2742,39 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
|
|
|
2696
2742
|
}
|
|
2697
2743
|
if (volatiles.LightScreen) {
|
|
2698
2744
|
volatiles.LightScreen = false;
|
|
2699
|
-
try log.end(.{ ident,
|
|
2745
|
+
try log.end(.{ ident, .LightScreenSilent });
|
|
2700
2746
|
}
|
|
2701
2747
|
if (volatiles.Reflect) {
|
|
2702
2748
|
volatiles.Reflect = false;
|
|
2703
|
-
try log.end(.{ ident,
|
|
2749
|
+
try log.end(.{ ident, .ReflectSilent });
|
|
2704
2750
|
}
|
|
2705
2751
|
if (showdown and volatiles.Toxic) {
|
|
2706
2752
|
volatiles.Toxic = false;
|
|
2707
2753
|
// Pokémon Showdown erroneously clears the toxic counter
|
|
2708
2754
|
volatiles.toxic = 0;
|
|
2709
|
-
try log.end(.{ ident,
|
|
2755
|
+
try log.end(.{ ident, .ToxicSilent });
|
|
2710
2756
|
}
|
|
2711
2757
|
}
|
|
2712
2758
|
|
|
2759
|
+
fn decrement(
|
|
2760
|
+
comptime field: chance.Action.Field,
|
|
2761
|
+
player: Player,
|
|
2762
|
+
options: anytype,
|
|
2763
|
+
n: anytype,
|
|
2764
|
+
) @TypeOf(n) {
|
|
2765
|
+
return if (options.calc.overridden(player, field)) |obs| switch (obs) {
|
|
2766
|
+
.started, .ended => 0,
|
|
2767
|
+
.continuing => if (n > 1) n - 1 else n,
|
|
2768
|
+
else => unreachable,
|
|
2769
|
+
} else n - 1;
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2713
2772
|
pub const Rolls = struct {
|
|
2714
2773
|
fn speedTie(battle: anytype, options: anytype) !bool {
|
|
2715
2774
|
const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
|
|
2716
2775
|
player == .P1
|
|
2776
|
+
else if (@hasDecl(@TypeOf(battle.rng), "speedTie"))
|
|
2777
|
+
battle.rng.speedTie()
|
|
2717
2778
|
else if (showdown)
|
|
2718
2779
|
battle.rng.range(u8, 0, 2) == 0
|
|
2719
2780
|
else
|
|
@@ -2723,21 +2784,31 @@ pub const Rolls = struct {
|
|
|
2723
2784
|
return p1;
|
|
2724
2785
|
}
|
|
2725
2786
|
|
|
2726
|
-
fn criticalHit(
|
|
2787
|
+
fn criticalHit(
|
|
2788
|
+
battle: anytype,
|
|
2789
|
+
player: Player,
|
|
2790
|
+
rate: u8,
|
|
2791
|
+
spurious: bool,
|
|
2792
|
+
options: anytype,
|
|
2793
|
+
) !bool {
|
|
2727
2794
|
const crit = if (options.calc.overridden(player, .critical_hit)) |val|
|
|
2728
2795
|
val == .true
|
|
2796
|
+
else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
|
|
2797
|
+
battle.rng.criticalHit(player, rate)
|
|
2729
2798
|
else if (showdown)
|
|
2730
2799
|
battle.rng.chance(u8, rate, 256)
|
|
2731
2800
|
else
|
|
2732
2801
|
std.math.rotl(u8, battle.rng.next(), 3) < rate;
|
|
2733
2802
|
|
|
2734
|
-
try options.chance.criticalHit(player, crit, rate);
|
|
2803
|
+
if (!spurious) try options.chance.criticalHit(player, crit, rate);
|
|
2735
2804
|
return crit;
|
|
2736
2805
|
}
|
|
2737
2806
|
|
|
2738
2807
|
fn damage(battle: anytype, player: Player, options: anytype) !u8 {
|
|
2739
2808
|
const roll = if (options.calc.overridden(player, .damage)) |val|
|
|
2740
2809
|
val
|
|
2810
|
+
else if (@hasDecl(@TypeOf(battle.rng), "damage"))
|
|
2811
|
+
battle.rng.damage(player)
|
|
2741
2812
|
else roll: {
|
|
2742
2813
|
if (showdown) break :roll battle.rng.range(u8, 217, 256);
|
|
2743
2814
|
while (true) {
|
|
@@ -2751,21 +2822,27 @@ pub const Rolls = struct {
|
|
|
2751
2822
|
return roll;
|
|
2752
2823
|
}
|
|
2753
2824
|
|
|
2754
|
-
|
|
2825
|
+
const uN = if (pkmn.options.miss) u8 else u9;
|
|
2826
|
+
fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) !bool {
|
|
2827
|
+
const acc: uN = if (pkmn.options.miss) accuracy else @as(uN, accuracy) + 1;
|
|
2755
2828
|
const ok = if (options.calc.overridden(player, .hit)) |val|
|
|
2756
2829
|
val == .true
|
|
2830
|
+
else if (@hasDecl(@TypeOf(battle.rng), "hit"))
|
|
2831
|
+
battle.rng.hit(player, acc)
|
|
2757
2832
|
else if (showdown)
|
|
2758
|
-
battle.rng.chance(
|
|
2833
|
+
battle.rng.chance(uN, acc, 256)
|
|
2759
2834
|
else
|
|
2760
|
-
battle.rng.next() <
|
|
2835
|
+
battle.rng.next() < acc;
|
|
2761
2836
|
|
|
2762
|
-
options.chance.hit(ok,
|
|
2837
|
+
try options.chance.hit(player, ok, acc);
|
|
2763
2838
|
return ok;
|
|
2764
2839
|
}
|
|
2765
2840
|
|
|
2766
2841
|
fn confused(battle: anytype, player: Player, options: anytype) !bool {
|
|
2767
2842
|
const cfz = if (options.calc.overridden(player, .confused)) |val|
|
|
2768
2843
|
val == .true
|
|
2844
|
+
else if (@hasDecl(@TypeOf(battle.rng), "confused"))
|
|
2845
|
+
battle.rng.confused(player)
|
|
2769
2846
|
else if (showdown)
|
|
2770
2847
|
!battle.rng.chance(u8, 128, 256)
|
|
2771
2848
|
else
|
|
@@ -2778,6 +2855,8 @@ pub const Rolls = struct {
|
|
|
2778
2855
|
fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
|
|
2779
2856
|
const par = if (options.calc.overridden(player, .paralyzed)) |val|
|
|
2780
2857
|
val == .true
|
|
2858
|
+
else if (@hasDecl(@TypeOf(battle.rng), "paralyzed"))
|
|
2859
|
+
battle.rng.paralyzed(player)
|
|
2781
2860
|
else if (showdown)
|
|
2782
2861
|
battle.rng.chance(u8, 63, 256)
|
|
2783
2862
|
else
|
|
@@ -2790,6 +2869,8 @@ pub const Rolls = struct {
|
|
|
2790
2869
|
fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
|
|
2791
2870
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2792
2871
|
val == .true
|
|
2872
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2873
|
+
battle.rng.secondaryChance(player, 25)
|
|
2793
2874
|
else if (showdown)
|
|
2794
2875
|
battle.rng.chance(u8, 25, 256)
|
|
2795
2876
|
else
|
|
@@ -2804,6 +2885,8 @@ pub const Rolls = struct {
|
|
|
2804
2885
|
|
|
2805
2886
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2806
2887
|
val == .true
|
|
2888
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2889
|
+
battle.rng.secondaryChance(player, rate)
|
|
2807
2890
|
else if (showdown)
|
|
2808
2891
|
battle.rng.chance(u8, rate, 256)
|
|
2809
2892
|
else
|
|
@@ -2818,6 +2901,8 @@ pub const Rolls = struct {
|
|
|
2818
2901
|
|
|
2819
2902
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2820
2903
|
val == .true
|
|
2904
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2905
|
+
battle.rng.secondaryChance(player, rate)
|
|
2821
2906
|
else if (showdown)
|
|
2822
2907
|
battle.rng.chance(u8, rate, 256)
|
|
2823
2908
|
else
|
|
@@ -2830,6 +2915,8 @@ pub const Rolls = struct {
|
|
|
2830
2915
|
fn unboost(battle: anytype, player: Player, options: anytype) !bool {
|
|
2831
2916
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2832
2917
|
val == .true
|
|
2918
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2919
|
+
battle.rng.secondaryChance(player, 85)
|
|
2833
2920
|
else if (showdown)
|
|
2834
2921
|
battle.rng.chance(u8, 85, 256)
|
|
2835
2922
|
else
|
|
@@ -2842,6 +2929,8 @@ pub const Rolls = struct {
|
|
|
2842
2929
|
fn metronome(battle: anytype, player: Player, options: anytype) !Move {
|
|
2843
2930
|
const move: Move = if (options.calc.overridden(player, .metronome)) |val|
|
|
2844
2931
|
val
|
|
2932
|
+
else if (@hasDecl(@TypeOf(battle.rng), "metronome"))
|
|
2933
|
+
battle.rng.metronome(player)
|
|
2845
2934
|
else if (showdown)
|
|
2846
2935
|
Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
|
|
2847
2936
|
else move: {
|
|
@@ -2860,6 +2949,8 @@ pub const Rolls = struct {
|
|
|
2860
2949
|
fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
|
|
2861
2950
|
const power = if (options.calc.overridden(player, .psywave)) |val|
|
|
2862
2951
|
val - 1
|
|
2952
|
+
else if (@hasDecl(@TypeOf(battle.rng), "psywave"))
|
|
2953
|
+
battle.rng.psywave(player)
|
|
2863
2954
|
else if (showdown)
|
|
2864
2955
|
battle.rng.range(u8, 0, max)
|
|
2865
2956
|
else power: {
|
|
@@ -2875,8 +2966,11 @@ pub const Rolls = struct {
|
|
|
2875
2966
|
}
|
|
2876
2967
|
|
|
2877
2968
|
fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2878
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2879
|
-
@intCast(val)
|
|
2969
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2970
|
+
if (val >= 1 and val <= 7) @as(u3, @intCast(val)) else null
|
|
2971
|
+
else
|
|
2972
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
|
|
2973
|
+
battle.rng.sleepDuration(player)
|
|
2880
2974
|
else if (showdown)
|
|
2881
2975
|
battle.rng.range(u3, 1, 8)
|
|
2882
2976
|
else duration: {
|
|
@@ -2887,46 +2981,55 @@ pub const Rolls = struct {
|
|
|
2887
2981
|
};
|
|
2888
2982
|
|
|
2889
2983
|
assert(duration >= 1 and duration <= 7);
|
|
2890
|
-
options.chance.duration(
|
|
2984
|
+
options.chance.duration(player, duration);
|
|
2891
2985
|
return duration;
|
|
2892
2986
|
}
|
|
2893
2987
|
|
|
2894
2988
|
fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
|
|
2895
|
-
const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
|
|
2896
|
-
val
|
|
2989
|
+
const duration: u4 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2990
|
+
if (val >= 1 and val <= 8) val else null
|
|
2991
|
+
else
|
|
2992
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
|
|
2993
|
+
battle.rng.disableDuration(player)
|
|
2897
2994
|
else if (showdown)
|
|
2898
2995
|
battle.rng.range(u4, 1, 9)
|
|
2899
2996
|
else
|
|
2900
2997
|
@intCast((battle.rng.next() & 7) + 1);
|
|
2901
2998
|
|
|
2902
2999
|
assert(duration >= 1 and duration <= 8);
|
|
2903
|
-
options.chance.duration(
|
|
3000
|
+
options.chance.duration(player, duration);
|
|
2904
3001
|
return duration;
|
|
2905
3002
|
}
|
|
2906
3003
|
|
|
2907
|
-
fn confusionDuration(battle: anytype, player: Player,
|
|
2908
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2909
|
-
@intCast(val)
|
|
3004
|
+
fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
3005
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
3006
|
+
if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
|
|
3007
|
+
else
|
|
3008
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
|
|
3009
|
+
battle.rng.confusionDuration(player)
|
|
2910
3010
|
else if (showdown)
|
|
2911
3011
|
battle.rng.range(u3, 2, 6)
|
|
2912
3012
|
else
|
|
2913
3013
|
@intCast((battle.rng.next() & 3) + 2);
|
|
2914
3014
|
|
|
2915
3015
|
assert(duration >= 2 and duration <= 5);
|
|
2916
|
-
options.chance.duration(
|
|
3016
|
+
options.chance.duration(player, duration);
|
|
2917
3017
|
return duration;
|
|
2918
3018
|
}
|
|
2919
3019
|
|
|
2920
3020
|
fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2921
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2922
|
-
@intCast(val)
|
|
3021
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
3022
|
+
if (val >= 2 and val <= 3) @as(u3, @intCast(val)) else null
|
|
3023
|
+
else
|
|
3024
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
|
|
3025
|
+
battle.rng.attackingDuration(player)
|
|
2923
3026
|
else if (showdown)
|
|
2924
3027
|
battle.rng.range(u3, 2, 4)
|
|
2925
3028
|
else
|
|
2926
3029
|
@intCast((battle.rng.next() & 1) + 2);
|
|
2927
3030
|
|
|
2928
3031
|
assert(duration >= 2 and duration <= 3);
|
|
2929
|
-
options.chance.duration(
|
|
3032
|
+
options.chance.duration(player, duration);
|
|
2930
3033
|
return duration;
|
|
2931
3034
|
}
|
|
2932
3035
|
|
|
@@ -2939,8 +3042,11 @@ pub const Rolls = struct {
|
|
|
2939
3042
|
options: anytype,
|
|
2940
3043
|
) !u3 {
|
|
2941
3044
|
const roll = if (effect == .Binding) .duration else .multi_hit;
|
|
2942
|
-
const n: u3 = if (options.calc.overridden(player, roll)) |val|
|
|
2943
|
-
@intCast(val)
|
|
3045
|
+
const n: u3 = (if (options.calc.overridden(player, roll)) |val|
|
|
3046
|
+
if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
|
|
3047
|
+
else
|
|
3048
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "distribution"))
|
|
3049
|
+
battle.rng.distribution(player)
|
|
2944
3050
|
else if (showdown)
|
|
2945
3051
|
DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
|
|
2946
3052
|
else n: {
|
|
@@ -2950,7 +3056,7 @@ pub const Rolls = struct {
|
|
|
2950
3056
|
|
|
2951
3057
|
assert(n >= 2 and n <= 5);
|
|
2952
3058
|
if (effect == .Binding) {
|
|
2953
|
-
options.chance.duration(
|
|
3059
|
+
options.chance.duration(player, n);
|
|
2954
3060
|
} else {
|
|
2955
3061
|
try options.chance.multiHit(player, n);
|
|
2956
3062
|
}
|
|
@@ -2965,15 +3071,15 @@ pub const Rolls = struct {
|
|
|
2965
3071
|
options: anytype,
|
|
2966
3072
|
) !u4 {
|
|
2967
3073
|
// TODO: consider throwing error instead of rerolling?
|
|
2968
|
-
const
|
|
3074
|
+
const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
|
|
2969
3075
|
if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
|
|
2970
3076
|
val
|
|
2971
3077
|
else
|
|
2972
3078
|
null
|
|
2973
3079
|
else
|
|
2974
|
-
null
|
|
2975
|
-
|
|
2976
|
-
|
|
3080
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
|
|
3081
|
+
battle.rng.moveSlot(player, moves, check_pp)
|
|
3082
|
+
else slot: {
|
|
2977
3083
|
if (showdown) {
|
|
2978
3084
|
if (check_pp == 0) {
|
|
2979
3085
|
var i: usize = moves.len;
|
|
@@ -3053,11 +3159,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3053
3159
|
const side = battle.side(player);
|
|
3054
3160
|
const foe = battle.foe(player);
|
|
3055
3161
|
|
|
3056
|
-
|
|
3162
|
+
const active = &side.active;
|
|
3057
3163
|
const stored = side.stored();
|
|
3058
3164
|
|
|
3059
3165
|
// While players are not given any input options on the cartridge in these cases,
|
|
3060
|
-
// Pokémon Showdown instead produces a list with a single move that must be chosen
|
|
3166
|
+
// Pokémon Showdown instead produces a list with a single move that must be chosen
|
|
3061
3167
|
//
|
|
3062
3168
|
// Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
|
|
3063
3169
|
// here when not in compatibility mode, but the engine needs to be able to differentiate
|
|
@@ -3065,7 +3171,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3065
3171
|
// passing due to being forced into using a move. Instead of introducing another option
|
|
3066
3172
|
// we simply repurpose Move with no move slot, even though pedantically this is not
|
|
3067
3173
|
// strictly correct as the player would not have been presented the option to move or
|
|
3068
|
-
// switch at all
|
|
3174
|
+
// switch at all
|
|
3069
3175
|
if (isForced(active)) {
|
|
3070
3176
|
out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
|
|
3071
3177
|
n += 1;
|
|
@@ -3084,7 +3190,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3084
3190
|
// On the cartridge, all of these happen after "FIGHT" (indicating you are not
|
|
3085
3191
|
// switching) but before you are allowed to select a move. Pokémon Showdown instead
|
|
3086
3192
|
// either disables all other moves in the case of limited or requires you to select a
|
|
3087
|
-
// move normally if sleeping/frozen/bound
|
|
3193
|
+
// move normally if sleeping/frozen/bound
|
|
3088
3194
|
if (!showdown and (limited or foe.active.volatiles.Binding or
|
|
3089
3195
|
Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
|
|
3090
3196
|
{
|
|
@@ -3097,7 +3203,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3097
3203
|
// Pokémon Showdown handles Bide and Binding moves by checking if the move in question
|
|
3098
3204
|
// is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
|
|
3099
3205
|
// Move will not result in forcing the subsequent use unless the user also had the
|
|
3100
|
-
// proc-ed move in their moveset) and disabling all other moves
|
|
3206
|
+
// proc-ed move in their moveset) and disabling all other moves
|
|
3101
3207
|
if (limited) {
|
|
3102
3208
|
assert(showdown);
|
|
3103
3209
|
assert(side.last_selected_move != .None);
|
|
@@ -3129,7 +3235,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3129
3235
|
// Struggle (Pokémon Showdown would use 'move 1' here)
|
|
3130
3236
|
if (n == before) {
|
|
3131
3237
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
3132
|
-
if (!
|
|
3238
|
+
if (!pkmn.options.mod) {
|
|
3133
3239
|
while (slot <= 4) : (slot += 1) {
|
|
3134
3240
|
if (active.moves[slot - 1].pp != 0) return n;
|
|
3135
3241
|
}
|